aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--DevDocs/code-style.txt850
-rw-r--r--LICENCE4
-rw-r--r--README53
-rwxr-xr-xcompile6
-rw-r--r--compile.bat9
-rw-r--r--gamedata/matchmaking.txt2
-rw-r--r--gamedata/vphysics.txt10
-rw-r--r--src/ac.c27
-rw-r--r--src/accessor.h38
-rw-r--r--src/alias.c13
-rw-r--r--src/autojump.c33
-rw-r--r--src/bind.c6
-rw-r--r--src/build/gluegen.c68
-rw-r--r--src/build/mkentprops.c22
-rw-r--r--src/build/mkgamedata.c37
-rw-r--r--src/chatrate.c10
-rw-r--r--src/chunklets/msg.c4
-rw-r--r--src/clientcon.c3
-rw-r--r--src/con_.c157
-rw-r--r--src/con_.h90
-rw-r--r--src/dbg.c80
-rw-r--r--src/democustom.c7
-rw-r--r--src/demorec.c38
-rw-r--r--src/demorec.h5
-rw-r--r--src/engineapi.c11
-rw-r--r--src/engineapi.h31
-rw-r--r--src/ent.c7
-rw-r--r--src/extmalloc.c8
-rw-r--r--src/fastfwd.c15
-rw-r--r--src/fastfwd.h2
-rw-r--r--src/fixes.c29
-rw-r--r--src/fov.c29
-rw-r--r--src/gameinfo.c2
-rw-r--r--src/gameserver.c5
-rw-r--r--src/gametype.h81
-rw-r--r--src/hook.c114
-rw-r--r--src/hook.h110
-rw-r--r--src/hud.c85
-rw-r--r--src/inputhud.c52
-rw-r--r--src/kvsys.c26
-rw-r--r--src/l4d1democompat.c52
-rw-r--r--src/l4daddon.c139
-rw-r--r--src/l4dmm.c20
-rw-r--r--src/l4dreset.c50
-rw-r--r--src/l4dwarp.c31
-rw-r--r--src/langext.h2
-rw-r--r--src/nosleep.c4
-rw-r--r--src/portalcolours.c14
-rw-r--r--src/portalisg.c152
-rw-r--r--src/rinput.c36
-rw-r--r--src/sst.c161
-rw-r--r--src/trace.c8
-rw-r--r--src/vcall.h33
-rw-r--r--src/version.h4
-rw-r--r--src/wincrt.c44
-rw-r--r--src/x86.c2
-rw-r--r--src/x86.h2
-rw-r--r--src/xhair.c2
-rw-r--r--test/hook.test.c20
-rw-r--r--tools/mkbindist.bat4
-rw-r--r--tools/steamfix.bat27
-rw-r--r--tools/windbg/.gitignore1
-rw-r--r--tools/windbg/initcmds6
-rw-r--r--tools/windbg/install.ps144
-rw-r--r--tools/windbg/natvis.xml5
-rw-r--r--tools/windbg/windbg.bat16
67 files changed, 2271 insertions, 788 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
new file mode 100644
index 0000000..189d0f8
--- /dev/null
+++ b/DevDocs/code-style.txt
@@ -0,0 +1,850 @@
+════ mike’s code style guide ════
+
+I have a lot of implicit hand-wavy rules about code structure and style which I
+never really formalised anywhere. Mostly this is because I don’t believe in
+having a totally rigid set of formatting rules and instead try to make the form
+of each chunk of code the best compromise for that particular bit of code.
+
+A lot of that is based on vibes and subjective judgement. There are essentially
+no hard rules.
+
+This document aims to state some of my thoughts and feelings on such matters so
+that anyone looking to contribute patches can at least aim to approximate my
+style a little closer. It might also give some insight into why all my code is
+so awesome and/or terrible.
+
+Please don’t get too caught up in this though. It’s very unlikely that you’ll
+consistently make the exact aesthetic judgements I would make because we don’t
+share the same brain. For all intents and purposes, you should be quite happy
+not to have my brain, so don’t worry about it.
+
+Also, this guide does not pertain to the actual programming logic side of
+things. For that, you should refer to code-standards.txt , once that is written.
+The plan is to get round to that at some point eventually.
+
+══ First, the hard rules ══
+
+Okay, I lied, there actually are some hard rules, but mostly about the actual
+data on disk rather than anything stylistic:
+
+• Source files are text files. This means every line ends in with an end-of-line
+ character (\n), including the last line. If your editor mistreats the final \n
+ as an extra blank line, that’s a you problem. Lines without end-of-lines are
+ not lines and text files ending in non-lines are not text files. I will die on
+ this hill.
+
+• \r\n is not a line ending. It is an erroneous carriage return character,
+ followed by a line ending. Your source files generally should not include
+ weird non-printable characters, least of all at the end of every single line.
+ Exception: Windows batch files, which require a carriage return at the end of
+ each line to ensure that labels/gotos work correctly.
+
+• Git for Windows is configured to wildly misbehave by default. If you have not
+ set core.eol=lf and core.autocrlf=false prior to cloning this repo, and Git
+ starts mangling file contents in confusing ways, then that’s a you problem.
+
+• SST uses UTF-8-encoded source files. It should go without saying but there
+ should be no BOM. Generally comments will use ASCII only for ease of typing,
+ although we use Unicode in the copyright headers to be fancy, because we can,
+ and if you gain something by putting Unicode characters in a doc comment, then
+ hell, go for it.
+
+• String literals with Unicode in them should put the UTF-8 straight in the
+ literal rather than doing the obfuscated \U nonsense. Although, be aware that
+ the environment SST runs in is prone to completely bungling Unicode display,
+ so most user-facing strings inside the codebase are gonna be limited to ASCII.
+
+• No spaces or Unicode in file paths. That’s just obnoxious.
+
+• We use tabs for indentation, and line width guidelines (see below) assume a
+ tab width of 4 spaces, but you can configure this to 8 or something for
+ reading purposes if you prefer. This configurability is one reason we don’t
+ indent with spaces. Another reason is that text editors still behave more
+ stupid with spaces. I don’t care what anyone says about how a good editor will
+ make spaces feel exactly the same as tabs. Empirical evidence shows that that
+ is clearly bollocks.
+
+• The asterisk goes on the right in pointer declarations. This is dictated by
+ the syntax and semantics of the language. Putting it on the left is
+ misleading, and looks wrong to most C programmers — as it should, because it
+ is. People coming from C++ may have to unlearn a bad habit here, but on the
+ plus side, when they go back to C++, they can do it correctly there now too!
+
+• No spaces between function names and parentheses. Yes spaces between control
+ flow keywords and parentheses. No space padding inside parentheses. Also no
+ space padding inside braces for initialisers, although yes spaces for small
+ struct declarations. Don’t ask me why on that last one, it’s just how I do it.
+
+• Spaces around all arithmetic operators. No spaces around dots or arrows;
+ hopefully this one’s not contentious‽
+
+• Spaces between string literal pieces and/or macros representing literal
+ pieces. This one’s actually a dumb C++ one that can probably go but it’s
+ everywhere and I don’t really care so just stick to it for consistency.
+
+• No spaces between casts and castees.
+
+• Just look at the code. Do what the code does. The rest of the rules will cover
+ nebulous edge cases but these really basic things should be easy. I trust you!
+
+══ Nebulous formatting and whitespace guidelines ══
+
+In no particular order:
+
+• Usually each statement should go on its own line for obvious reasons, but very
+ simple statements that are very closely related can go together if it improves
+ the information density of the code without sacrificing clarity.
+
+ Rationale:
+ I like to maximise the amount of information being conveyed in the smallest
+ amount of space, especially on the vertical axis. Being able to fit more
+ context on screen at once makes it easier to keep track of what’s going on in
+ complex regions of code.
+
+ Bad example:
+ ++x;
+ ++y;
+ ++z;
+
+ Good example:
+ ++x; ++y; ++z;
+
+• Try and keep lines under 80 characters. If wrapping would look really stupid
+ and unbalanced and the last character on the line is not important to the
+ understanding of the line, 81 might do. In very rare cases, we can suffer 82.
+
+ Rationale:
+ People who argue about We Don’t Use 80 Character Terminals Any More™ are
+ missing the point. Really long lines are just obnoxious to read. Plus, it’s
+ nice to be able to fit 3 or 4 files on screen side-by-side (or views of the
+ same file if it’s a big one!). However, sometimes you just need to squeeze in
+ a semicolon and realistically 80 is an arbitrary limit, so it’s okay to apply
+ something like the TeX badness system where you trade one form of ugliness for
+ a lesser form. In this case, an overly long line to avoid really ugly wrapping.
+
+• Continue hard-wrapped lines with 2 tabs of indentation.
+
+ Rationale:
+ I don’t have a good reason for this. I used to do it in Java when I was a
+ 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:
+ if (cond1 &&
+ cond2) {
+ do_thing();
+ }
+
+ "Good" example:
+ if (cond1 &&
+ cond2) {
+ do_thing();
+ }
+
+• Try to hard-wrap lines at a natural reading break point. Don’t just wrap on a
+ word if you can keep an entire string literal or struct initialiser together
+ for instance. This is ultimately a subjective judgement, but do your best.
+
+ Rationale:
+ Keeping individual _things_ like strings or structs together makes it a little
+ easier to scan with the eyes. It also makes it a lot easier to grep. If you
+ break a string into lines mid-phrase, grep is ruined. If you really need to
+ break up a long string literal, at least putting the break between sentences
+ will make grepping less likely to fail.
+
+ Bad example:
+ errmsg_warn("could not do operation with rather lengthy description: failed"
+ " to lookup that important thing");
+
+ Good example:
+ errmsg_warn("could not do operation with rather lengthy description: "
+ "failed to lookup that important thing");
+
+• Hard wrap a little earlier if it avoids leaving a single dangling token which,
+ as with poorly-typeset prose, just looks stupid.
+
+ Rationale:
+ You want something substantial on each line for your eyes to lock onto. Dangly
+ diminutive tailpieces look stupid and ugly and have no friends.
+
+ Bad example:
+ call_function("parameter 1 takes up some space", param_2 + 5, another_var,
+ x);
+
+ Good example:
+ call_function("parameter 1 takes up some space", param_2 + 5,
+ another_var, x);
+
+ Maybe better example (it’s up to you):
+ call_function("parameter 1 takes up some space",
+ param_2 + 5, another_var, x);
+
+ Note: that maybe better example would become a definitely better example if
+ the middle two (or last three) parameters were related in some way.
+
+• Don’t hard wrap before punctuation. Closing parentheses, commas, semicolons,
+ conditional operators and so on should remain attached to a preceding token.
+
+ Rationale:
+ Starting a line with punctuation in code looks just as wonky to me as it would
+ in natural language prose. Ending on punctuation also primes my brain to read
+ a continuation on the next line.
+
+ Bad example:
+ if (cond1
+ && cond2)
+
+ Good example:
+ if (cond1 &&
+ cond2)
+
+• An open brace should always be on the same line as the function header or
+ conditional statement that precedes it. Even if a line is wrapped, do not
+ leave the brace on its own. Take something else with it.
+
+ Rationale:
+ I prefer the visual flow of a brace being attached to a thing and think it
+ looks kind of silly on its own line. I also strongly avoid wasting vertical
+ space in most cases.
+
+ Bad example:
+ void my_function_name()
+ {
+ // ...
+ }
+
+ Good example:
+ void my_function_name() {
+ // ...
+ }
+
+• Control flow statements should always use braces if the body is on its own
+ line, even if it’s only one statement. No classic Unix/BSD-style hanging
+ statements.
+
+ Rationale:
+ It’s harder to introduce stupid bugs if the addition of a new statement to a
+ block does not require fiddling with braces, and it’s less editing work to add
+ or remove statements when the braces are always there. Braces also provide a
+ stronger visual association with the controlling statement than just an
+ indent.
+
+ Bad example:
+ if (err)
+ invoke_error_handler(err, context, "tried to do thing and it failed!");
+
+ Good example:
+ if (err) {
+ invoke_error_handler(err, context, "tried to do thing and it failed!");
+ }
+
+• If a control flow statement and its associated single statement can fit on one
+ line, then go ahead and do that, with no need for braces. If there’s more than
+ one statement but they’re very trivial and/or closely related, a braced
+ one-liner is also alright.
+
+ Rationale:
+ This saves a ton of vertical space for trivial conditions, especially when
+ there’s a bunch of them in a row.
+
+ Bad example:
+ if (err) {
+ return false;
+ }
+ if (extrastuff) {
+ process_florbs();
+ refresh_grumbles();
+ }
+
+ Good example:
+ if (err) return false;
+ if (extrastuff) { process_florbs(); refresh_grumbles(); }
+
+• Paired if/else statements should either both be one-liners (or maybe even a
+ one-liner altogether), or both use braces. No mixing and matching.
+
+ Rationale:
+ This improves the visual rhythm and consistency when reading through the file.
+ It reduces the extent to which a reader’s eyes must jump around the screen to
+ find the correct bit of text to read.
+
+ Bad example:
+ if (!p) errmsg_warn("unexpected null value");
+ else {
+ p->nsprungles += 71;
+ defrombulate(p);
+ errmsg_note("we do be defrombulating them sprungles!!!");
+ }
+
+ Good example:
+ if (!p) {
+ errmsg_warn("unexpected null value");
+ }
+ else {
+ p->nsprungles += 71;
+ defrombulate(p);
+ errmsg_note("we do be defrombulating them sprungles!!!");
+ }
+
+• When putting the else on its own line, put it on its own line. Don’t hug the
+ closing brace of the if.
+
+ 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
+ out temporarily for debugging and such.
+
+ Bad example:
+ if (cond) {
+ do_some_cool_stuff();
+ } else {
+ do_something_else();
+ }
+
+ Good example:
+ if (cond) {
+ do_some_cool_stuff();
+ }
+ else {
+ do_something_else();
+ }
+
+• If there are a bunch of related conditionals in a row, and only some of them
+ fit on one line, consider keeping them all braced to improve the visual
+ symmetry.
+
+ Rationale:
+ This reads better, similarly to the if-else example, and is easier to edit.
+
+ Bad example:
+ if (cond1) return;
+ if (cond2 || flag3 == THIS_IS_VERY_LONG_BY_COMPARISON) {
+ do_other_thing(false);
+ }
+ if (cond3 || flag3 == SHORTER_ONE) continue;
+
+ Good example (note that this one is way more subjective and case-by-case!):
+ if (cond1) {
+ return;
+ }
+ if (cond2 || flag3 == THIS_IS_VERY_LONG_BY_COMPARISON) {
+ do_other_thing(false);
+ }
+ if (cond3 || flag3 == SHORTER_ONE) {
+ continue;
+ }
+
+• Favour inter-line visual symmetry over compactness of any individual line.
+
+ Rationale:
+ Basically a generalised form of many of these other rules. It seemed like a
+ good idea to just state this outright. Plus, it lets me give a very specific
+ example that would be hard to shoehorn in elsewhere.
+
+ Bad example:
+ x[0] = y[0]; x[1] = y[1]; x[2] = y[2]; x[3] = y[3]; x[4] = y[4];
+ x[5] = y[5]; x[6] = y[6]; x[7] = y[7];
+
+ Good example:
+ x[0] = y[0]; x[1] = y[1]; x[2] = y[2]; x[3] = y[3];
+ x[4] = y[4]; x[5] = y[5]; x[6] = y[6]; x[7] = y[7];
+
+• Spread large struct initialisers over multiple lines. Don’t do the usual
+ two-tab hard wrapping, but instead do a brace indent thing and put each member
+ on a line.
+
+ Rationale:
+ Visual weight and symmetry, and ease of amendment. As usual.
+
+ Bad example:
+ struct gizmo g = {"this string exists to make the example line long!", 15,
+ sizeof(whatever)};
+
+ Good example:
+ struct gizmo g = {
+ "this string exists to make the example line long!",
+ 15,
+ sizeof(whatever)
+ };
+
+• As an exception to the above, very closely related things might go on the same
+ line. Use judgement as to whether that grouping makes things clearer or worse.
+
+ Rationale:
+ Hopefully by now the theme is emerging that I value hard rule consistency less
+ than moment-to-moment clarity. In cases where most things are on different
+ lines, grouping things on the same line can sometimes aid clarity and/or
+ information density. It’s the same idea as grouping statements/declarations.
+
+ Examples: N/A. This is way too subjective and situational to make up a
+ contrived example. But you’ll see it sometimes in my code and you’re welcome
+ to do it yourself whenever the vibes are right. It’s all just vibes.
+
+• Declare multiple *related* variables of the same type on one line as a
+ comma-separated list.
+
+ Rationale:
+ This makes things more terse and compact in cases when the grouping is
+ obvious. As soon as the declarations need to span multiple lines, there seems
+ to be less benefit to doing this, but in cases where there’s a small number of
+ things and the compaction creates no real loss of clarity, it makes sense.
+
+ Bad example:
+ int a;
+ int b;
+ int c;
+ char *x;
+ char *y;
+
+ Good example (assuming these things are related):
+ int a, b, c;
+ char *x, *y;
+
+• Cram goto labels into the indent margins, on the same line as the actual code.
+
+ Rationale:
+ Labels can introduce visual noise, especially in the case of many cascading/
+ unwinding error cleanup handlers. Considering that goto use tends to be
+ limited, it doesn’t seem that important to give highly descriptive names to
+ labels.
+
+ Bad example:
+ void myfunc() {
+ if (!thing1()) goto err_1;
+ if (!thing2()) goto err_2;
+ return true;
+
+ err_2:
+ cleanup1();
+ err_1:
+ cleanup2();
+ return false;
+ }
+
+ Good example:
+ void myfunc() {
+ if (!thing1()) goto e1;
+ if (!thing2()) goto e2;
+ return true;
+
+ e2: cleanup1();
+ e1: cleanup2();
+ return false;
+ }
+
+• Don’t indent preprocessor directives.
+
+ Rationale:
+ The preprocessor is its own meta language and it’s not really helpful to
+ intermingle it with the main control flow syntax. It also just kind of looks
+ subjectively messy to have preprocessor stuff reaching far across the screen.
+ I do see the argument for indenting nested ifdefs, but usually there’s few
+ enough of them that it won’t be that confusing either way and in cases where
+ there’s a lot of nesting… well, you have bigger problems.
+
+ Bad example:
+ #ifndef _WIN32
+ #ifdef __LP64__
+ #define IS64BIT 1
+ #else
+ #define IS64BIT 0
+ #endif
+ #endif
+
+ Good example:
+ #ifndef _WIN32
+ #ifdef __LP64__
+ #define IS64BIT 1
+ #else
+ #define IS64BIT 0
+ #endif
+ #endif
+
+• Use C++/C99-style comments for explanatory notes, and classic C-style comments
+ for documentation in headers etc. As an exception, C-style comments can be
+ used inside multi-line macro bodies with line continuation, but should not be
+ padded with extra asterisks in such cases.
+
+ Rationale:
+ It’s nice to have a way to quickly visually distinguish public documentation
+ from internal notes. To me, the single-line comments have a bit less formality
+ about them, so they’re good for quickly jotting something down, whereas the
+ big banner comments stand out a bit more like formal declarations of important
+ information.
+
+ Bad example:
+ // This function computes the sum of two integers. It assumes the result
+ // will not overflow.
+ int sum(int x, int y) {
+ return x + y; /* no need for a comment here but this is an example! */
+ }
+
+ #define DO_INTERESTING_CALCULATION() do { \
+ /*
+ * There's quite a lot to explain here. We not only have to increment
+ * one of the numbers, but we also have to multiply the result by
+ * another number, and then multiply that by yet another number, and add
+ * 9 to that. This behaviour is specified in detail in one of the WHATWG
+ * specs, presumably.
+ */ \
+ ++foo; \
+ bar *= foo; \
+ baz = baz * bar + 9; \
+ } while (0)
+
+ Good example:
+ /*
+ * This function computes the sum of two integers. It assumes the result
+ * will not overflow.
+ */
+ int sum(int x, int y) {
+ return x + y; // no need for a comment here but this is an example!
+ }
+
+ #define DO_INTERESTING_CALCULATION() do { \
+ /* There's quite a lot to explain here. We not only have to increment
+ one of the numbers, but we also have to multiply the result by
+ another number. I won't repeat the entire stupid joke again. */ \
+ ++foo; \
+ bar *= foo; \
+ baz = baz * bar + 9; \
+ } while (0)
+
+ Caveat: note that generally the /**/ documentation comments should also go
+ in the header rather than the implementation file, as this allows for both
+ easy manual reading and LSP hover support, but whatever, you get the idea.
+
+• Put short comments pertaining to single lines of code at the ends of those
+ lines if you can make them fit. Otherwise, write them above the relevant code.
+ Do not continue an end-of-line comment onto an over-indented blank line.
+
+ Rationale:
+ Probably obvious by now. Don’t waste space. Having comments hang off the ends
+ of lines and melt down the right-hand side of the screen not only wastes space
+ but also makes you look like a child writing with crayons.
+
+ Bad example:
+ if (cond1 || cond2) reasonably_long_call(param1); // either conditiom can
+ // trigger this here due
+ // to that thing I
+ // mentioned.
+ Good example:
+ // either condition cam 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
+ config to stop this dangling idiocy from happening when you take a new line.
+
+══ The second hard problem in computer science ══
+
+… Naming things!
+
+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:
+ This makes it trivial to look things up in the official SDK sources. Since the
+ naming here differs greatly from the naming elsewhere it stands out as being
+ its own thing. There are some exceptions, notably the con_var and con_cmd
+ structs which are our names for ConVar and ConCommand. The reasoning for those
+ is that they are effectively part of SST’s internal API, so we’ve made them
+ stylistically our own.
+
+ Bad example:
+ struct baseent {
+ void *vtable;
+ // ...
+ };
+
+ Good example:
+ struct CBaseEntity {
+ void *vtable;
+ // ...
+ };
+
+• Generally, prefer exact names for SDK functions too.
+
+ Rationale:
+ Same as above; it makes it obvious which bit of the engine we’re referring to.
+
+ Bad example:
+ DECL_VFUNC(struct CGameMovement, bool, checkjump)
+
+ Good example:
+ DECL_VFUNC(struct CGameMovement, bool, CheckJumpButton)
+
+• Don’t bother with exact names for struct members or other variables.
+
+ Rationale:
+ A lot of the time we’re poking into internals and in many cases the variables
+ aren’t even at consistent byte offsets from one game version to the next (see
+ also gamedata files). Many of Valve’s chosen variable names are also very
+ annoying and violate every other guideline you’re about to see, so at the
+ granularity of local variables I made the decision long ago to write whatever
+ names made sense to me and not worry about it.
+
+ Bad example:
+ int buttons = mv->m_iButtons;
+
+ Good example:
+ int buttons = mv->buttons;
+
+The rest of these points apply to first-party code, i.e., SST itself:
+
+• Functions, variable names and struct/enum tags should all be lowercase.
+
+ Rationale:
+ TitleCase is annoying to type and camelCase looks ugly. Most C code uses
+ lowercase stuff so this looks the most familiar to most people, probably.
+
+ Bad example:
+ int myVar;
+ struct MyStruct;
+
+ Good example:
+ int myvar;
+ struct mystruct;
+
+• typedefs should almost never be used, except for base integer type
+ abbrevations (intdefs.h) and function pointers. The latter are annoying to
+ write out over and over so the typical convention (in cases like hooking) is
+ to declare a FunctionName_func and then use that everywhere else. Even when
+ dealing with e.g. Windows APIs which typedef the hell out of everything, try
+ to avoid actually using those typedefs unless your hand is truly forced.
+
+ Rationale:
+ C doesn’t treat typedefs as different types, so all typedefs really achieve on
+ numeric and pointer types is obfuscation. In the case of structs, I always
+ liked having the separate struct namespace, and using a typedef obviously goes
+ against that. In more recent times I have *somewhat* thought about changing my
+ mind on that, but it’d be a big change and I still don’t feel that strongly in
+ favour of it. Also, since everything is lowercase, the struct keyword does
+ help disambiguate a bit. If we were gonna typedef structs, there’d probably
+ also be some discussion about using TitleCase or something.
+
+• const definitions and enum values should be uppercase.
+
+ Rationale:
+ This distinguishes them from variables, indicating that they are essentially
+ build-time symbols that get substituted by the compiler, similar to macros
+ (see also the next point).
+
+ Bad example: static const int maxents = 64;
+ Good example: static const int MAXENTS = 64;
+
+ 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.
+
+• 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
+ it should be uppercase to indicate that it’s effectively also a symbolic
+ constant. If your macro essentially behaves as an inline function, then it
+ should be lowercase, although you should also consider just writing an inline
+ function instead. If your macro aims to extend the syntax of C like the things
+ in langext.h, it can also be lowercase in order to look like a real keyword.
+
+ Rationale: I’m very accustomed to using uppercase for constants but not all
+ macros behave as constants. My usual goal with macros is for the interface not
+ to feel like macros, even if the implementation is macros. That principle
+ becomes really important when you see how horrendous some of the macros can
+ get under the hood.
+
+ Bad example:
+ #define MyConstant 4
+ #define FOREACH_THING(x) /* impl here */
+ #define doerrorcheck(var) do if (!var) die("couldn't get " #var); while (0)
+
+ Good example:
+ #define MYCONSTANT 4
+ #define foreach_thing(x) /* impl here */
+ #define DOERRORCHECK(var) do if (!var) die("couldn't get " #var); while (0)
+
+• A few special things actually do use TitleCase, namely event names and
+ gametype tags.
+
+ Rationale:
+ These are sort of special things, and always passed into macros. They’re not
+ regular tokens that you’d see anywhere else in the code, so it sort of felt
+ right to give them a bit of a different style. Honestly, it’s all just vibes.
+
+ Bad example:
+ DEF_EVENT(redraw_copilot_widget)
+ X(portal4) /* (in GAMETYPE_BASETAGS in gametype.h) */
+
+ Good example:
+ DEF_EVENT(RedrawCopilotWidget)
+ X(Portal4) /* (in GAMETYPE_BASETAGS in gametype.h) */
+
+• Prefer the shortest name you can get away with. If you feel that a longer name
+ is necessary for understanding, that is okay. But if you can abbreviate
+ without losing too much clarity, then that is a good idea. If your variable is
+ local to a small scope you can probably get away with an even shorter name.
+
+ Rationale:
+ The only research I’ve seen on the topic indicates that longer names do not
+ meaningfully quicken debugging speed even for developers unfamiliar with a
+ codebase. Since I prefer higher information density, I strongly prefer
+ cramming more structure into the same amount of space via smaller individual
+ tokens, rather than having longer and more redundant names. You’ll see a ton
+ of abbreviations in this codebase and some of them might seem a bit obtuse to
+ begin with, but it seems that other people have been able to get used to that.
+
+ Bad example:
+ bool success = check_conditions(has_required_data, parse_integer(input));
+
+ Good example:
+ bool ok = checkcond(has_data, parseint(input));
+
+• Try to make a name slightly shorter again if you can avoid wrapping.
+
+ Rationale: all the previous whitespace rules aiming to minimise ugliness can
+ often be aided by fudging the actual text a bit. See also: Tom 7’s “Badness 0”
+ from SIGBOVIK 2024. https://youtu.be/Y65FRxE7uMc
+
+ Examples: can’t be bothered contriving one of these, but watch that video,
+ it’s very funny.
+
+• Underscores are used for namespaces and semantic groupings. Don’t use them as
+ spaces. Having two or three words joined without spaces is perfectly fine.
+
+ Rationale:
+ Overusing underscores leads to long verbose names, which goes against the
+ previous guideline. Calling a variable “gametypeid” instead of “game_type_id”
+ is not, in my opinion, that much less readable, and it allows underscores to
+ be reserved for a more specific purpose. This is not a hard rule in the event
+ that you absolutely insist that an underscore looks significantly better, but
+ be prepared for me to ignore your insistence some percentage of the time.
+
+ Examples:
+ Exactly the same as above, really. Note the lack of underscores in the good
+ example; has_data is an exception because we have a has_ convention throughout
+ the codebase; it is essentially a namespace.
+
+• External-linkage (aka public) functions, variables, and structures should be
+ prefixed with the module name, which is the source file basename. For
+ instance, any part of the API defined in ent.c should generally be named
+ ent_something. The same idea goes for uppercase names of macros and enum
+ values, usually.
+
+ Rationale:
+ This makes it extremely easy to tell where something is defined at a glance.
+ It also encourages thinking about where stuff should go, since the best name
+ for something will kind of dictate where it belongs *and* vice-versa.
+
+ Examples: N/A. This should be blindingly obvious, come on.
+
+• Special macros that define or declare things will begin with DEF_ or DECL_,
+ respectively, rather than following the above rule. Again, there are no hard
+ rules. Event handlers use HANDLE_EVENT and the feature.h macros just do
+ whatever.
+
+ Rationale:
+ In these specific cases, clarity was thought to be aided by having the purpose
+ of the macro front and centre, and in the case of DEF/DECL, by grouping them
+ semantically with other similar macros across the codebase. Most macros will
+ not be subject to such exceptions, but like everything in this codebase, you
+ must always allow for exceptions, except in cases where you shouldn’t.
+
+ Examples: N/A.
+
+• Stuff that’s forced to go in public headers but isn’t supposed to be part of
+ the interface should be prefixed with an underscore.
+
+ Rationale: this communicates that it’s unstable private API, and also stops it
+ from showing up unhelpfully in autocompletion results.
+
+ Bad example: int mything_lookupinternalthingmy();
+ Good example: int _mything_lookupinternalthingmy();
+
+══ General naming conventions ══
+
+We have a few recurring patterns in the source, some of which are even required
+for certain macros to work correctly! These include:
+
+• vtidx_* for virtual table indices, used by DECL_VFUNC_DYN.
+• off_* for offsets to member variables, used by DEF_ACCESSORS.
+• sz_* for sizes of structs/classes, used by DEF_ARRAYIDX_ACCESSOR.
+• has_* in front of any generated gamedata/entprops value, to indicate the
+ presence or absence of a known value at runtime, in the current game/engine.
+ Also used in front of a feature name when REQUEST() is specified.
+
+Some other common prefixes and suffixes are just good for making the whole thing
+feel coherent to a human reader even if nothing really requires them:
+
+• *_func for function pointer typedefs.
+• orig_* for a function pointer used to call an original function from a hook.
+• hook_* for the hook function itself, which intercepts calls to the original.
+• find_* for a function within a feature which attempts to find some variable or
+ function buried within the engine, usually by chasing pointers through machine
+ instructions or internal engine data structures or something.
+
+And there’s a bunch of abbreviations I tend to use often without thinking, which
+I’ve been told in the past may be confusing to new players:
+
+• ctxt for context
+• sz for size
+• cb for callback
+• msg for message
+• cmd for command
+• insns for instructions
+• ret or sometimes r for any return value
+• i for a loop counter or temporary variable
+• p or maybe ptr for a temporary pointer
+• the first letter of any type name for a temporary variable in a narrow scope
+
+Hopefully you get the idea: there’s a general tendency toward terseness. Usually
+the names don’t actually matter that much, to be honest. Most of the guidelines
+around naming here have to do with upholding an overall structure and consistent
+feeling when reading the codebase rather than worrying about the giving every
+individual thing the perfect name. If one thing gets a really dumb name, the sky
+will still be up there. You can always rename it later. The thing I mean, not
+the sky. And also a sky by any name would smell just as sweet I mean a rose.
+
+══ Miscellany ══
+
+• Trivial but worth mentioning: don’t write void for zero-argument functions.
+
+ Rationale: This is a C23 codebase. () is adequate.
+ Bad example: void stub(void);
+ Good example: void stub();
+
+• Do still use {0} for default struct initialisation, rather than {}.
+
+ Rationale: C23 is still not that widely supported and I don’t want to demand a
+ super-cutting-edge compiler version, in case something breaks. Eventually,
+ after the dust has settled, this rule will be flipped over.
+
+ Bad example: struct gizmo g = {}
+ Good example: struct gizmo g = {0};
+
+• Always put the & in front of a function when creating a function pointer.
+
+ Rationale: it’s a pointer, innit. No need to rely on legacy implicit weirdness.
+ Bad example: regcb(cb);
+ Good example: regcb(&cb);
+
+That’s about it, I think.
+
+Thanks, and have fun!
+
+────────────────────────────────────────────────────────────────────────────────
+Copyright © Michael Smith <mikesmiffy128@gmail.com>
+
+Permission to use, copy, modify, and/or distribute this documentation file for
+any purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THIS DOCUMENTATION IS PROVIDED “AS-IS” AND HAS NO WARRANTY.
diff --git a/LICENCE b/LICENCE
index ebecc17..f28241a 100644
--- a/LICENCE
+++ b/LICENCE
@@ -22,6 +22,10 @@ the copyright notices in individual files for full details.
The README file is also in the public domain.
+Documentation files under DevDocs/ are generally provided under similar terms to
+the source code, with slightly different wording; refer to the notices at the
+ends of individual files for details.
+
Files under src/3p/ are written and licensed by third parties. See the copyright
notices in and alongside those files for details of authorship and relevant
redistribution rights. Some — but not all — of this third party code is
diff --git a/README b/README
index 9669078..af0b2b6 100644
--- a/README
+++ b/README
@@ -10,21 +10,42 @@ Windows:
• Install the Windows 10 SDK and MSVC toolset via the Visual Studio Installer
(if you don’t care what else gets installed, installing the C++ Desktop
workload should be fine).
- • Install native Clang from https://clang.llvm.org (NOT MinGW/MSYS2 Clang!).
+ • Install native Clang from the LLVM GitHub releases page. (NOT MinGW/MSYS2
+ Clang!). C23 support is required; at the time of writing Clang 16 worked, but
+ Clang 20 is being used now. If you get syntax errors or unexpected keywords
+ or something, your compiler is probably too old.
• Run compile.bat (in lieu of a better build tool, to be added later).
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 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.
+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.
+
+════ 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 ════
@@ -41,15 +62,17 @@ left4dead2/, hl2/. Left 4 Dead and later branches *ALSO* try to load from the
top-level game directory where the EXE is, if other paths don’t work.
Since this plugin is designed to be universal, a reasonable recommendation is to
-always put it in bin/ and then use the command `plugin_load ../bin/sst`. The way
-the paths work out, that always works no matter what, thus taking all the
-thought out of it. It’s also a sensible way to share the plugin between multiple
-mods in an engine installation, where relevant.
-
-It’s additionally 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 especially handy if you’re building from source
-and don’t want to copy it over every time.
+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
+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
+games. The way the build scripts are written, you can rebuild the plugin
+in-place before reloading it from the game console. It can be helpful to write
+some console aliases or set up a bind to reload the plugin quickly on-the-fly.
Note: some very old (and very new) Source builds don’t have a plugin_load
command. For the time being, these versions are unsupported.
diff --git a/compile b/compile
index d3aa259..a15605b 100755
--- a/compile
+++ b/compile
@@ -25,7 +25,7 @@ stdflags="-std=c2x -D_DEFAULT_SOURCE -D_FILE_OFFSET_BITS=64 -D_TIME_BITS=64"
dbg=0
if [ "$dbg" = 1 ]; then
- cflags="-O0 -g3"
+ cflags="-O0 -g3 -fsanitize-trap=undefined -DSST_DBG"
ldflags="-O0 -g3"
else
cflags="-O2 -fvisibility=hidden"
@@ -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
@@ -110,7 +110,7 @@ ld
$HOSTCC -O2 -g3 $warnings $stdflags -include test/test.h -o .build/bitbuf.test test/bitbuf.test.c
.build/bitbuf.test
-# skipping this test on linux for now, since inline hooks aren't compiled in
+# XXX: skipping this test on linux for now but should enable when we can test it
#$HOSTCC -m32 -O2 -g3 -include test/test.h -o .build/hook.test test/hook.test.c
#.build/hook.test
$HOSTCC -O2 -g3 $warnings $stdflags -include test/test.h -o .build/kv.test test/kv.test.c
diff --git a/compile.bat b/compile.bat
index a756576..a9ccf44 100644
--- a/compile.bat
+++ b/compile.bat
@@ -26,7 +26,7 @@ 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
+ set cflags=-O0 -g3 -fsanitize-trap=undefined -DSST_DBG
set ldflags=-O0 -g3
) else (
set cflags=-O2
@@ -43,10 +43,6 @@ set dmodname= -DMODULE_NAME=%basename%
if "%dmodname%"==" -DMODULE_NAME=con_" set dmodname= -DMODULE_NAME=con
if "%dmodname%"==" -DMODULE_NAME=sst" set dmodname=
set objs=%objs% .build/%basename%.o
-:: note: we use a couple of C23 things now because otherwise we'd have to wait a
-:: year to get anything done. typeof=__typeof prevents pedantic warnings caused
-:: by typeof still technically being an extension, and stdbool gives us
-:: predefined bool/true/false before compilers start doing that by default
%CC% -c -flto -mno-stack-arg-probe %cflags% %warnings% %stdflags% -I.build/include ^
-D_DLL%dmodname% -o .build/%basename%.o %1 || goto :end
goto :eof
@@ -98,6 +94,7 @@ setlocal DisableDelayedExpansion
:+ nosleep.c
:+ os.c
:+ portalcolours.c
+:+ portalisg.c
:+ rinput.c
:+ sst.c
:+ trace.c
@@ -126,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/matchmaking.txt b/gamedata/matchmaking.txt
index 2c93120..0713b56 100644
--- a/gamedata/matchmaking.txt
+++ b/gamedata/matchmaking.txt
@@ -1,6 +1,8 @@
# IMatchFramework
vtidx_GetMatchNetworkMsgController
L4D 10 # NOTE: probably same for aswarm or p2 except with IAppSystem shift
+
+# IMatchNetworkMsgController
vtidx_GetActiveGameServerDetails
L4D 1
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 32309c5..00edaed 100644
--- a/src/ac.c
+++ b/src/ac.c
@@ -281,8 +281,11 @@ struct inputevent {
int data, data2, data3;
};
-DECL_VFUNC_DYN(void, GetDesktopResolution, int *, int *)
-DECL_VFUNC_DYN(void, DispatchAllStoredGameMessages)
+struct IGameUIFuncs { void **vtable; };
+struct IGame { void **vtable; };
+
+DECL_VFUNC_DYN(struct IGameUIFuncs, void, GetDesktopResolution, int *, int *)
+DECL_VFUNC_DYN(struct IGame, void, DispatchAllStoredGameMessages)
typedef void (*Key_Event_func)(struct inputevent *);
static Key_Event_func orig_Key_Event;
@@ -314,13 +317,14 @@ static bool find_Key_Event() {
// -> IGame/CGame (first mov into ECX)
// -> CGame::DispatchAllStoredGameMessages vfunc
// -> First call instruction (either DispatchInputEvent or Key_Event)
- void *gameuifuncs = factory_engine("VENGINE_GAMEUIFUNCS_VERSION005", 0);
+ struct IGameUIFuncs *gameuifuncs = factory_engine(
+ "VENGINE_GAMEUIFUNCS_VERSION005", 0);
if_cold (!gameuifuncs) {
errmsg_errorx("couldn't get engine game UI interface");
return false;
}
- void *cgame;
- const uchar *insns = (const uchar *)VFUNC(gameuifuncs, GetDesktopResolution);
+ struct IGame *cgame;
+ const uchar *insns = gameuifuncs->vtable[vtidx_GetDesktopResolution];
for (const uchar *p = insns; p - insns < 16;) {
if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) {
void **indirect = mem_loadptr(p + 2);
@@ -332,7 +336,7 @@ static bool find_Key_Event() {
errmsg_errorx("couldn't find pointer to CGame instance");
return false;
-ok: insns = (const uchar *)VFUNC(cgame, DispatchAllStoredGameMessages);
+ok: insns = cgame->vtable[vtidx_DispatchAllStoredGameMessages];
for (const uchar *p = insns; p - insns < 128;) {
if (p[0] == X86_CALL) {
orig_Key_Event = (Key_Event_func)(p + 5 + mem_loads32(p + 1));
@@ -380,12 +384,9 @@ HANDLE_EVENT(PluginUnloaded) {
INIT {
if_cold (!find_Key_Event()) return FEAT_INCOMPAT;
- orig_Key_Event = (Key_Event_func)hook_inline((void *)orig_Key_Event,
- (void *)&hook_Key_Event);
- if_cold (!orig_Key_Event) {
- errmsg_errorsys("couldn't hook Key_Event function");
- return FEAT_FAIL;
- }
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_Key_Event, (void **)&orig_Key_Event, "Key_Event");
+ if_cold (h.err) return h.err;
#ifdef _WIN32
keybox = VirtualAlloc(0, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
@@ -423,6 +424,7 @@ INIT {
// run of bytes
memcpy(keybox->lbpub, lbpubkeys[LBPK_L4D], 32);
}
+ hook_inline_commit(h.prologue, (void *)hook_Key_Event);
return FEAT_OK;
#ifdef _WIN32
@@ -430,7 +432,6 @@ e: VirtualFree(keybox, 4096, MEM_RELEASE);
#else
e: munmap(keybox, 4096);
#endif
- unhook_inline((void *)orig_Key_Event);
return FEAT_FAIL;
}
diff --git a/src/accessor.h b/src/accessor.h
index dcd9f28..ec63b3c 100644
--- a/src/accessor.h
+++ b/src/accessor.h
@@ -27,15 +27,16 @@
#endif
/*
- * Defines a function to offset a pointer from a struct/class to a field based
+ * Defines a function to offset a pointer from a struct/class to a member based
* on a corresponding offset value off_<field>. Such an offset would be
* generally defined in gamedata. The function will be named getptr_<field>.
* Essentially allows easy access to an opaque thing contained with another
* opaque thing.
*/
-#define DEF_PTR_ACCESSOR(type, field) \
- _ACCESSOR_UNUSED static inline typeof(type) *getptr_##field(void *obj) { \
- return mem_offset(obj, off_##field); \
+#define DEF_PTR_ACCESSOR(class, type, member) \
+ _ACCESSOR_UNUSED static inline typeof(type) *getptr_##member( \
+ typeof(class) *obj) { \
+ return mem_offset(obj, off_##member); \
}
/*
@@ -43,21 +44,22 @@
* Requires that the field type is complete - that is, either scalar or a fully
* defined struct.
*/
-#define DEF_ACCESSORS(type, field) \
- DEF_PTR_ACCESSOR(type, field) \
- _ACCESSOR_UNUSED static inline typeof(type) get_##field(const void *obj) { \
- return *getptr_##field((void *)obj); \
+#define DEF_ACCESSORS(class, type, member) \
+ DEF_PTR_ACCESSOR(class, type, member) \
+ _ACCESSOR_UNUSED static inline typeof(type) get_##member( \
+ const typeof(class) *obj) { \
+ return *getptr_##member((typeof(class) *)obj); \
} \
- _ACCESSOR_UNUSED static inline void set_##field(const void *obj, \
+ _ACCESSOR_UNUSED static inline void set_##member(typeof(class) *obj, \
typeof(type) val) { \
- *getptr_##field((void *)obj) = val; \
+ *getptr_##member(obj) = val; \
}
/*
- * Defines an array indexing function arrayidx_<class> which allows offsetting
- * an opaque pointer by sz_<class> bytes. This size value would generally be
- * defined in gamedata. Allows iterating over structs/classes with sizes that
- * vary by game and are thus unknown at compile time.
+ * Defines an array indexing function arrayidx_<classname> which allows
+ * offsetting an opaque pointer by sz_<classname> bytes. This size value would
+ * generally be defined in gamedata. Allows iterating over structs/classes with
+ * sizes that vary by game and are thus unknown at compile time.
*
* Note that idx is signed so this can also be used for relative pointer offsets
* in either direction.
@@ -68,10 +70,10 @@
* single load of the global followed by repeated addition with no need for
* multiplication, given that we use LTO, so... don't worry about it! It's fine!
*/
-#define DEF_ARRAYIDX_ACCESSOR(class) \
- _ACCESSOR_UNUSED static inline struct class *arrayidx_##class(void *array, \
- ssize idx) { \
- return mem_offset(array, idx * sz_##class); \
+#define DEF_ARRAYIDX_ACCESSOR(type, classname) \
+ _ACCESSOR_UNUSED static inline typeof(type) *arrayidx_##classname( \
+ typeof(type) *array, ssize idx) { \
+ return mem_offset(array, idx * sz_##classname); \
}
#endif
diff --git a/src/alias.c b/src/alias.c
index 300ea10..697e97c 100644
--- a/src/alias.c
+++ b/src/alias.c
@@ -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 a8a21ca..eb0e34f 100644
--- a/src/autojump.c
+++ b/src/autojump.c
@@ -24,7 +24,6 @@
#include "hook.h"
#include "intdefs.h"
#include "langext.h"
-#include "mem.h"
#include "os.h"
#include "vcall.h"
@@ -33,20 +32,21 @@ REQUIRE_GAMEDATA(off_mv)
REQUIRE_GAMEDATA(vtidx_CheckJumpButton)
REQUIRE_GLOBAL(factory_client) // note: server will never be null
-DEF_ACCESSORS(struct CMoveData *, mv)
+struct CGameMovement { void **vtable; };
+DEF_ACCESSORS(struct CGameMovement, struct CMoveData *, mv)
-DEF_FEAT_CVAR(sst_autojump, "Jump upon hitting the ground while holding space", 0,
- CON_REPLICATE | CON_DEMO)
+DEF_FEAT_CVAR(sst_autojump, "Jump upon hitting the ground while holding space",
+ 0, CON_REPLICATE | CON_DEMO)
#define NIDX 256 // *completely* arbitrary lol
static bool justjumped[NIDX] = {0};
static inline int handleidx(ulong h) { return h & (1 << 11) - 1; }
-static void *gmsv = 0, *gmcl = 0;
-typedef bool (*VCALLCONV CheckJumpButton_func)(void *);
+static struct CGameMovement *gmsv = 0, *gmcl = 0;
+typedef bool (*VCALLCONV CheckJumpButton_func)(struct CGameMovement *);
static CheckJumpButton_func origsv, origcl;
-static bool VCALLCONV hooksv(void *this) {
+static bool VCALLCONV hooksv(struct CGameMovement *this) {
struct CMoveData *mv = get_mv(this);
int idx = handleidx(mv->playerhandle);
if (con_getvari(sst_autojump) && mv->firstrun && !justjumped[idx]) {
@@ -57,7 +57,7 @@ static bool VCALLCONV hooksv(void *this) {
return ret;
}
-static bool VCALLCONV hookcl(void *this) {
+static bool VCALLCONV hookcl(struct CGameMovement *this) {
struct CMoveData *mv = get_mv(this);
// FIXME: this will stutter in the rare case where justjumped is true.
// currently doing clientside justjumped handling makes multiplayer
@@ -68,9 +68,8 @@ static bool VCALLCONV hookcl(void *this) {
return justjumped[0] = origcl(this);
}
-static bool unprot(void *gm) {
- void **vtable = mem_loadptr(gm);
- bool ret = os_mprot(vtable + vtidx_CheckJumpButton, sizeof(void *),
+static bool unprot(struct CGameMovement *gm) {
+ bool ret = os_mprot(gm->vtable + vtidx_CheckJumpButton, sizeof(void *),
PAGE_READWRITE);
if (!ret) errmsg_errorsys("couldn't make virtual table writable");
return ret;
@@ -79,7 +78,7 @@ static bool unprot(void *gm) {
// reimplementing cheats check for dumb and bad reasons, see below
static struct con_var *sv_cheats;
static void cheatcb(struct con_var *this) {
- if (this->ival) 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);
@@ -92,16 +91,16 @@ INIT {
errmsg_errorx("couldn't get server-side game movement interface");
return FEAT_FAIL;
}
- if_cold (!unprot(gmsv)) return false;
+ if_cold (!unprot(gmsv)) return FEAT_FAIL;
gmcl = factory_client("GameMovement001", 0);
if_cold (!gmcl) {
errmsg_errorx("couldn't get client-side game movement interface");
return FEAT_FAIL;
}
if_cold (!unprot(gmcl)) return FEAT_FAIL;
- origsv = (CheckJumpButton_func)hook_vtable(*(void ***)gmsv,
+ origsv = (CheckJumpButton_func)hook_vtable(gmsv->vtable,
vtidx_CheckJumpButton, (void *)&hooksv);
- origcl = (CheckJumpButton_func)hook_vtable(*(void ***)gmcl,
+ origcl = (CheckJumpButton_func)hook_vtable(gmcl->vtable,
vtidx_CheckJumpButton, (void *)&hookcl);
if (GAMETYPE_MATCHES(Portal1)) {
@@ -119,8 +118,8 @@ INIT {
}
END {
- unhook_vtable(*(void ***)gmsv, vtidx_CheckJumpButton, (void *)origsv);
- unhook_vtable(*(void ***)gmcl, vtidx_CheckJumpButton, (void *)origcl);
+ unhook_vtable(gmsv->vtable, vtidx_CheckJumpButton, (void *)origsv);
+ unhook_vtable(gmcl->vtable, vtidx_CheckJumpButton, (void *)origcl);
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/bind.c b/src/bind.c
index a746ef5..7fafdb1 100644
--- a/src/bind.c
+++ b/src/bind.c
@@ -36,9 +36,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 +56,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 4abfd2b..e2bc2f3 100644
--- a/src/build/gluegen.c
+++ b/src/build/gluegen.c
@@ -602,18 +602,44 @@ static cold noreturn diewrite() { die(100, "couldn't write to file"); }
_("/* This file is autogenerated by src/build/gluegen.c. DO NOT EDIT! */")
#define H() H_() _("")
-static void recursefeatdescs(FILE *out, s16 node) {
+static void recursedbgmodnames(FILE *out, s16 node) {
if (node < 0) {
+ if (!(mod_flags[-node] & HAS_INIT)) return;
+F( " switch (status_%.*s) {",
+ mod_names[-node].len, mod_names[-node].s)
+_( " case FEAT_SKIP: colour = &grey; break;")
+_( " case FEAT_OK: colour = &green; break;")
+_( " default: colour = &red; break;")
+_( " }")
if (mod_featdescs[-node].s) {
+F( " con_colourmsg(colour, featmsgs[status_%.*s], \"%.*s (\" %.*s \")\");",
+ mod_names[-node].len, mod_names[-node].s,
+ mod_names[-node].len, mod_names[-node].s,
+ mod_featdescs[-node].len, mod_featdescs[-node].s)
+ }
+ else {
+F( " con_colourmsg(colour, featmsgs[status_%.*s], \"%.*s\");",
+ mod_names[-node].len, mod_names[-node].s,
+ mod_names[-node].len, mod_names[-node].s)
+ }
+ }
+ else if (node > 0) {
+ for (int i = 0; i < 16; ++i) {
+ recursedbgmodnames(out, radices[node].children[i]);
+ }
+ }
+}
+
+static void recursefeatdescs(FILE *out, s16 node) {
+ if (node < 0) {
F( " if (status_%.*s != FEAT_SKIP) {",
- mod_names[-node].len, mod_names[-node].s)
+ mod_names[-node].len, mod_names[-node].s)
F( " con_colourmsg(status_%.*s == FEAT_OK ? &green : &red,",
- mod_names[-node].len, mod_names[-node].s)
+ mod_names[-node].len, mod_names[-node].s)
F( " featmsgs[status_%.*s], %.*s);",
- mod_names[-node].len, mod_names[-node].s,
- mod_featdescs[-node].len, mod_featdescs[-node].s)
+ mod_names[-node].len, mod_names[-node].s,
+ mod_featdescs[-node].len, mod_featdescs[-node].s)
_( " }")
- }
}
else if (node > 0) {
for (int i = 0; i < 16; ++i) {
@@ -660,7 +686,7 @@ static int evargs_notype(FILE *out, s16 i, const char *suffix) {
return j;
}
-static inline void gencode(FILE *out, s16 featdescs) {
+static inline void gencode(FILE *out, s16 modnames, s16 featdescs) {
for (int i = 1; i < nmods; ++i) {
if (mod_flags[i] & HAS_INIT) {
F( "extern int _feat_init_%.*s();", mod_names[i].len, mod_names[i].s)
@@ -726,13 +752,13 @@ _( "static inline void initfeatures() {")
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_,
@@ -790,10 +816,13 @@ _( "")
if (!(cvar_flags[i] & CMETA_CVAR_UNREG)) {
if (cvar_flags[i] & CMETA_CVAR_FEAT) {
struct cmeta_slice modname = mod_names[cvar_feats[i]];
-F( " if (status_%.*s != FEAT_SKIP) con_regvar(%.*s);",
- modname.len, modname.s, cvar_names[i].len, cvar_names[i].s)
-F( " else if (status_%.*s != FEAT_OK) %.*s->base.flags |= CON_HIDDEN;",
+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;",
modname.len, modname.s, cvar_names[i].len, cvar_names[i].s)
+_( " }")
}
else {
F( " con_regvar(%.*s);", cvar_names[i].len, cvar_names[i].s)
@@ -818,7 +847,13 @@ _( " struct rgba white = {255, 255, 255, 255};")
_( " struct rgba green = {128, 255, 128, 255};")
_( " struct rgba red = {255, 128, 128, 255};")
_( " con_colourmsg(&white, \"---- List of plugin features ---\\n\");");
+_( "#ifdef SST_DBG")
+_( " struct rgba grey = {192, 192, 192, 255};")
+_( " struct rgba *colour;")
+ recursedbgmodnames(out, modnames);
+_( "#else")
recursefeatdescs(out, featdescs);
+_( "#endif")
_( "}")
_( "")
_( "static inline void endfeatures() {")
@@ -834,7 +869,8 @@ _( "}")
_( "")
_( "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)
}
_( "}")
for (int i = 1; i < nevents; ++i) {
@@ -953,7 +989,7 @@ int OS_MAIN(int argc, os_char *argv[]) {
FILE *out = fopen(".build/include/glue.gen.h", "wb");
if_cold (!out) die(100, "couldn't open .build/include/glue.gen.h");
H()
- gencode(out, featdesclookup);
+ gencode(out, modlookup, featdesclookup);
if_cold (fflush(out)) die(100, "couldn't finish writing output");
return 0;
}
diff --git a/src/build/mkentprops.c b/src/build/mkentprops.c
index bd4b082..17d9aa0 100644
--- a/src/build/mkentprops.c
+++ b/src/build/mkentprops.c
@@ -368,6 +368,21 @@ _( " }")
_( "}")
}
+static inline void dodbgdump(FILE *out) {
+_( "static inline void dumpentprops() {")
+_( " con_msg(\"-- entprops.txt --\\n\");")
+ for (int i = 0; i < ndecls; ++i) {
+ const char *s = sbase + decls[i];
+F( " if (has_%s) {", s);
+F( " con_msg(\" [x] %s = %%d\\n\", %s);", s, s)
+_( " }")
+_( " else {")
+F( " con_msg(\" [ ] %s\\n\");", s)
+_( " }")
+ }
+_( "}")
+}
+
int OS_MAIN(int argc, os_char *argv[]) {
if_cold (argc != 2) die(1, "wrong number of arguments");
int f = os_open_read(argv[1]);
@@ -389,6 +404,13 @@ int OS_MAIN(int argc, os_char *argv[]) {
if_cold (!out) die(100, "couldn't open entpropsinit.gen.h");
H();
doinit(out);
+
+ // technically we don't need this header in release builds, but whatever.
+ out = fopen(".build/include/entpropsdbg.gen.h", "wb");
+ if_cold (!out) die(100, "couldn't open entpropsdbg.gen.h");
+ H();
+ dodbgdump(out);
+
return 0;
}
diff --git a/src/build/mkgamedata.c b/src/build/mkgamedata.c
index 325cda2..d3b9c9c 100644
--- a/src/build/mkgamedata.c
+++ b/src/build/mkgamedata.c
@@ -169,7 +169,9 @@ static inline void knowngames(FILE *out) {
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) {
@@ -265,6 +267,32 @@ _i("}")
_( "}")
}
+static inline void dbgdump(FILE *out) {
+_( "static void dumpgamedata() {")
+ int cursrc = -1;
+ for (int i = 0; i < nents; ++i) {
+ if (indents[i] != 0) continue;
+ if_cold (srcfiles[i] != cursrc) {
+ cursrc = srcfiles[i];
+F( " con_msg(\"-- %" fS " --\\n\");", srcnames[cursrc])
+ }
+ const char *s = sbase + tags[i];
+ int line = srclines[i];
+ if (exprs[i]) {
+F( " con_msg(\" [x] %s = %%d (line %d)\\n\", %s);", s, line, s)
+ }
+ else {
+F( " if (has_%s) {", sbase + tags[i])
+F( " con_msg(\" [x] %s = %%d (line %d)\\n\", %s);", s, line, s)
+_( " }")
+_( " else {")
+F( " con_msg(\" [ ] %s (line %d)\\n\");", s, line);
+_( " }")
+ }
+ }
+_( "}")
+}
+
int OS_MAIN(int argc, os_char *argv[]) {
srcnames = (const os_char *const *)argv;
int sbase_len = 0, sbase_max = 65536;
@@ -304,6 +332,13 @@ int OS_MAIN(int argc, os_char *argv[]) {
defs(out);
_("")
init(out);
+
+ // technically we don't need this header in release builds, but whatever.
+ out = fopen(".build/include/gamedatadbg.gen.h", "wb");
+ if_cold (!out) die(100, "couldn't open gamedatadbg.gen.h");
+ H();
+ dbgdump(out);
+
return 0;
}
diff --git a/src/chatrate.c b/src/chatrate.c
index 54572a5..ba14ea1 100644
--- a/src/chatrate.c
+++ b/src/chatrate.c
@@ -32,9 +32,9 @@ 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(struct con_cmd *cmd_say) {
+static inline bool find_ratelimit_insn(con_cmdcb say_cb) {
// Find the add instruction
- uchar *insns = (uchar *)cmd_say->cb;
+ uchar *insns = (uchar *)say_cb;
for (uchar *p = insns; p - insns < 128;) {
// find FADD
if (p[0] == X86_FLTBLK5 && p[1] == X86_MODRM(0, 0, 5)) {
@@ -53,7 +53,7 @@ static inline bool find_ratelimit_insn(struct con_cmd *cmd_say) {
static inline bool patch_ratelimit_insn() {
// if FADD replace with FSUB; otherwise it is ADDSD, replace that with SUBSD
- if (!os_mprot(patchedbyte, 1, PAGE_EXECUTE_READWRITE)) {
+ if_cold (!os_mprot(patchedbyte, 1, PAGE_EXECUTE_READWRITE)) {
errmsg_errorsys("failed to patch chat rate limit: "
"couldn't make memory writable");
return false;
@@ -71,8 +71,8 @@ static inline void unpatch_ratelimit_insn() {
INIT {
struct con_cmd *cmd_say = con_findcmd("say");
- if_cold (!cmd_say) return false;
- if (!find_ratelimit_insn(cmd_say)) {
+ if_cold (!cmd_say) return FEAT_INCOMPAT; // should never happen!
+ if (!find_ratelimit_insn(cmd_say->cb)) {
errmsg_errorx("couldn't find chat rate limit instruction");
return FEAT_INCOMPAT;
}
diff --git a/src/chunklets/msg.c b/src/chunklets/msg.c
index f49581b..a4f158b 100644
--- a/src/chunklets/msg.c
+++ b/src/chunklets/msg.c
@@ -258,12 +258,12 @@ int msg_rputssz16(unsigned char *end, int val) {
int msg_putssz(unsigned char *out, unsigned int val) {
if (val <= 65535) return msg_putssz16(out, val);
doput32(out, 0xDB, val);
- return (32) / 8 + 1;
+ return 5;
}
int msg_rputssz(unsigned char *end, unsigned int val) {
if (val <= 65535) return msg_rputssz16(end, val);
doput32(end - (32) / 8 - 1, 0xDB, val);
- return (32) / 8 + 1;
+ return 5;
}
int msg_putbsz16(unsigned char *out, int val) {
diff --git a/src/clientcon.c b/src/clientcon.c
index 78c8957..767a4cd 100644
--- a/src/clientcon.c
+++ b/src/clientcon.c
@@ -25,7 +25,8 @@ REQUIRE(ent)
REQUIRE_GAMEDATA(vtidx_ClientPrintf)
REQUIRE_GLOBAL(engserver)
-DECL_VFUNC_DYN(void, ClientPrintf, struct edict *, const char *)
+DECL_VFUNC_DYN(struct VEngineServer, void, ClientPrintf, struct edict *,
+ const char *)
void clientcon_msg(struct edict *e, const char *s) {
ClientPrintf(engserver, e, s);
diff --git a/src/con_.c b/src/con_.c
index 35d4c45..d0ad7ce 100644
--- a/src/con_.c
+++ b/src/con_.c
@@ -41,43 +41,34 @@
static int dllid; // from AllocateDLLIdentifier(), lets us unregister in bulk
int con_cmdclient;
-DECL_VFUNC(void *, FindCommandBase_p2, 13, const char *)
-DECL_VFUNC(void *, FindCommand_nonp2, 14, const char *)
-DECL_VFUNC(void *, FindVar_nonp2, 12, const char *)
-
-DECL_VFUNC_DYN(int, AllocateDLLIdentifier)
-DECL_VFUNC_DYN(void, RegisterConCommand, /*ConCommandBase*/ void *)
-DECL_VFUNC_DYN(void, UnregisterConCommands, int)
-DECL_VFUNC_DYN(struct con_var *, FindVar, const char *)
-// DECL_VFUNC(const struct con_var *, FindVar_const, 13, const char *)
-DECL_VFUNC_DYN(struct con_cmd *, FindCommand, const char *)
-DECL_VFUNC_DYN(void, CallGlobalChangeCallbacks, struct con_var *, const char *,
- float)
+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)
+DECL_VFUNC_DYN(struct ICvar, struct con_var *, FindVar, const char *)
+//DECL_VFUNC(struct ICvar, const struct con_var *, FindVar_const, 13, 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)(void *, const struct rgba *,
+typedef void (*ConsoleColorPrintf_func)(struct ICvar *, const struct rgba *,
const char *, ...);
// these have to be extern for con_colourmsg(), due to varargs nonsense
-void *_con_iface;
+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);
+ v->v2.strval = extmalloc(v->v2.strlen); // note: _DEF_CVAR() sets strlen
+ memcpy(v->v2.strval, v->v2.defaultval, v->v2.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)
-
static void VCALLCONV dtor(void *_) {} // we don't use constructors/destructors
static bool VCALLCONV IsCommand_cmd(void *this) { return true; }
@@ -87,38 +78,38 @@ 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);
+ return !!(this->base.flags & flags);
}
static void VCALLCONV AddFlags_cmd(struct con_cmd *this, int flags) {
this->base.flags |= flags;
}
static void VCALLCONV AddFlags_var(struct con_var *this, int flags) {
- this->parent->base.flags |= flags;
+ this->base.flags |= flags;
}
static void VCALLCONV RemoveFlags_cmd(struct con_cmd *this, int flags) {
this->base.flags &= ~flags;
}
static void VCALLCONV RemoveFlags_var(struct con_var *this, int flags) {
- this->parent->base.flags &= ~flags;
+ this->base.flags &= ~flags;
}
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;
+ return this->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;
+ return this->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;
+ return this->base.help;
}
static bool VCALLCONV IsRegistered(struct con_cmdbase *this) {
return this->registered;
@@ -131,34 +122,39 @@ static void VCALLCONV Create_base(struct con_cmdbase *this, const char *name,
static void VCALLCONV Init(struct con_cmdbase *this) {} // ""
static bool VCALLCONV ClampValue(struct con_var *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; }
+ if (this->v2.hasmin && this->v2.minval > *f) {
+ *f = this->v2.minval;
+ return true;
+ }
+ if (this->v2.hasmax && this->v2.maxval < *f) {
+ *f = this->v2.maxval;
+ return true;
+ }
return false;
}
-int VCALLCONV AutoCompleteSuggest(void *this, const char *partial,
+int VCALLCONV AutoCompleteSuggest(struct con_cmd *this, const char *partial,
/*CUtlVector*/ void *commands) {
// TODO(autocomplete): implement this if needed later
return 0;
}
-bool VCALLCONV CanAutoComplete(void *this) {
+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);
+ this->cb(args->argc, args->argv);
}
static void VCALLCONV ChangeStringValue(struct con_var *this, const char *s,
float oldf) {
- char *old = alloca(this->strlen);
- memcpy(old, this->strval, this->strlen);
+ char *old = alloca(this->v2.strlen);
+ memcpy(old, this->v2.strval, this->v2.strlen);
int len = strlen(s) + 1;
- if (len > this->strlen) {
- this->strval = extrealloc(this->strval, len);
- this->strlen = len;
+ if (len > this->v2.strlen) {
+ this->v2.strval = extrealloc(this->v2.strval, len);
+ this->v2.strlen = len;
}
- memcpy(this->strval, s, len);
+ memcpy(this->v2.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
@@ -167,74 +163,72 @@ static void VCALLCONV ChangeStringValue(struct con_var *this, const char *s,
CallGlobalChangeCallbacks(_con_iface, this, old, oldf);
}
-static void VCALLCONV InternalSetValue_impl(struct con_var *this, const char *v) {
- float oldf = this->fval;
+// 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(struct con_var *this, const char *v) {
+ float oldf = this->v2.fval;
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)) {
snprintf(tmp, sizeof(tmp), "%f", newf);
v = tmp;
}
- this->fval = newf;
- this->ival = (int)newf;
+ this->v2.fval = newf;
+ this->v2.ival = (int)newf;
if (!(this->base.flags & CON_NOPRINT)) ChangeStringValue(this, v, oldf);
}
-static void VCALLCONV InternalSetFloatValue_impl(struct con_var *this, float v) {
- if (v == this->fval) return;
+static void VCALLCONV InternalSetFloatValue(struct con_var *this, float v) {
+ if (v == this->v2.fval) return;
ClampValue(this, &v);
- float old = this->fval;
- this->fval = v; this->ival = (int)this->fval;
+ float old = this->v2.fval;
+ this->v2.fval = v; this->v2.ival = (int)this->v2.fval;
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);
}
}
-static void VCALLCONV InternalSetIntValue_impl(struct con_var *this, int v) {
- if (v == this->ival) return;
+static void VCALLCONV InternalSetIntValue(struct con_var *this, int v) {
+ if (v == this->v2.ival) return;
float f = (float)v;
if (ClampValue(this, &f)) v = (int)f;
- float old = this->fval;
- this->fval = f; this->ival = v;
+ float old = this->v2.fval;
+ this->v2.fval = f; this->v2.ival = 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(void, InternalSetValue, const char *)
-DECL_VFUNC_DYN(void, InternalSetFloatValue, float)
-DECL_VFUNC_DYN(void, InternalSetIntValue, int)
-DECL_VFUNC_DYN(void, InternalSetColorValue, struct rgba)
-
// 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->base, 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->base, 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->base, 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->base, v);
+ InternalSetIntValue(this, v.val);
}
-// more misc thunks, hopefully these just compile to a sub and a jmp
+// more misc thunks, 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));
@@ -247,7 +241,9 @@ static bool VCALLCONV IsFlagSet_thunk(void *thisoff, int flags) {
}
// dunno what this is actually for...
-static int VCALLCONV GetSplitScreenPlayerSlot(void *thisoff) { return 0; }
+static int VCALLCONV GetSplitScreenPlayerSlot(struct con_var *thisoff) {
+ return 0;
+}
// aand yet another Create nop
static void VCALLCONV Create_var(void *thisoff, const char *name,
@@ -355,13 +351,12 @@ void con_init() {
*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;
+ *pv++ = (void *)&InternalSetValue;
+ *pv++ = (void *)&InternalSetFloatValue;
+ *pv++ = (void *)&InternalSetIntValue;
if (GAMETYPE_MATCHES(L4D2x) || GAMETYPE_MATCHES(Portal2)) { // ugh, annoying
// InternalSetColorValue, literally the same machine instructions as int
- *pv++ = (void *)&InternalSetIntValue_impl;
+ *pv++ = (void *)&InternalSetIntValue;
}
*pv++ = (void *)&ClampValue;;
*pv++ = (void *)&ChangeStringValue;
@@ -471,7 +466,13 @@ struct con_cmd *con_findcmd(const char *name) {
return FindCommand(_con_iface, 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 v->v2.parent->v2.M; \
+ }
GETTER(const char *, con_getvarstr, strval)
GETTER(float, con_getvarf, fval)
GETTER(int, con_getvari, ival)
@@ -489,8 +490,8 @@ 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 bdd4b87..c2c0cce 100644
--- a/src/con_.h
+++ b/src/con_.h
@@ -66,10 +66,13 @@ enum {
CON_CCMDEXEC = 1 << 30 /* ClientCmd() function may run the command */
};
-/* A callback function invoked to execute a command. */
-typedef void (*con_cmdcb)(const struct con_cmdargs *cmd);
+/* A callback function invoked by SST to execute its own commands. */
+typedef void (*con_cmdcb)(int argc, const char *const *argv);
-/* Obsolete callback; not used by SST, but might still exist in the engine. */
+/* A callback function used by most commands in most versions of the engine. */
+typedef void (*con_cmdcbv2)(const struct con_cmdargs *cmd);
+
+/* An older style of callback function used by some old commands, and in OE. */
typedef void (*con_cmdcbv1)();
/*
@@ -101,9 +104,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 +117,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 +129,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;
+ 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 +152,27 @@ 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.
+ */
+static inline struct con_var_common *con_getvarcommon(struct con_var *v) {
+ return &v->v2;
+}
+
/*
* 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 +188,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);
/*
@@ -180,9 +208,10 @@ 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 void *_con_iface;
-extern void (*_con_colourmsgf)(void *this, const struct rgba *c,
+extern struct ICvar *_con_iface;
+extern void (*_con_colourmsgf)(struct ICvar *this, const struct rgba *c,
const char *fmt, ...) _CON_PRINTF(3, 4);
/*
* This provides the same functionality as ConColorMsg which was removed from
@@ -230,18 +259,16 @@ extern struct _con_vtab_iconvar_wrap {
.name = "" #name_, .help = "" desc, .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), \
- /* N.B. the NOLINT comment below isn't for you, the reader, it's for the
- computer, because clangd decided the only way to turn off a bogus
- warning is to write a bogus comment. Also note, this comment you're
- reading now isn't very useful either, I'm just angry. */ \
- .strlen = _Generic(value, char *: sizeof(value), /*NOLINT*/ \
- default: sizeof(#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_;
@@ -268,9 +295,8 @@ extern struct _con_vtab_iconvar_wrap {
.name = "" #name_, .help = "" desc, .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) \
@@ -286,13 +312,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 *const *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 *const *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 *const *argv) \
/* { body here } */
/*
diff --git a/src/dbg.c b/src/dbg.c
index fa85ee8..af995ce 100644
--- a/src/dbg.c
+++ b/src/dbg.c
@@ -18,22 +18,29 @@
#include <Windows.h>
#endif
+#include "accessor.h"
#include "con_.h"
#include "engineapi.h"
+#include "errmsg.h"
+#include "gamedata.h"
#include "intdefs.h"
#include "ppmagic.h"
#include "udis86.h"
+#include "vcall.h"
+
+#include <gamedatadbg.gen.h>
+#include <entpropsdbg.gen.h>
#ifdef _WIN32
usize dbg_toghidra(const void *addr) {
- const void *mod;
+ const char *mod;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, (ushort *)addr,
- (HMODULE *)&mod /* please leave me alone */)) {
+ (HMODULE *)&mod)) {
con_warn("dbg_toghidra: couldn't get base address\n");
return 0;
}
- return (const char *)addr - (const char *)mod + 0x10000000;
+ return (const char *)addr - mod + 0x10000000;
}
#endif
@@ -48,8 +55,8 @@ void dbg_hexdump(const char *name, const void *p, int len) {
for (const uchar *cp = p; cp - (uchar *)p < len; ++cp) {
// group into words and wrap every 8 words
switch ((cp - (uchar *)p) & 31) {
- case 0: con_msg("\n"); break;
- CASES(4, 8, 12, 16, 20, 24, 28): con_msg(" ");
+ case 0: con_colourmsg(&nice_colour, "\n"); break;
+ CASES(4, 8, 12, 16, 20, 24, 28): con_colourmsg(&nice_colour, " ");
}
con_colourmsg(&nice_colour, "%02X ", *cp);
}
@@ -74,4 +81,67 @@ 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 (argc != 2) {
+ con_warn("usage: sst_dbg_getcmdcb command\n");
+ return;
+ }
+ struct con_cmd *thecmd = con_findcmd(argv[1]);
+ if (!thecmd) {
+ errmsg_errorstd("couldn't find command %s\n", argv[1]);
+ return;
+ }
+#ifdef _WIN32
+ con_msg("addr: %p\nghidra: %p\n", (void *)thecmd->cb_insns,
+ (void *)dbg_toghidra((void *)thecmd->cb_insns)); // ugh
+#else
+ con_msg("addr: %p\n", (void *)thecmd->cb);
+#endif
+}
+
+DECL_VFUNC_DYN(struct IServerGameDLL, struct ServerClass *, GetAllServerClasses)
+DEF_ARRAYIDX_ACCESSOR(struct SendProp, SendProp)
+DEF_ACCESSORS(struct SendProp, const char *, SP_varname)
+DEF_ACCESSORS(struct SendProp, int, SP_type)
+DEF_ACCESSORS(struct SendProp, int, SP_offset)
+DEF_ACCESSORS(struct SendProp, struct SendTable *, SP_subtable)
+
+static void dumptable(struct SendTable *st, int indent) {
+ for (int i = 0; i < st->nprops; ++i) {
+ for (int i = 0; i < indent; i++) con_msg(" ");
+ struct SendProp *p = arrayidx_SendProp(st->props, i);
+ const char *name = get_SP_varname(p);
+ if (get_SP_type(p) == DPT_DataTable) {
+ struct SendTable *st = get_SP_subtable(p);
+ if (!strcmp(name, "baseclass")) {
+ con_msg("baseclass -> table %s (skipped)\n", st->tablename);
+ }
+ else {
+ con_msg("%s -> subtable %s\n", name, st->tablename);
+ dumptable(st, indent + 1);
+ }
+ }
+ else {
+ con_msg("%s -> offset %d\n", name, get_SP_offset(p));
+ }
+ }
+}
+DEF_CCMD_HERE(sst_dbg_sendtables, "Dump ServerClass/SendTable hierarchy", 0) {
+ if (!srvdll) {
+ errmsg_errorx("can't iterate ServerClass list: missing srvdll global");
+ return;
+ }
+ for (struct ServerClass *class = GetAllServerClasses(srvdll); class;
+ class = class->next) {
+ struct SendTable *st = class->table;
+ con_msg("class %s (table %s)\n", class->name, st->tablename);
+ dumptable(st, 1);
+ }
+}
+
+DEF_CCMD_HERE(sst_dbg_gamedata, "Dump current gamedata values", 0) {
+ dumpgamedata();
+ dumpentprops();
+}
+
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/democustom.c b/src/democustom.c
index 7ca8677..cae58a8 100644
--- a/src/democustom.c
+++ b/src/democustom.c
@@ -17,16 +17,13 @@
#include <string.h>
#include "bitbuf.h"
-#include "con_.h"
#include "demorec.h"
#include "engineapi.h"
-#include "errmsg.h"
#include "feature.h"
#include "gamedata.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
-#include "ppmagic.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"
@@ -88,7 +85,7 @@ void democustom_write(const void *buf, int len) {
}
static bool find_WriteMessages() {
- const uchar *insns = (*(uchar ***)demorecorder)[vtidx_RecordPacket];
+ const uchar *insns = (uchar *)demorecorder->vtable[vtidx_RecordPacket];
// RecordPacket calls WriteMessages right away, so just look for a call
for (const uchar *p = insns; p - insns < 32;) {
if (*p == X86_CALL) {
@@ -100,7 +97,7 @@ static bool find_WriteMessages() {
return false;
}
-DECL_VFUNC_DYN(int, GetEngineBuildNumber)
+DECL_VFUNC_DYN(struct VEngineClient, int, GetEngineBuildNumber)
INIT {
// More UncraftedkNowledge:
diff --git a/src/demorec.c b/src/demorec.c
index a14798a..6f1b2a7 100644
--- a/src/demorec.c
+++ b/src/demorec.c
@@ -18,6 +18,7 @@
#include <string.h>
#include "con_.h"
+#include "demorec.h"
#include "engineapi.h"
#include "errmsg.h"
#include "event.h"
@@ -29,19 +30,21 @@
#include "langext.h"
#include "mem.h"
#include "os.h"
-#include "ppmagic.h"
#include "sst.h"
#include "vcall.h"
#include "x86.h"
#include "x86util.h"
FEATURE("improved demo recording")
+REQUIRE_GAMEDATA(vtidx_SetSignonState)
+REQUIRE_GAMEDATA(vtidx_StartRecording)
REQUIRE_GAMEDATA(vtidx_StopRecording)
+REQUIRE_GAMEDATA(vtidx_RecordPacket)
DEF_FEAT_CVAR(sst_autorecord,
"Continuously record demos even after reconnecting", 1, CON_ARCHIVE)
-void *demorecorder;
+struct CDemoRecorder *demorecorder;
static int *demonum;
static bool *recording;
const char *demorec_basename;
@@ -56,10 +59,11 @@ DEF_PREDICATE(DemoControlAllowed)
DEF_EVENT(DemoRecordStarting)
DEF_EVENT(DemoRecordStopped, int)
-typedef void (*VCALLCONV SetSignonState_func)(void *, int);
+struct CDemoRecorder;
+
+typedef void (*VCALLCONV SetSignonState_func)(struct CDemoRecorder *, int);
static SetSignonState_func orig_SetSignonState;
-static void VCALLCONV hook_SetSignonState(void *this_, int state) {
- struct CDemoRecorder *this = this_;
+static void VCALLCONV hook_SetSignonState(struct CDemoRecorder *this, int state) {
// NEW fires once every map or save load, but only bumps number if demo file
// was left open (i.e. every transition). bump it unconditionally instead!
if (state == SIGNONSTATE_NEW) {
@@ -76,9 +80,9 @@ static void VCALLCONV hook_SetSignonState(void *this_, int state) {
orig_SetSignonState(this, state);
}
-typedef void (*VCALLCONV StopRecording_func)(void *);
+typedef void (*VCALLCONV StopRecording_func)(struct CDemoRecorder *);
static StopRecording_func orig_StopRecording;
-static void VCALLCONV hook_StopRecording(void *this) {
+static void VCALLCONV hook_StopRecording(struct CDemoRecorder *this) {
bool wasrecording = *recording;
int lastnum = *demonum;
orig_StopRecording(this);
@@ -94,10 +98,10 @@ static void VCALLCONV hook_StopRecording(void *this) {
}
}
-DECL_VFUNC_DYN(void, StartRecording)
+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 con_cmdcbv2 orig_record_cb, orig_stop_cb;
static void hook_record_cb(const struct con_cmdargs *args) {
if_cold (!CHECK_DemoControlAllowed()) return;
@@ -256,14 +260,14 @@ int demorec_demonum() {
INIT {
cmd_record = con_findcmd("record");
- orig_record_cb = con_getcmdcb(cmd_record);
+ orig_record_cb = con_getcmdcbv2(cmd_record);
cmd_stop = con_findcmd("stop");
- orig_stop_cb = con_getcmdcb(cmd_stop);
+ orig_stop_cb = con_getcmdcbv2(cmd_stop);
if_cold (!find_demorecorder()) {
errmsg_errorx("couldn't find demo recorder instance");
return FEAT_INCOMPAT;
}
- void **vtable = mem_loadptr(demorecorder);
+ void **vtable = demorecorder->vtable;
// XXX: 16 is totally arbitrary here! figure out proper bounds later
if_cold (!os_mprot(vtable, 16 * sizeof(void *), PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
@@ -283,8 +287,8 @@ INIT {
orig_StopRecording = (StopRecording_func)hook_vtable(vtable,
vtidx_StopRecording, (void *)&hook_StopRecording);
- cmd_record->cb = &hook_record_cb;
- cmd_stop->cb = &hook_stop_cb;
+ cmd_record->cb_v2 = &hook_record_cb;
+ cmd_stop->cb_v2 = &hook_stop_cb;
return FEAT_OK;
}
@@ -293,11 +297,11 @@ END {
if_hot (!sst_userunloaded) return;
// avoid dumb edge case if someone somehow records and immediately unloads
if (*recording && *demonum == 0) *demonum = 1;
- void **vtable = *(void ***)demorecorder;
+ 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;
+ cmd_record->cb_v2 = orig_record_cb;
+ cmd_stop->cb_v2 = orig_stop_cb;
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/demorec.h b/src/demorec.h
index 63009a0..4e33cc5 100644
--- a/src/demorec.h
+++ b/src/demorec.h
@@ -20,8 +20,9 @@
#include "event.h"
-// For internal use by democustom
-extern void *demorecorder;
+// For internal use by democustom. Consider this opaque.
+// XXX: should the struct be put in engineapi or something?
+extern struct CDemoRecorder { void **vtable; } *demorecorder;
/*
* Whether to ignore the value of the sst_autorecord cvar and just keep
diff --git a/src/engineapi.c b/src/engineapi.c
index 54a6d8f..44713da 100644
--- a/src/engineapi.c
+++ b/src/engineapi.c
@@ -40,15 +40,16 @@ ifacefactory factory_client = 0, factory_server = 0, factory_engine = 0,
struct VEngineClient *engclient;
struct VEngineServer *engserver;
-void *srvdll;
+struct IServerGameDLL *srvdll;
-DECL_VFUNC(void *, GetGlobalVars, 1) // seems to be very stable, thank goodness
-void *globalvars;
+DECL_VFUNC(void, struct CGlobalVars *, GetGlobalVars, 1) // seems very stable
+struct CGlobalVars *globalvars;
-void *inputsystem, *vgui;
+struct IInputSystem *inputsystem;
+struct CEngineVGui *vgui;
struct CServerPlugin *pluginhandler;
-DECL_VFUNC_DYN(void *, GetAllServerClasses)
+DECL_VFUNC_DYN(struct IServerGameDLL, struct ServerClass *, GetAllServerClasses)
#include <entpropsinit.gen.h> // generated by build/mkentprops.c
#include <gamedatainit.gen.h> // generated by build/mkgamedata.c
diff --git a/src/engineapi.h b/src/engineapi.h
index c7a7e1f..4118a3c 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
@@ -38,15 +37,14 @@ extern ifacefactory factory_client, factory_server, factory_engine,
// various engine types {{{
-struct VEngineClient {
- void **vtable;
- /* opaque fields */
-};
-
-struct VEngineServer {
- void **vtable;
- /* opaque fields */
-};
+// Virtual classes with opaque members; vtables exposed for ease of hooking etc.
+struct ICvar { void **vtable; };
+struct VEngineClient { void **vtable; };
+struct VClient { void **vtable; };
+struct VEngineServer { void **vtable; };
+struct IServerGameDLL { void **vtable; };
+struct IInputSystem { void **vtable; };
+struct CEngineVGui { void **vtable; };
struct CUtlMemory {
void *mem;
@@ -124,11 +122,12 @@ struct ServerClass {
extern struct VEngineClient *engclient;
extern struct VEngineServer *engserver;
-extern void *srvdll;
-extern void *globalvars;
-extern void *inputsystem, *vgui;
+extern struct IServerGameDLL *srvdll;
+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
+// XXX: not exactly engine *API* but not currently clear where else to put this
struct CPlugin_common {
bool paused;
void *theplugin; // our own "this" pointer (or whichever other plugin it is)
@@ -143,8 +142,8 @@ struct CPlugin {
struct CPlugin_common v1;
struct {
char basename[128]; // WHY VALVE WHYYYYYYY!!!!
- struct CPlugin_common common;
- } v2;
+ struct CPlugin_common v2;
+ };
};
};
struct CServerPlugin /* : IServerPluginHelpers */ {
diff --git a/src/ent.c b/src/ent.c
index 118c170..14e0788 100644
--- a/src/ent.c
+++ b/src/ent.c
@@ -21,7 +21,6 @@
#include "errmsg.h"
#include "feature.h"
#include "gamedata.h"
-#include "gametype.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
@@ -31,10 +30,10 @@
FEATURE()
-DECL_VFUNC_DYN(void *, PEntityOfEntIndex, int)
+DECL_VFUNC_DYN(struct VEngineServer, struct edict *, PEntityOfEntIndex, int)
-DEF_PTR_ACCESSOR(struct edict *, edicts)
-DEF_ARRAYIDX_ACCESSOR(edict)
+DEF_PTR_ACCESSOR(struct CGlobalVars, struct edict *, edicts)
+DEF_ARRAYIDX_ACCESSOR(struct edict, edict)
static struct edict **edicts = 0;
diff --git a/src/extmalloc.c b/src/extmalloc.c
index e3f40b8..08538ea 100644
--- a/src/extmalloc.c
+++ b/src/extmalloc.c
@@ -25,15 +25,15 @@
#ifdef _WIN32
-__declspec(dllimport) void *g_pMemAlloc;
+__declspec(dllimport) struct IMemAlloc *g_pMemAlloc;
// this interface has changed a bit between versions but thankfully the basic
// functions we care about have always been at the start - nice and easy.
// note that since Microsoft are a bunch of crazies, overloads are grouped and
// reversed so the vtable order here is maybe not what you'd expect otherwise.
-DECL_VFUNC(void *, Alloc, 1, usize)
-DECL_VFUNC(void *, Realloc, 3, void *, usize)
-DECL_VFUNC(void, Free, 5, void *)
+DECL_VFUNC(struct IMemAlloc, void *, Alloc, 1, usize)
+DECL_VFUNC(struct IMemAlloc, void *, Realloc, 3, void *, usize)
+DECL_VFUNC(struct IMemAlloc, void, Free, 5, void *)
void *extmalloc(usize sz) { return Alloc(g_pMemAlloc, sz); }
void *extrealloc(void *mem, usize sz) { return Realloc(g_pMemAlloc, mem, sz); }
diff --git a/src/fastfwd.c b/src/fastfwd.c
index 73db41e..1b7588d 100644
--- a/src/fastfwd.c
+++ b/src/fastfwd.c
@@ -232,19 +232,18 @@ INIT {
return FEAT_INCOMPAT;
}
if_cold (!(func = find_floatcall(func, 1, "_Host_RunFrame"))) {
- errmsg_errorx("couldn't find _Host_RunFrame");
+ errmsg_errorx("couldn't find _Host_RunFrame function");
return FEAT_INCOMPAT;
}
if_cold (!find_Host_AccumulateTime(func)) {
- errmsg_errorx("couldn't find Host_AccumulateTime");
+ errmsg_errorx("couldn't find Host_AccumulateTime function");
return FEAT_INCOMPAT;
}
- orig_Host_AccumulateTime = (Host_AccumulateTime_func)hook_inline(
- (void *)orig_Host_AccumulateTime, (void *)hook_Host_AccumulateTime);
- if_cold (!orig_Host_AccumulateTime) {
- errmsg_errorsys("couldn't hook Host_AccumulateTime function");
- return FEAT_FAIL;
- }
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_Host_AccumulateTime, (void **)&orig_Host_AccumulateTime,
+ "Host_AccumulateTime");
+ if_cold (h.err) return h.err;
+ hook_inline_commit(h.prologue, (void *)&hook_Host_AccumulateTime);
return FEAT_OK;
}
diff --git a/src/fastfwd.h b/src/fastfwd.h
index 6313e0c..e76c92c 100644
--- a/src/fastfwd.h
+++ b/src/fastfwd.h
@@ -20,7 +20,7 @@
/*
* Fast-forwards in-game time by a number of seconds, ignoring the usual
* host_framerate and host_timescale settings. timescale controls how many
- * seconds of game pass per real-time second.
+ * seconds of game time pass per real-time second.
*/
void fastfwd(float seconds, float timescale);
diff --git a/src/fixes.c b/src/fixes.c
index ea008e5..d4b5330 100644
--- a/src/fixes.c
+++ b/src/fixes.c
@@ -32,7 +32,10 @@
static void chflags(const char *name, int unset, int set) {
struct con_var *v = con_findvar(name);
- if (v) v->parent->base.flags = v->parent->base.flags & ~unset | set;
+ 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) {
@@ -93,17 +96,19 @@ static void generalfixes() {
// 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;
+ struct con_var *p = con_getvarcommon(v)->parent;
+ struct con_var_common *c = con_getvarcommon(p);
+ if (p->base.flags & (CON_HIDDEN | CON_DEVONLY)) {
+ p->base.flags &= ~(CON_HIDDEN | CON_DEVONLY);
+ c->hasmax = true; c->maxval = 300;
}
- else if (!v->parent->hasmax) {
+ else if (!c->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;
+ c->hasmax = true; c->maxval = 1000;
}
// also show the lower limit in help, and prevent 0 (which is unlimited)
- v->parent->hasmin = true; v->parent->minval = 30;
+ c->hasmin = true; c->minval = 30;
con_setvarf(v, con_getvarf(v)); // hack: reapply limit if we loaded late
}
}
@@ -128,11 +133,13 @@ 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 &
+ 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_HIDDEN | CON_DEVONLY) | CON_ARCHIVE;
- v->parent->hasmin = true; v->parent->minval = -1;
- v->parent->hasmax = true; v->parent->maxval = 0;
+ c->hasmin = true; c->minval = -1;
+ c->hasmax = true; c->maxval = 0;
}
#ifdef _WIN32
diff --git a/src/fov.c b/src/fov.c
index 967d783..2ef13e9 100644
--- a/src/fov.c
+++ b/src/fov.c
@@ -24,7 +24,6 @@
#include "ent.h"
#include "event.h"
#include "feature.h"
-#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
#include "langext.h"
@@ -88,8 +87,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;
@@ -100,16 +101,16 @@ INIT {
errmsg_errorx("couldn't find SetDefaultFOV function");
return FEAT_INCOMPAT;
}
- orig_SetDefaultFOV = (SetDefaultFOV_func)hook_inline(
- (void *)orig_SetDefaultFOV, (void *)&hook_SetDefaultFOV);
- if_cold (!orig_SetDefaultFOV) {
- errmsg_errorsys("couldn't hook SetDefaultFOV function");
- return FEAT_FAIL;
- }
+
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_SetDefaultFOV, (void **)&orig_SetDefaultFOV,
+ "SetDefaultFov");
+ if_cold (h.err) return h.err;
+ hook_inline_commit(h.prologue, (void *)&hook_SetDefaultFOV);
// we might not be using our cvar but simpler to do this unconditionally
fov_desired->cb = &fovcb;
- fov_desired->parent->base.flags &= ~CON_HIDDEN;
+ fov_desired->base.flags &= ~CON_HIDDEN;
// hide the original fov command since we've effectively broken it anyway :)
cmd_fov->base.flags |= CON_DEVONLY;
return FEAT_OK;
@@ -118,10 +119,10 @@ INIT {
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
diff --git a/src/gameinfo.c b/src/gameinfo.c
index 7432073..a3f4eac 100644
--- a/src/gameinfo.c
+++ b/src/gameinfo.c
@@ -39,7 +39,7 @@ const os_char *gameinfo_gamedir
;
const char *gameinfo_title = title;
-DECL_VFUNC_DYN(const char *, GetGameDirectory)
+DECL_VFUNC_DYN(struct VEngineClient, const char *, GetGameDirectory)
bool gameinfo_init() {
if_cold (!has_vtidx_GetGameDirectory) {
diff --git a/src/gameserver.c b/src/gameserver.c
index 7cd7526..6f3d394 100644
--- a/src/gameserver.c
+++ b/src/gameserver.c
@@ -28,9 +28,10 @@
FEATURE()
REQUIRE_GAMEDATA(vtidx_GetSpawnCount)
-DECL_VFUNC_DYN(int, GetSpawnCount)
+struct CGameServer;
+DECL_VFUNC_DYN(struct CGameServer, int, GetSpawnCount)
-static void *sv;
+static struct CGameServer *sv;
int gameserver_spawncount() { return GetSpawnCount(sv); }
diff --git a/src/gametype.h b/src/gametype.h
index 51f110d..fa899c2 100644
--- a/src/gametype.h
+++ b/src/gametype.h
@@ -23,43 +23,54 @@
extern u32 _gametype_tag;
-/* general engine branches used in a bunch of stuff */
-#define _gametype_tag_OE 1
-#define _gametype_tag_OrangeBox (1 << 1)
-#define _gametype_tag_2013 (1 << 2)
+#define GAMETYPE_BASETAGS(X) \
+ /* general engine branches used in a bunch of stuff */ \
+ X(OE) \
+ X(OrangeBox) \
+ X(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) \
+\
+ /* games needing game-specific stuff, but not tied to a singular branch */ \
+ X(Portal1) \
+ X(HL2series) /* HL2, episodes, mods */ \
+\
+ /* VEngineClient versions */ \
+ X(Client015) \
+ X(Client014) \
+ X(Client013) \
+ X(Client012) \
+\
+ /* VEngineServer versions */ \
+ X(Server021) \
+\
+ /* ServerGameDLL versions */ \
+ X(SrvDLL009) /* 2013-ish */ \
+ X(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 */ \
-/* specific games with dedicated branches / engine changes */
-// TODO(compat): detect dmomm, even if only just to fail (VEngineServer broke)
-// TODO(compat): buy dmomm in a steam sale to implement and test the above, lol
-#define _gametype_tag_DMoMM (1 << 3)
-#define _gametype_tag_L4D1 (1 << 4)
-#define _gametype_tag_L4D2 (1 << 5)
-#define _gametype_tag_L4DS (1 << 6) /* Survivors (weird arcade port) */
-#define _gametype_tag_Portal2 (1 << 7)
+enum {
+#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)
+#undef _GAMETYPE_ENUMVAL
+};
-/* games needing game-specific stuff, but not tied to a singular branch */
-#define _gametype_tag_Portal1 (1 << 8)
-#define _gametype_tag_HL2series (1 << 9) /* HL2, episodes, and mods */
-
-/* VEngineClient versions */
-#define _gametype_tag_Client015 (1 << 10)
-#define _gametype_tag_Client014 (1 << 11)
-#define _gametype_tag_Client013 (1 << 12)
-#define _gametype_tag_Client012 (1 << 13)
-#define _gametype_tag_Server021 (1 << 14)
-
-/* ServerGameDLL versions */
-#define _gametype_tag_SrvDLL009 (1 << 15) // 2013-ish
-#define _gametype_tag_SrvDLL005 (1 << 16) // mostly everything else, it seems
-
-/* games needing version-specific stuff */
-#define _gametype_tag_Portal1_3420 (1 << 17)
-#define _gametype_tag_L4D1_1015plus (1 << 18) // Crash Course update
-#define _gametype_tag_L4D1_1022plus (1 << 19) // Mac update, with code shuffling
-#define _gametype_tag_L4D2_2125plus (1 << 20)
-#define _gametype_tag_TheLastStand (1 << 21) /* The JAiZ update */
-
-/* Matches for any multiple possible tags */
+/* Matches for any of multiple possible tags */
#define _gametype_tag_L4D (_gametype_tag_L4D1 | _gametype_tag_L4D2)
// XXX: *stupid* naming, refactor one day (damn Survivors ruining everything)
#define _gametype_tag_L4D2x (_gametype_tag_L4D2 | _gametype_tag_L4DS)
diff --git a/src/hook.c b/src/hook.c
index b6ca703..5f964ad 100644
--- a/src/hook.c
+++ b/src/hook.c
@@ -17,97 +17,78 @@
#include <string.h>
-#include "con_.h"
+#include "hook.h"
#include "intdefs.h"
#include "langext.h"
#include "mem.h"
#include "os.h"
#include "x86.h"
-#ifdef _WIN32
-// try to avoid pulling in all of Windows.h for this... (redundant dllimport
-// avoids warnings in hook.test.c where Windows.h is included via test.h)
-__declspec(dllimport) int __stdcall FlushInstructionCache(
- void *, const void *, usize);
-#endif
-
// Warning: half-arsed hacky implementation (because that's all we really need)
// Almost certainly breaks in some weird cases. Oh well! Most of the time,
// vtable hooking is more reliable, this is only for, uh, emergencies.
-#if defined(__GNUC__) || defined(__clang__)
-__attribute__((aligned(4096)))
-#elif defined(_MSC_VER)
-__declspec(align(4096))
-#else
-#error no way to align stuff!
-#endif
-static uchar trampolines[4096];
-static uchar *nexttrampoline = trampolines;
+static _Alignas(4096) uchar trampolines[4096];
+static uchar *curtrampoline = trampolines;
bool hook_init() {
// PE doesn't support rwx sections, not sure about ELF. Meh, just set it
// here instead.
- return os_mprot(trampolines, sizeof(trampolines), PAGE_EXECUTE_READWRITE);
-}
-
-static inline void iflush(void *p, int len) {
-#if defined(_WIN32)
- // -1 is the current process, and it's a constant in the WDK, so it's
- // assumed we can safely avoid the useless GetCurrentProcess call
- FlushInstructionCache((void *)-1, p, len);
-#elif defined(__GNUC__)
- __builtin___clear_cache((char *)p, (char *)p + len);
-#else
-#error no way to flush instruction cache
-#endif
+ return os_mprot(trampolines, 4096, PAGE_EXECUTE_READWRITE);
}
-void *hook_inline(void *func_, void *target) {
- uchar *func = func_;
+struct hook_inline_prep_ret hook_inline_prep(void *func, void **trampoline) {
+ uchar *p = func;
// dumb hack: if we hit some thunk that immediately jumps elsewhere (which
// seems common for win32 API functions), hook the underlying thing instead.
- while (*func == X86_JMPIW) func += mem_loads32(func + 1) + 5;
- if_cold (!os_mprot(func, 5, PAGE_EXECUTE_READWRITE)) return 0;
+ // later: that dumb hack has now ended up having implications in the
+ // redesign of the entire API. :-)
+ while (*p == X86_JMPIW) p += mem_loads32(p + 1) + 5;
+ void *prologue = p;
int len = 0;
for (;;) {
- // FIXME: these cases may result in somewhat dodgy error messaging. They
- // shouldn't happen anyway though. Maybe if we're confident we just
- // compile 'em out of release builds some day, but that sounds a little
- // scary. For now preferring confusing messages over crashes, I guess.
- if (func[len] == X86_CALL) {
- con_warn("hook_inline: can't trampoline call instructions\n");
- return 0;
+ if_cold (p[len] == X86_CALL) {
+ return (struct hook_inline_prep_ret){
+ 0, "can't trampoline call instructions"
+ };
}
- int ilen = x86_len(func + len);
- if (ilen == -1) {
- con_warn("hook_inline: unknown or invalid instruction\n");
- return 0;
+ int ilen = x86_len(p + len);
+ if_cold (ilen == -1) {
+ return (struct hook_inline_prep_ret){
+ 0, "unknown or invalid instruction"
+ };
}
len += ilen;
- if (len >= 5) break;
- if (func[len] == X86_JMPIW) {
- con_warn("hook_inline: can't trampoline jmp instructions\n");
- return 0;
+ if (len >= 5) {
+ // we should have statically made trampoline buffer size big enough
+ assume(curtrampoline - trampolines < sizeof(trampolines) - len - 6);
+ *curtrampoline = len; // stuff length in there for quick unhooking
+ uchar *newtrampoline = curtrampoline + 1;
+ curtrampoline += len + 6;
+ memcpy(newtrampoline, p, len);
+ newtrampoline[len] = X86_JMPIW;
+ u32 diff = p - (newtrampoline + 5); // goto the continuation
+ memcpy(newtrampoline + len + 1, &diff, 4);
+ *trampoline = newtrampoline;
+ return (struct hook_inline_prep_ret){prologue, 0};
+ }
+ if_cold (p[len] == X86_JMPIW) {
+ return (struct hook_inline_prep_ret){
+ 0, "can't trampoline jump instructions"
+ };
}
}
- // for simplicity, just bump alloc the trampoline. no need to free anyway
- if_cold (nexttrampoline - trampolines > sizeof(trampolines) - len - 6) {
- con_warn("hook_inline: out of trampoline space\n");
- return 0;
- }
- uchar *trampoline = nexttrampoline;
- nexttrampoline += len + 6; // NOT thread-safe. we don't need that anyway!
- *trampoline++ = len; // stick length in front for quicker unhooking
- memcpy(trampoline, func, len);
- trampoline[len] = X86_JMPIW;
- uint diff = func - (trampoline + 5); // goto the continuation
- memcpy(trampoline + len + 1, &diff, 4);
- diff = (uchar *)target - (func + 5); // goto the hook target
- func[0] = X86_JMPIW;
- memcpy(func + 1, &diff, 4);
- iflush(func, 5);
- return trampoline;
+}
+
+bool hook_inline_mprot(void *prologue) {
+ return os_mprot(prologue, 5, PAGE_EXECUTE_READWRITE);
+}
+
+void hook_inline_commit(void *restrict prologue, void *restrict target) {
+ uchar *p = prologue;
+ u32 diff = (uchar *)target - (p + 5); // goto the hook target
+ p[0] = X86_JMPIW;
+ memcpy(p + 1, &diff, 4);
}
void unhook_inline(void *orig) {
@@ -116,7 +97,6 @@ void unhook_inline(void *orig) {
int off = mem_loads32(p + len + 1);
uchar *q = p + off + 5;
memcpy(q, p, 5); // XXX: not atomic atm! (does any of it even need to be?)
- iflush(q, 5);
}
// vi: sw=4 ts=4 noet tw=80 cc=80
diff --git a/src/hook.h b/src/hook.h
index 09af156..0aeae73 100644
--- a/src/hook.h
+++ b/src/hook.h
@@ -18,6 +18,9 @@
#define INC_HOOK_H
#include "intdefs.h"
+#include "errmsg.h"
+#include "feature.h"
+#include "langext.h"
bool hook_init();
@@ -39,17 +42,110 @@ static inline void unhook_vtable(void **vtable, usize off, void *orig) {
}
/*
- * Returns a trampoline pointer, or null if hooking failed. Unlike hook_vtable,
- * handles memory protection for you.
+ * Finds the correct function prologue location to install an inline hook, and
+ * tries to initialise a trampoline with sufficient instructions and a jump back
+ * to enable calling the original function.
*
- * This function is not remotely thread-safe, and should never be called from
- * any thread besides the main one nor be used to hook anything that gets called
- * from other threads.
+ * This is a low-level API and in most cases, if doing hooking from inside a
+ * plugin feature, the hook_inline_featsetup() function should be used instead.
+ * It automatically performs conventional error logging for both this step and
+ * the hook_inline_mprot() call below, and returns error codes that are
+ * convenient for use in a feature INIT function.
+ *
+ * When this function succeeds, the returned struct will have the prologue
+ * member set to the prologue or starting point of the hooked function (which is
+ * not always the same as the original function pointer). The trampoline
+ * parameter, being a pointer-to-pointer, is an output parameter to which a
+ * trampoline pointer will be written. The trampoline is a small run of
+ * instructions from the original function, followed by a jump back to it,
+ * allowing the original to be seamlessly called from a hook.
+ *
+ * In practically rare cases, this function will fail due to unsupported
+ * instructions in the function prologue. In such instances, the returned struct
+ * will have a null prologue, and the second member err, will point to a
+ * null-terminated string for error logging. In this case, the trampoline
+ * pointer will remain untouched.
+ */
+struct hook_inline_prep_ret {
+ void *prologue;
+ const char *err;
+} hook_inline_prep(void *func, void **trampoline);
+
+/*
+ * This is a small helper function to make the memory page containing a
+ * function's prologue writable, allowing an inline hook to be inserted with
+ * hook_inline_commit().
+ *
+ * This is a low-level API and in most cases, if doing hooking from inside a
+ * plugin feature, the hook_inline_featsetup() function should be used instead.
+ * It automatically performs conventional error logging for both this step and
+ * the prior hook_inline_prep() call documented above, and returns error codes
+ * that are convenient for use in a feature INIT function.
+ *
+ * After using hook_inline_prep() to obtain the prologue and an appropriate
+ * trampoline, call this to unlock the prologue, and then use
+ * hook_inline_commit() to finalise the hook. In the event that multiple
+ * functions need to be hooked at once, the commit calls can be batched up at
+ * the end, removing the need for rollbacks since commitment is guaranteed to
+ * succeed after all setup is complete.
+ *
+ * This function returns true on success, or false if a failure occurs at the
+ * level of the OS memory protection API. os_lasterror() or errmsg_*sys() can be
+ * used to report such an error.
*/
-void *hook_inline(void *func, void *target);
+bool hook_inline_mprot(void *func);
+
+/*
+ * Finalises an inline hook set up using the hook_inline_prep() and
+ * hook_inline_mprot() functions above (or the hook_inline_featsetup() helper
+ * function below). prologue must be the prologue obtained via the
+ * aforementioned functons and target must be the function that will be jumped
+ * to in place of the original. It is very important that these functions are
+ * ABI-compatible lest obvious bad things happen.
+ *
+ * The resulting hook can be removed later by calling unhook_inline().
+ */
+void hook_inline_commit(void *restrict prologue, void *restrict target);
+
+/*
+ * This is a helper specifically for use in feature INIT code. It doesn't make
+ * much sense to call it elsewhere.
+ *
+ * Combines the functionality of the hook_inline_prep() and hook_inline_mprot()
+ * functions above, logs to the console on error automatically in a conventional
+ * format, and returns an error status that can be propagated straight from a
+ * feature INIT function.
+ *
+ * func must point to the original function to be hooked, orig must point to
+ * your trampoline pointer (which can in turn be used to call the original
+ * function indirectly from within your hook or elsewhere), and fname should be
+ * the name of the function for error logging purposes.
+ *
+ * If the err member of the returned struct is nonzero, simply return it as-is.
+ * Otherwise, the prologue member will contain the prologue pointer to pass to
+ * hook_inline_commit() to finalise the hook.
+ */
+static inline struct hook_inline_featsetup_ret {
+ void *prologue;
+ int err;
+} hook_inline_featsetup(void *func, void **orig, const char *fname) {
+ void *trampoline;
+ struct hook_inline_prep_ret prep = hook_inline_prep(func, &trampoline);
+ if_cold (prep.err) {
+ errmsg_warnx("couldn't hook %s function: %s", fname, prep.err);
+ return (struct hook_inline_featsetup_ret){0, FEAT_INCOMPAT};
+ }
+ if_cold (!hook_inline_mprot(prep.prologue)) {
+ errmsg_errorsys("couldn't hook %s function: %s", fname,
+ "couldn't make prologue writable");
+ return (struct hook_inline_featsetup_ret){0, FEAT_FAIL};
+ }
+ *orig = trampoline;
+ return (struct hook_inline_featsetup_ret){prep.prologue, 0};
+}
/*
- * Reverts the function to its original unhooked state. Takes the pointer to the
+ * Reverts a function to its original unhooked state. Takes the pointer to the
* callable "original" function, i.e. the trampoline, NOT the initial function
* pointer from before hooking.
*/
diff --git a/src/hud.c b/src/hud.c
index 8c861aa..cb360ca 100644
--- a/src/hud.c
+++ b/src/hud.c
@@ -64,41 +64,45 @@ DEF_EVENT(HudPaint, int /*width*/, int /*height*/)
// right calling convention (x86 Windows/MSVC is funny about passing structs...)
struct handlewrap { ulong x; };
-// CEngineVGui
-DECL_VFUNC_DYN(unsigned int, GetPanel, int)
-
-// vgui::ISchemeManager
-DECL_VFUNC_DYN(void *, GetIScheme, struct handlewrap)
-// vgui::IScheme
-DECL_VFUNC_DYN(struct handlewrap, GetFont, const char *, bool)
-
-// vgui::ISurface
-DECL_VFUNC_DYN(void, DrawSetColor, struct rgba)
-DECL_VFUNC_DYN(void, DrawFilledRect, int, int, int, int)
-DECL_VFUNC_DYN(void, DrawOutlinedRect, int, int, int, int)
-DECL_VFUNC_DYN(void, DrawLine, int, int, int, int)
-DECL_VFUNC_DYN(void, DrawPolyLine, int *, int *, int)
-DECL_VFUNC_DYN(void, DrawSetTextFont, struct handlewrap)
-DECL_VFUNC_DYN(void, DrawSetTextColor, struct rgba)
-DECL_VFUNC_DYN(void, DrawSetTextPos, int, int)
-DECL_VFUNC_DYN(void, DrawPrintText, hud_wchar *, int, int)
-DECL_VFUNC_DYN(void, GetScreenSize, int *, int *)
-DECL_VFUNC_DYN(int, GetFontTall, struct handlewrap)
-DECL_VFUNC_DYN(int, GetCharacterWidth, struct handlewrap, int)
-DECL_VFUNC_DYN(int, GetTextSize, struct handlewrap, const hud_wchar *,
- int *, int *)
-
-// vgui::Panel
-DECL_VFUNC_DYN(void, SetPaintEnabled, bool)
-
-static void *matsurf, *toolspanel, *scheme;
-
-typedef void (*VCALLCONV Paint_func)(void *);
+struct CEngineVGui;
+DECL_VFUNC_DYN(struct CEngineVGui, unsigned int, GetPanel, int)
+
+struct ISchemeManager;
+DECL_VFUNC_DYN(struct ISchemeManager, void *, GetIScheme, struct handlewrap)
+struct IScheme;
+DECL_VFUNC_DYN(struct IScheme, struct handlewrap, GetFont, const char *, bool)
+
+struct ISurface;
+DECL_VFUNC_DYN(struct ISurface, void, DrawSetColor, struct rgba)
+DECL_VFUNC_DYN(struct ISurface, void, DrawFilledRect, int, int, int, int)
+DECL_VFUNC_DYN(struct ISurface, void, DrawOutlinedRect, int, int, int, int)
+DECL_VFUNC_DYN(struct ISurface, void, DrawLine, int, int, int, int)
+DECL_VFUNC_DYN(struct ISurface, void, DrawPolyLine, int *, int *, int)
+DECL_VFUNC_DYN(struct ISurface, void, DrawSetTextFont, struct handlewrap)
+DECL_VFUNC_DYN(struct ISurface, void, DrawSetTextColor, struct rgba)
+DECL_VFUNC_DYN(struct ISurface, void, DrawSetTextPos, int, int)
+DECL_VFUNC_DYN(struct ISurface, void, DrawPrintText, hud_wchar *, int, int)
+DECL_VFUNC_DYN(struct ISurface, void, GetScreenSize, int *, int *)
+DECL_VFUNC_DYN(struct ISurface, int, GetFontTall, struct handlewrap)
+DECL_VFUNC_DYN(struct ISurface, int, GetCharacterWidth, struct handlewrap, int)
+DECL_VFUNC_DYN(struct ISurface, int, GetTextSize, struct handlewrap,
+ const hud_wchar *, int *, int *)
+
+struct IPanel { void **vtable; };
+DECL_VFUNC_DYN(struct IPanel, void, SetPaintEnabled, bool)
+
+static struct ISurface *matsurf;
+static struct IPanel *toolspanel;
+static struct IScheme *scheme;
+
+typedef void (*VCALLCONV Paint_func)(struct IPanel *);
static Paint_func orig_Paint;
-void VCALLCONV hook_Paint(void *this) {
- int width, height;
- hud_screensize(&width, &height);
- if (this == toolspanel) EMIT_HudPaint(width, height);
+void VCALLCONV hook_Paint(struct IPanel *this) {
+ if (this == toolspanel) {
+ int width, height;
+ hud_screensize(&width, &height);
+ EMIT_HudPaint(width, height);
+ }
orig_Paint(this);
}
@@ -147,8 +151,8 @@ void hud_textsize(ulong font, const ushort *s, int *width, int *height) {
GetTextSize(matsurf, (struct handlewrap){font}, s, width, height);
}
-static bool find_toolspanel(void *enginevgui) {
- const uchar *insns = (const uchar *)VFUNC(enginevgui, GetPanel);
+static bool find_toolspanel(struct CEngineVGui *enginevgui) {
+ const uchar *insns = enginevgui->vtable[vtidx_GetPanel];
for (const uchar *p = insns; p - insns < 16;) {
// first CALL instruction in GetPanel calls GetRootPanel, which gives a
// pointer to the specified panel
@@ -170,7 +174,7 @@ INIT {
errmsg_errorx("couldn't get MatSystemSurface006 interface");
return FEAT_INCOMPAT;
}
- void *schememgr = factory_engine("VGUI_Scheme010", 0);
+ struct ISchemeManager *schememgr = factory_engine("VGUI_Scheme010", 0);
if_cold (!schememgr) {
errmsg_errorx("couldn't get VGUI_Scheme010 interface");
return FEAT_INCOMPAT;
@@ -179,13 +183,12 @@ INIT {
errmsg_errorx("couldn't find engine tools panel");
return FEAT_INCOMPAT;
}
- void **vtable = *(void ***)toolspanel;
- if_cold (!os_mprot(vtable + vtidx_Paint, sizeof(void *),
+ if_cold (!os_mprot(toolspanel->vtable + vtidx_Paint, sizeof(void *),
PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
return FEAT_FAIL;
}
- orig_Paint = (Paint_func)hook_vtable(vtable, vtidx_Paint,
+ orig_Paint = (Paint_func)hook_vtable(toolspanel->vtable, vtidx_Paint,
(void *)&hook_Paint);
SetPaintEnabled(toolspanel, true);
// 1 is the default, first loaded scheme. should always be sourcescheme.res
@@ -196,7 +199,7 @@ INIT {
END {
// don't unhook toolspanel if exiting: it's already long gone!
if_cold (sst_userunloaded) {
- unhook_vtable(*(void ***)toolspanel, vtidx_Paint, (void *)orig_Paint);
+ unhook_vtable(toolspanel->vtable, vtidx_Paint, (void *)orig_Paint);
SetPaintEnabled(toolspanel, false);
}
}
diff --git a/src/inputhud.c b/src/inputhud.c
index 1ce919a..b93e9d8 100644
--- a/src/inputhud.c
+++ b/src/inputhud.c
@@ -61,7 +61,7 @@ DEF_FEAT_CVAR_MINMAX(sst_inputhud_y,
"Input HUD y position (fraction between screen top and bottom)",
0.95, 0, 1, CON_ARCHIVE)
-static void *input;
+static struct CInput { void **vtable; } *input;
static int heldbuttons = 0, tappedbuttons = 0;
static struct rgba colours[3] = {
@@ -98,8 +98,8 @@ struct CUserCmd {
};
#define vtidx_GetUserCmd_l4dbased vtidx_GetUserCmd
-DECL_VFUNC_DYN(struct CUserCmd *, GetUserCmd, int)
-DECL_VFUNC_DYN(struct CUserCmd *, GetUserCmd_l4dbased, int, int)
+DECL_VFUNC_DYN(struct CInput, struct CUserCmd *, GetUserCmd, int)
+DECL_VFUNC_DYN(struct CInput, struct CUserCmd *, GetUserCmd_l4dbased, int, int)
typedef void (*VCALLCONV CreateMove_func)(void *, int, float, bool);
static CreateMove_func orig_CreateMove;
@@ -114,16 +114,17 @@ static void VCALLCONV hook_CreateMove(void *this, int seq, float ft,
if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; }
}
// basically a dupe, but calling the other version of GetUserCmd
-static void VCALLCONV hook_CreateMove_l4dbased(void *this, int seq, float ft,
- bool active) {
+static void VCALLCONV hook_CreateMove_l4dbased(struct CInput *this, int seq,
+ float ft, bool active) {
orig_CreateMove(this, seq, ft, active);
struct CUserCmd *cmd = GetUserCmd_l4dbased(this, -1, seq);
if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; }
}
-typedef void (*VCALLCONV DecodeUserCmdFromBuffer_func)(void *, void *, int);
-typedef void (*VCALLCONV DecodeUserCmdFromBuffer_l4dbased_func)(void *, int,
+typedef void (*VCALLCONV DecodeUserCmdFromBuffer_func)(struct CInput *,
void *, int);
+typedef void (*VCALLCONV DecodeUserCmdFromBuffer_l4dbased_func)(struct CInput *,
+ int, void *, int);
static union {
DecodeUserCmdFromBuffer_func prel4d;
DecodeUserCmdFromBuffer_l4dbased_func l4dbased;
@@ -131,13 +132,13 @@ static union {
#define orig_DecodeUserCmdFromBuffer _orig_DecodeUserCmdFromBuffer.prel4d
#define orig_DecodeUserCmdFromBuffer_l4dbased \
_orig_DecodeUserCmdFromBuffer.l4dbased
-static void VCALLCONV hook_DecodeUserCmdFromBuffer(void *this, void *reader,
- int seq) {
+static void VCALLCONV hook_DecodeUserCmdFromBuffer(struct CInput *this,
+ void *reader, int seq) {
orig_DecodeUserCmdFromBuffer(this, reader, seq);
struct CUserCmd *cmd = GetUserCmd(this, seq);
if (cmd) { heldbuttons = cmd->buttons; tappedbuttons |= cmd->buttons; }
}
-static void VCALLCONV hook_DecodeUserCmdFromBuffer_l4dbased(void *this,
+static void VCALLCONV hook_DecodeUserCmdFromBuffer_l4dbased(struct CInput *this,
int slot, void *reader, int seq) {
orig_DecodeUserCmdFromBuffer_l4dbased(this, slot, reader, seq);
struct CUserCmd *cmd = GetUserCmd_l4dbased(this, slot, seq);
@@ -326,6 +327,7 @@ static void reloadfonts() {
HANDLE_EVENT(HudPaint, int screenw, int screenh) {
if (!con_getvari(sst_inputhud)) return;
if_cold (screenw != lastw || screenh != lasth) reloadfonts();
+ lastw = screenw; lasth = screenh;
int basesz = screenw > screenh ? screenw : screenh;
int boxsz = ceilf(basesz * 0.025f);
if (boxsz < 24) boxsz = 24;
@@ -365,13 +367,12 @@ HANDLE_EVENT(HudPaint, int screenw, int screenh) {
}
// find the CInput "input" global
-static inline bool find_input(void* vclient) {
+static inline bool find_input(struct VClient *vclient) {
#ifdef _WIN32
// the only CHLClient::DecodeUserCmdFromBuffer() does is call a virtual
// function, so find its thisptr being loaded into ECX
- void* decodeusercmd =
- (*(void***)vclient)[vtidx_VClient_DecodeUserCmdFromBuffer];
- for (uchar *p = (uchar *)decodeusercmd; p - (uchar *)decodeusercmd < 32;) {
+ uchar *insns = vclient->vtable[vtidx_VClient_DecodeUserCmdFromBuffer];
+ for (uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_MOVRMW && p[1] == X86_MODRM(0, 1, 5)) {
void **indirect = mem_loadptr(p + 2);
input = *indirect;
@@ -386,7 +387,7 @@ static inline bool find_input(void* vclient) {
}
INIT {
- void *vclient;
+ struct VClient *vclient;
if (!(vclient = factory_client("VClient015", 0)) &&
!(vclient = factory_client("VClient016", 0)) &&
!(vclient = factory_client("VClient017", 0))) {
@@ -397,7 +398,7 @@ INIT {
errmsg_errorx("couldn't find input global");
return FEAT_INCOMPAT;
}
- void **vtable = mem_loadptr(input);
+ void **vtable = input->vtable;
// just unprotect the first few pointers (GetUserCmd is 8)
if_cold (!os_mprot(vtable, sizeof(void *) * 8, PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
@@ -431,25 +432,26 @@ 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;
}
END {
- void **vtable = mem_loadptr(input);
- unhook_vtable(vtable, vtidx_CreateMove, (void *)orig_CreateMove);
+ unhook_vtable(input->vtable, vtidx_CreateMove, (void *)orig_CreateMove);
// N.B.: since the orig_ function is in a union, we don't have to worry
// about which version we're unhooking
- unhook_vtable(vtable, vtidx_DecodeUserCmdFromBuffer,
+ unhook_vtable(input->vtable, vtidx_DecodeUserCmdFromBuffer,
(void *)orig_DecodeUserCmdFromBuffer);
}
diff --git a/src/kvsys.c b/src/kvsys.c
index 7996928..9c0f75c 100644
--- a/src/kvsys.c
+++ b/src/kvsys.c
@@ -16,8 +16,6 @@
*/
#include "abi.h"
-#include "con_.h"
-#include "engineapi.h"
#include "extmalloc.h"
#include "errmsg.h"
#include "feature.h"
@@ -25,19 +23,21 @@
#include "hook.h"
#include "kvsys.h"
#include "langext.h"
-#include "mem.h"
#include "os.h"
#include "vcall.h"
#include "x86.h"
FEATURE()
-void *KeyValuesSystem(); // vstlib symbol
-static void *kvs;
+struct IKeyValuesSystem { void **vtable; };
+
+struct IKeyValuesSystem *KeyValuesSystem(); // vstlib symbol
+static struct IKeyValuesSystem *kvs;
static int vtidx_GetSymbolForString = 3, vtidx_GetStringForSymbol = 4;
static bool iskvv2 = false;
-DECL_VFUNC_DYN(int, GetSymbolForString, const char *, bool)
-DECL_VFUNC_DYN(const char *, GetStringForSymbol, int)
+DECL_VFUNC_DYN(struct IKeyValuesSystem, int, GetSymbolForString, const char *,
+ bool)
+DECL_VFUNC_DYN(struct IKeyValuesSystem, const char *, GetStringForSymbol, int)
const char *kvsys_symtostr(int sym) { return GetStringForSymbol(kvs, sym); }
int kvsys_strtosym(const char *s) { return GetSymbolForString(kvs, s, true); }
@@ -100,16 +100,16 @@ INIT {
// kvs ABI check is probably relevant for other games, but none that we
// currently actively support
if (GAMETYPE_MATCHES(L4D2x)) {
- void **kvsvt = mem_loadptr(kvs);
- detectabichange(kvsvt);
- if_cold (!os_mprot(kvsvt + vtidx_GetStringForSymbol, sizeof(void *),
- PAGE_READWRITE)) {
+ void **vtable = kvs->vtable;
+ detectabichange(vtable);
+ if_cold (!os_mprot(vtable + vtidx_GetStringForSymbol,
+ sizeof(void *), PAGE_READWRITE)) {
errmsg_warnx("couldn't make KeyValuesSystem vtable writable");
errmsg_note("won't be able to prevent any nag messages");
}
else {
orig_GetStringForSymbol = (GetStringForSymbol_func)hook_vtable(
- kvsvt, vtidx_GetStringForSymbol,
+ vtable, vtidx_GetStringForSymbol,
(void *)hook_GetStringForSymbol);
}
}
@@ -118,7 +118,7 @@ INIT {
END {
if (orig_GetStringForSymbol) {
- unhook_vtable(*(void ***)kvs, vtidx_GetStringForSymbol,
+ unhook_vtable(kvs->vtable, vtidx_GetStringForSymbol,
(void *)orig_GetStringForSymbol);
}
}
diff --git a/src/l4d1democompat.c b/src/l4d1democompat.c
index 63c67e9..1cfe959 100644
--- a/src/l4d1democompat.c
+++ b/src/l4d1democompat.c
@@ -31,11 +31,12 @@
FEATURE("Left 4 Dead 1 demo file backwards compatibility")
GAMESPECIFIC(L4D1_1022plus)
+struct CDemoFile;
// NOTE: not bothering to put this in gamedata since it's actually a constant.
// We could optimise the gamedata system further to constant-fold things with no
// leaves beyond the GAMESPECIFIC cutoff or whatever. But that sounds annoying.
#define off_CDemoFile_protocol 272
-DEF_ACCESSORS(int, CDemoFile_protocol)
+DEF_ACCESSORS(struct CDemoFile, int, CDemoFile_protocol)
// L4D1 bumps the demo protocol version with every update to the game, which
// means whenever there is a security update, you cannot watch old demos. From
@@ -49,9 +50,9 @@ static GetHostVersion_func orig_GetHostVersion;
typedef void (*VCALLCONV ReadDemoHeader_func)(void *);
static ReadDemoHeader_func orig_ReadDemoHeader;
-static inline bool find_ReadDemoHeader(con_cmdcb cb) {
+static inline bool find_ReadDemoHeader(con_cmdcb listdemo_cb) {
// Find the call to ReadDemoHeader in the listdemo callback
- const uchar *insns = (const uchar*)cb;
+ 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 &&
@@ -106,7 +107,7 @@ static int hook_GetHostVersion() {
}
static int *this_protocol;
-static void VCALLCONV hook_ReadDemoHeader(void *this) {
+static void VCALLCONV hook_ReadDemoHeader(struct CDemoFile *this) {
// The mid-function hook needs to get the protocol from `this`, but by that
// point we won't be able to rely on the ECX register and/or any particular
// stack spill layout. So... offset the pointer and stick it in a global.
@@ -135,9 +136,9 @@ static int hook_midpoint() {
}
INIT {
- con_cmdcb orig_listdemo_cb = con_findcmd("listdemo")->cb;
- if_cold (!orig_listdemo_cb) return FEAT_INCOMPAT;
- if_cold (!find_ReadDemoHeader(orig_listdemo_cb)) {
+ 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)) {
errmsg_errorx("couldn't find ReadDemoHeader function");
return FEAT_INCOMPAT;
}
@@ -150,29 +151,22 @@ INIT {
return FEAT_INCOMPAT;
}
gameversion = orig_GetHostVersion();
- orig_GetHostVersion = (GetHostVersion_func)hook_inline(
- (void *)orig_GetHostVersion, (void *)&hook_GetHostVersion);
- if (!orig_GetHostVersion) {
- errmsg_errorsys("couldn't hook GetHostVersion");
- return FEAT_FAIL;
- }
- orig_ReadDemoHeader = (ReadDemoHeader_func)hook_inline(
- (void *)orig_ReadDemoHeader, (void *)&hook_ReadDemoHeader);
- if (!orig_ReadDemoHeader) {
- errmsg_errorsys("couldn't hook ReadDemoHeader");
- goto e1;
- }
- ReadDemoHeader_midpoint = hook_inline(
- (void *)ReadDemoHeader_midpoint, (void *)&hook_midpoint);
- if (!ReadDemoHeader_midpoint) {
- errmsg_errorsys("couldn't hook ReadDemoHeader midpoint");
- goto e2;
- }
+ struct hook_inline_featsetup_ret h1 = hook_inline_featsetup(
+ (void *)orig_GetHostVersion, (void **)&orig_GetHostVersion,
+ "GetHostVersion");
+ if_cold (h1.err) return h1.err;
+ struct hook_inline_featsetup_ret h2 = hook_inline_featsetup(
+ (void *)orig_ReadDemoHeader, (void **)&orig_ReadDemoHeader,
+ "ReadDemoHeader");
+ if_cold (h2.err) return h2.err;
+ struct hook_inline_featsetup_ret h3 = hook_inline_featsetup(
+ ReadDemoHeader_midpoint, &ReadDemoHeader_midpoint,
+ "ReadDemoHeader midpoint");
+ if_cold (h3.err) return h3.err;
+ hook_inline_commit(h1.prologue, (void *)&hook_GetHostVersion);
+ hook_inline_commit(h2.prologue, (void *)&hook_ReadDemoHeader);
+ hook_inline_commit(h3.prologue, (void *)&hook_midpoint);
return FEAT_OK;
-
-e2: unhook_inline((void *)orig_ReadDemoHeader);
-e1: unhook_inline((void *)orig_GetHostVersion);
- return FEAT_FAIL;
}
END {
diff --git a/src/l4daddon.c b/src/l4daddon.c
index 0609459..fd344ce 100644
--- a/src/l4daddon.c
+++ b/src/l4daddon.c
@@ -54,7 +54,7 @@ static char last_mission[128] = {0}, last_gamemode[128] = {0};
static int last_addonvecsz = 0;
static bool last_disallowaddons = false;
-DECL_VFUNC_DYN(void, ManageAddonsForActiveSession)
+DECL_VFUNC_DYN(struct VEngineClient, void, ManageAddonsForActiveSession)
// Crazy full name: FileSystem_ManageAddonsForActiveSession. Hence the acronym.
// Note: the 4th parameter was first added in 2.2.0.4 (21 Oct 2020), but we
@@ -86,9 +86,10 @@ static void hook_FS_MAFAS(bool disallowaddons, char *mission, char *gamemode,
// are enabled. Both disconnecting from a server and using the addon menu
// call FS_MAFAS to allow every enabled VPK to be loaded, so we let those
// calls go through. This fixes the vast majority of laggy cases without
- // breaking anything practice.
+ // breaking anything in practice.
//
- // As a bonus, doing all this also seems to speed up map loads by about 1s.
+ // As a bonus, doing all this also seems to speed up map loads by about 1s
+ // per skipped call, which ends up being quite a significant saving.
//
// TODO(opt): There's one unhandled edge case reconnecting the to the same
// server we were just on, if the server hasn't changed maps. It's unclear
@@ -96,53 +97,51 @@ static void hook_FS_MAFAS(bool disallowaddons, char *mission, char *gamemode,
int curaddonvecsz = *addonvecsz;
if (curaddonvecsz != last_addonvecsz) {
- // addons list has changed, meaning we're in the main menu. we will have
- // already been called with null mission and/or gamemode and reset the
- // last_ things, so update the count then call the original function.
+ // If the length changed, the list obviously changed, which also implies
+ // we're in the main menu. We'll have already just been passed nulls for
+ // gamemode and mission, so just note the size and continue as normal.
last_addonvecsz = curaddonvecsz;
- goto e;
}
-
- // if we have zero addons loaded, we can skip doing anything else.
- if (!curaddonvecsz) return;
-
- // we have some addons, which may or may not have changed. based on the
- // above assumption that nothing will change *during* a campaign, cache
- // campaign and mode names try to early-exit if neither has changed. the
- // mission string can be empty if playing a gamemode not supported by the
- // current map (such as survival on c8m1), so always call the original in
- // that case since we can't know whether we changed campaigns or not.
- if (mission && gamemode && *mission) {
+ else if (!curaddonvecsz) {
+ // We have zero addons loaded, and nothing changed, so skip the call.
+ return;
+ }
+ else if (mission && gamemode && *mission) {
+ // We have some addons, and the count didn't change, but the exact list
+ // could have. However, we assume nothing changes *during* a campaign.
+ // If we know what both the mission and gamemode are, and we know they
+ // haven't changed, then we can skip the cache invalidation call.
int missionlen = strlen(mission + 1) + 1;
int gamemodelen = strlen(gamemode);
- if (missionlen < sizeof(last_mission) &&
+ bool canskip = false;
+ if_hot (missionlen < sizeof(last_mission) &&
gamemodelen < sizeof(last_gamemode)) {
- bool canskip = disallowaddons == last_disallowaddons &&
+ canskip = disallowaddons == last_disallowaddons &&
!strncmp(mission, last_mission, missionlen + 1) &&
!strncmp(gamemode, last_gamemode, gamemodelen + 1);
- if_hot (canskip) {
- disallowaddons = last_disallowaddons;
- memcpy(last_mission, mission, missionlen + 1);
- memcpy(last_gamemode, gamemode, gamemodelen + 1);
- return;
- }
}
+ last_disallowaddons = disallowaddons;
+ memcpy(last_mission, mission, missionlen + 1);
+ memcpy(last_gamemode, gamemode, gamemodelen + 1);
+ if_hot (canskip) return;
}
-
- // If we get here, we don't know for sure whether something might have
- // changed, so we have to assume it did; we reset our cached values to avoid
- // any false negatives in future, and then call the original function.
- last_disallowaddons = false;
- last_mission[0] = '\0';
- last_gamemode[0] = '\0';
-
-e: orig_FS_MAFAS(disallowaddons, mission, gamemode, ismutation);
+ else {
+ // If we get here, either we've left a game (null mission and gamemode)
+ // or been given an unknown mission (empty string). The latter case
+ // happens whenever the map doesn't support the campaign, e.g. c8m1
+ // survival, and implies we have to assume something might have changed.
+ // In either case, reset our cached values to prevent and subsequent
+ // false positives.
+ last_disallowaddons = false;
+ last_mission[0] = '\0';
+ last_gamemode[0] = '\0';
+ }
+ orig_FS_MAFAS(disallowaddons, mission, gamemode, ismutation);
}
static inline bool find_FS_MAFAS() {
#ifdef _WIN32
- const uchar *insns = (const uchar *)VFUNC(engclient,
- ManageAddonsForActiveSession);
+ const uchar *insns = engclient->vtable[vtidx_ManageAddonsForActiveSession];
// CEngineClient::ManageAddonsForActiveSession just calls FS_MAFAS
for (const uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_CALL) {
@@ -178,43 +177,36 @@ static inline bool find_addonvecsz(con_cmdcb show_addon_metadata_cb) {
static void *broken_addon_check = 0;
static uchar orig_broken_addon_check_bytes[13];
-enum { SMALLNOP = 9, BIGNOP = 13 };
-static inline bool nop_addon_check(int noplen) {
- // In versions prior to 2204 (21 Oct 2020), FS_MAFAS checks if any
- // addons are enabled before doing anything else. If no addons are enabled,
- // then the function just returns immediately. FS_MAFAS gets called by
- // update_addon_paths, which is run when you click 'Done' in the addons
- // menu. This means that turning off all addons breaks everything until the
- // game is restarted or another addon is loaded. To fix this, we just
- // replace the CMP and JZ instructions with NOPs. Depending on the version
- // of the code, we either have to replace 9 bytes (e.g. 2203) or 13 bytes
- // (e.g. 2147). So, we have a 9-byte NOP followed by a 4-byte NOP and can
- // just use the given length value.
- static const uchar nops[] =
- HEXBYTES(66, 0F, 1F, 84, 00, 00, 00, 00, 00, 0F, 1F, 40, 00);
- // NOTE: always using 13 for orig even though noplen can be 9 or 13; not
- // worth tracking that vs. just always putting 13 bytes back later.
- // Also passing 13 to mprot just in case the instructions straddle a page
- // boundary (unlikely, of course).
- if_hot (os_mprot(broken_addon_check, 13, PAGE_EXECUTE_READWRITE)) {
- memcpy(orig_broken_addon_check_bytes, broken_addon_check, 13);
- memcpy(broken_addon_check, nops, noplen);
- return true;
- }
- else {
- errmsg_warnsys("couldn't fix broken addon check: "
- "couldn't make make memory writable");
- return false;
- }
-}
-
static inline void try_fix_broken_addon_check() {
uchar *insns = (uchar *)orig_FS_MAFAS;
for (uchar *p = insns; p - insns < 32;) {
if (p[0] == X86_ALUMI8S && p[1] == X86_MODRM(0, 7, 5) &&
mem_loadptr(p + 2) == addonvecsz) {
- if (nop_addon_check(p[7] == X86_2BYTE ? SMALLNOP : BIGNOP)) {
+ // In versions prior to 2204 (21 Oct 2020), FS_MAFAS checks if any
+ // addons are enabled before doing anything else. If no addons are
+ // enabled, then the function just returns immediately. FS_MAFAS
+ // gets called by update_addon_paths, which is run when you click
+ // 'Done' in the addons menu. This means that turning off all addons
+ // breaks everything until the game is restarted or another addon is
+ // loaded. To fix this, we just replace the CMP and JZ instructions
+ // with NOPs. Depending on the version of the code, we either have
+ // to replace 9 bytes (e.g. 2203) or 13 bytes (e.g. 2147). So, we
+ // have a 9-byte NOP followed by a 4-byte NOP and can just determine
+ // a length to use by peeking at an instruction.
+ static const uchar nops[] =
+ HEXBYTES(66, 0F, 1F, 84, 00, 00, 00, 00, 00, 0F, 1F, 40, 00);
+ int noplen = p[7] == X86_2BYTE ? 13 : 9;
+ // note: we always copy 13 to orig so we can put it back
+ // unconditionally without having to store a length. give 13 to
+ // mprot too just so there's no page boundary issues
+ if_hot (os_mprot(p, 13, PAGE_EXECUTE_READWRITE)) {
broken_addon_check = p; // conditional so END doesn't crash!
+ memcpy(orig_broken_addon_check_bytes, broken_addon_check, 13);
+ memcpy(broken_addon_check, nops, noplen);
+ }
+ else {
+ errmsg_warnsys("couldn't fix broken addon check: "
+ "couldn't make make memory writable");
}
return;
}
@@ -241,12 +233,11 @@ INIT {
return FEAT_INCOMPAT;
}
try_fix_broken_addon_check();
- orig_FS_MAFAS = (FS_MAFAS_func)hook_inline((void *)orig_FS_MAFAS,
- (void *)&hook_FS_MAFAS);
- if_cold (!orig_FS_MAFAS) {
- errmsg_errorsys("couldn't hook FileSystem_ManageAddonsForActiveSession");
- return FEAT_FAIL;
- }
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_FS_MAFAS, (void **)&orig_FS_MAFAS,
+ "FileSystem_ManageAddonsForActiveSession");
+ if_cold (h.err) return h.err;
+ hook_inline_commit(h.prologue, (void *)&hook_FS_MAFAS);
return FEAT_OK;
}
diff --git a/src/l4dmm.c b/src/l4dmm.c
index 40d3c57..67af36d 100644
--- a/src/l4dmm.c
+++ b/src/l4dmm.c
@@ -21,10 +21,8 @@
#include "errmsg.h"
#include "feature.h"
#include "gamedata.h"
-#include "gametype.h"
#include "kvsys.h"
#include "langext.h"
-#include "mem.h"
#include "os.h"
#include "vcall.h"
@@ -34,9 +32,12 @@ REQUIRE(kvsys)
REQUIRE_GAMEDATA(vtidx_GetMatchNetworkMsgController)
REQUIRE_GAMEDATA(vtidx_GetActiveGameServerDetails)
-DECL_VFUNC_DYN(void *, GetMatchNetworkMsgController)
-DECL_VFUNC_DYN(struct KeyValues *, GetActiveGameServerDetails,
- struct KeyValues *)
+struct IMatchFramework;
+struct IMatchNetworkMsgController;
+DECL_VFUNC_DYN(struct IMatchFramework, struct IMatchNetworkMsgController*,
+ GetMatchNetworkMsgController)
+DECL_VFUNC_DYN(struct IMatchNetworkMsgController, struct KeyValues *,
+ GetActiveGameServerDetails, struct KeyValues *)
// Old L4D1 uses a heavily modified version of the CMatchmaking in Source 2007.
// None of it is publicly documented or well-understood but I was able to figure
@@ -47,12 +48,14 @@ struct contextval {
const char *val;
/* other stuff unknown */
};
-DECL_VFUNC(struct contextval *, unknown_contextlookup, 67, const char *)
+struct CMatchmaking;
+DECL_VFUNC(struct CMatchmaking, struct contextval *, unknown_contextlookup, 67,
+ const char *)
static void *matchfwk;
static union { // space saving
struct { int sym_game, sym_campaign; }; // "game/campaign" KV lookup
- void *oldmmiface; // old L4D1 interface
+ struct CMatchmaking *oldmmiface; // old L4D1 interface
} U;
#define oldmmiface U.oldmmiface
#define sym_game U.sym_game
@@ -77,7 +80,8 @@ const char *l4dmm_curcampaign() {
return 0;
}
#endif
- void *ctrlr = GetMatchNetworkMsgController(matchfwk);
+ struct IMatchNetworkMsgController *ctrlr =
+ GetMatchNetworkMsgController(matchfwk);
struct KeyValues *kv = GetActiveGameServerDetails(ctrlr, 0);
if_cold (!kv) return 0; // not in server, probably
const char *ret = 0;
diff --git a/src/l4dreset.c b/src/l4dreset.c
index 2de75cd..725c763 100644
--- a/src/l4dreset.c
+++ b/src/l4dreset.c
@@ -28,8 +28,8 @@
#include "fastfwd.h"
#include "feature.h"
#include "gamedata.h"
-#include "gametype.h"
#include "gameserver.h"
+#include "gametype.h"
#include "hook.h"
#include "intdefs.h"
#include "langext.h"
@@ -55,11 +55,11 @@ REQUIRE_GAMEDATA(vtidx_GameFrame) // note: for L4D1 only, always defined anyway
REQUIRE_GAMEDATA(vtidx_GameShutdown)
REQUIRE_GAMEDATA(vtidx_OnGameplayStart)
-static void **votecontroller;
+static struct CVoteController **votecontroller;
static int off_callerrecords = -1;
static int off_voteissues;
-static void *director; // "TheDirector" server global
+struct CDirector { void **vtable; } *director; // "TheDirector" server global
// Note: the vote callers vector contains these as elements. We don't currently
// do anything with the structure, but we're keeping it here for reference.
@@ -73,12 +73,13 @@ static void *director; // "TheDirector" server global
};*/
struct CVoteIssue;
-DECL_VFUNC(const char *, SetIssueDetails, 1 + NVDTOR, const char *)
-DECL_VFUNC(const char *, GetDisplayString, 8 + NVDTOR)
-DECL_VFUNC(const char *, ExecuteCommand, 9 + NVDTOR)
+DECL_VFUNC(struct CVoteIssue, const char *, SetIssueDetails, 1 + NVDTOR,
+ const char *)
+DECL_VFUNC(struct CVoteIssue, const char *, GetDisplayString, 8 + NVDTOR)
+DECL_VFUNC(struct CVoteIssue, const char *, ExecuteCommand, 9 + NVDTOR)
-DEF_PTR_ACCESSOR(struct CUtlVector, voteissues)
-DEF_PTR_ACCESSOR(struct CUtlVector, callerrecords)
+DEF_PTR_ACCESSOR(struct CVoteController, struct CUtlVector, voteissues)
+DEF_PTR_ACCESSOR(struct CVoteController, struct CUtlVector, callerrecords)
static struct CVoteIssue *getissue(const char *textkey) {
struct CVoteIssue **issues = getptr_voteissues(*votecontroller)->m.mem;
@@ -234,9 +235,9 @@ HANDLE_EVENT(Tick, bool simulating) {
}
}
-typedef void (*VCALLCONV OnGameplayStart_func)(void *this);
+typedef void (*VCALLCONV OnGameplayStart_func)(struct CDirector *this);
static OnGameplayStart_func orig_OnGameplayStart;
-static void VCALLCONV hook_OnGameplayStart(void *this) {
+static void VCALLCONV hook_OnGameplayStart(struct CDirector *this) {
orig_OnGameplayStart(this);
if (nextmapnum) {
// if we changed map more than 1 time, cancel the reset. this'll happen
@@ -348,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;
}
@@ -357,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 {
@@ -380,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
@@ -500,8 +500,8 @@ ok: // Director::Update calls UnfreezeTeam after the first jmp instruction
return false;
}
-
-DECL_VFUNC_DYN(int, GetEngineBuildNumber)
+// XXX: duped def in democustom: should this belong somewhere else?
+DECL_VFUNC_DYN(struct VEngineClient, int, GetEngineBuildNumber)
INIT {
struct con_cmd *cmd_listissues = con_findcmd("listissues");
@@ -509,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;
@@ -533,7 +532,7 @@ INIT {
#ifdef _WIN32 // L4D1 has no Linux build, no need to check whether L4D2
if (GAMETYPE_MATCHES(L4D2)) {
#endif
- vtable = mem_loadptr(director);
+ vtable = director->vtable;
if_cold (!os_mprot(vtable + vtidx_OnGameplayStart, sizeof(*vtable),
PAGE_READWRITE)) {
errmsg_errorsys("couldn't make virtual table writable");
@@ -544,13 +543,16 @@ INIT {
#ifdef _WIN32 // L4D1 has no Linux build!
}
else /* L4D1 */ {
- void *GameFrame = (*(void ***)srvdll)[vtidx_GameFrame];
+ void *GameFrame = srvdll->vtable[vtidx_GameFrame];
if_cold (!find_UnfreezeTeam(GameFrame)) {
errmsg_errorx("couldn't find UnfreezeTeam function");
return FEAT_INCOMPAT;
}
- orig_UnfreezeTeam = (UnfreezeTeam_func)hook_inline(
- (void *)orig_UnfreezeTeam, (void *)&hook_UnfreezeTeam);
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_UnfreezeTeam, (void **)&orig_UnfreezeTeam,
+ "UnfreezeTeam");
+ if_cold (h.err) return h.err;
+ hook_inline_commit(h.prologue, (void *)&hook_UnfreezeTeam);
}
#endif
// Only try cooldown stuff for L4D2, since L4D1 always had unlimited votes.
diff --git a/src/l4dwarp.c b/src/l4dwarp.c
index 99d678e..462239d 100644
--- a/src/l4dwarp.c
+++ b/src/l4dwarp.c
@@ -47,9 +47,11 @@ REQUIRE_GAMEDATA(vtidx_AddBoxOverlay2)
REQUIRE_GAMEDATA(vtidx_AddLineOverlay)
REQUIRE_GAMEDATA(vtidx_Teleport)
-DECL_VFUNC_DYN(void, Teleport, const struct vec3f */*pos*/,
- const struct vec3f */*pos*/, const struct vec3f */*vel*/)
-DECL_VFUNC(const struct vec3f *, OBBMaxs, 2)
+// XXX: could make these calls type safe in future? just tricky because the
+// entity hierarchy is kind of crazy so it's not clear which type name to pick
+DECL_VFUNC_DYN(void, void, Teleport, const struct vec3f */*pos*/,
+ const struct vec3f */*ang*/, const struct vec3f */*vel*/)
+DECL_VFUNC(void, const struct vec3f *, OBBMaxs, 2)
// IMPORTANT: padsz parameter is missing in L4D1, but since it's cdecl, we can
// still call it just the same (we always pass 0, so there's no difference).
@@ -78,17 +80,18 @@ typedef void (*VCALLCONV CTraceFilterSimple_ctor)(
#define PLAYERMASK 0x0201420B
// debug overlay stuff, only used by sst_l4d_previewwarp
-static void *dbgoverlay;
-DECL_VFUNC_DYN(void, AddLineOverlay, const struct vec3f *,
- const struct vec3f *, int, int, int, bool, float)
-DECL_VFUNC_DYN(void, AddBoxOverlay2, const struct vec3f *,
+static struct IVDebugOverlay *dbgoverlay;
+DECL_VFUNC_DYN(struct IVDebugOverlay, void, AddLineOverlay,
+ const struct vec3f *, const struct vec3f *, int, int, int, bool, float)
+DECL_VFUNC_DYN(struct IVDebugOverlay, void, AddBoxOverlay2,
const struct vec3f *, const struct vec3f *, const struct vec3f *,
- const struct rgba *, const struct rgba *, float)
+ const struct vec3f *, const struct rgba *, const struct rgba *, float)
-DEF_ACCESSORS(struct vec3f, entpos)
-DEF_ACCESSORS(struct vec3f, eyeang)
-DEF_ACCESSORS(uint, teamnum)
-DEF_PTR_ACCESSOR(void, collision)
+// XXX: more type safety stuff here also
+DEF_ACCESSORS(void, struct vec3f, entpos)
+DEF_ACCESSORS(void, struct vec3f, eyeang)
+DEF_ACCESSORS(void, uint, teamnum)
+DEF_PTR_ACCESSOR(void, void, collision)
static struct vec3f warptarget(void *ent) {
struct vec3f org = get_entpos(ent), ang = get_eyeang(ent);
@@ -107,10 +110,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;
}
diff --git a/src/langext.h b/src/langext.h
index 0a17cb2..de96ef5 100644
--- a/src/langext.h
+++ b/src/langext.h
@@ -26,7 +26,7 @@
#define assume(x) ((void)(__assume(x), 0))
#define cold __declspec(noinline)
#else
-static inline _Noreturn void _invoke_ub(void) {}
+static inline _Noreturn void _invoke_ub() {}
#define unreachable (_invoke_ub())
#define assume(x) ((void)(!!(x) || (_invoke_ub(), 0)))
#define cold
diff --git a/src/nosleep.c b/src/nosleep.c
index a622044..b440bf3 100644
--- a/src/nosleep.c
+++ b/src/nosleep.c
@@ -42,9 +42,9 @@ static void VCALLCONV hook_SleepUntilInput(void *this, int timeout) {
}
PREINIT {
- if (con_findvar("engine_no_focus_sleep")) return false;
+ if (con_findvar("engine_no_focus_sleep")) return FEAT_SKIP;
con_regvar(engine_no_focus_sleep);
- return true;
+ return FEAT_OK;
}
INIT {
diff --git a/src/portalcolours.c b/src/portalcolours.c
index 7779cb3..24224c1 100644
--- a/src/portalcolours.c
+++ b/src/portalcolours.c
@@ -87,6 +87,9 @@ static bool find_UTIL_Portal_Color(void *base) {
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);
@@ -100,12 +103,11 @@ INIT {
errmsg_errorx("couldn't find UTIL_Portal_Color");
return FEAT_INCOMPAT;
}
- orig_UTIL_Portal_Color = (UTIL_Portal_Color_func)hook_inline(
- (void *)orig_UTIL_Portal_Color, (void *)&hook_UTIL_Portal_Color);
- if_cold (!orig_UTIL_Portal_Color) {
- errmsg_errorsys("couldn't hook UTIL_Portal_Color");
- return FEAT_INCOMPAT;
- }
+ struct hook_inline_featsetup_ret h = hook_inline_featsetup(
+ (void *)orig_UTIL_Portal_Color, (void **)&orig_UTIL_Portal_Color,
+ "UTIL_Portal_Color");
+ if_cold (h.err) return h.err;
+ hook_inline_commit(h.prologue, (void *)&hook_UTIL_Portal_Color);
sst_portal_colour0->cb = &colourcb;
sst_portal_colour1->cb = &colourcb;
sst_portal_colour2->cb = &colourcb;
diff --git a/src/portalisg.c b/src/portalisg.c
new file mode 100644
index 0000000..ffa80f2
--- /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 "con_.h"
+#include "engineapi.h"
+#include "errmsg.h"
+#include "feature.h"
+#include "gamedata.h"
+#include "intdefs.h"
+#include "langext.h"
+#include "mem.h"
+#include "x86.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) {
+ struct con_cmdargs disconnect_args = {0};
+ disconnect_cb(&disconnect_args);
+ *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 850efee..1e957b0 100644
--- a/src/rinput.c
+++ b/src/rinput.c
@@ -126,8 +126,8 @@ static uint VCALLCONV hook_GetRawMouseAccumulators(void *this, int *x, int *y) {
INIT {
bool has_rawinput = !!con_findvar("m_rawinput");
if (has_rawinput) {
- if (!has_vtidx_GetRawMouseAccumulators) return false;
- if (!inputsystem) return false;
+ if (!has_vtidx_GetRawMouseAccumulators) return FEAT_INCOMPAT;
+ if (!inputsystem) return FEAT_INCOMPAT;
vtable_insys = mem_loadptr(inputsystem);
// XXX: this is kind of duping nosleep, but that won't always init...
if_cold (!os_mprot(vtable_insys + vtidx_GetRawMouseAccumulators,
@@ -180,22 +180,17 @@ INIT {
goto ok;
}
- orig_GetCursorPos = (GetCursorPos_func)hook_inline((void *)&GetCursorPos,
- (void *)&hook_GetCursorPos);
- if_cold (!orig_GetCursorPos) {
- errmsg_errorsys("couldn't hook %s", "GetCursorPos");
- goto e0;
- }
- orig_SetCursorPos = (SetCursorPos_func)hook_inline((void *)&SetCursorPos,
- (void *)&hook_SetCursorPos);
- if_cold (!orig_SetCursorPos) {
- errmsg_errorsys("couldn't hook %s", "SetCursorPos");
- goto e1;
- }
+ int err;
+ struct hook_inline_featsetup_ret h1 = hook_inline_featsetup(
+ (void *)GetCursorPos, (void **)&orig_GetCursorPos, "GetCursorPos");
+ if_cold (err = h1.err) goto e0;
+ struct hook_inline_featsetup_ret h2 = hook_inline_featsetup(
+ (void *)SetCursorPos, (void **)&orig_SetCursorPos, "SetCursorPos");
+ if_cold (err = h2.err) goto e0;
inwin = CreateWindowExW(0, L"RInput", L"RInput", 0, 0, 0, 0, 0, 0, 0, 0, 0);
if_cold (!inwin) {
errmsg_errorsys("couldn't create input window");
- goto e2;
+ goto e0;
}
RAWINPUTDEVICE rd = {
.hwndTarget = inwin,
@@ -204,18 +199,19 @@ INIT {
};
if_cold (!RegisterRawInputDevices(&rd, 1, sizeof(rd))) {
errmsg_errorsys("couldn't create raw mouse device");
- goto e3;
+ err = FEAT_FAIL;
+ goto e1;
}
+ 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;
return FEAT_OK;
-e3: DestroyWindow(inwin);
-e2: unhook_inline((void *)orig_SetCursorPos);
-e1: unhook_inline((void *)orig_GetCursorPos);
+e1: DestroyWindow(inwin);
e0: UnregisterClassW(L"RInput", 0);
- return FEAT_FAIL;
+ return err;
}
END {
diff --git a/src/sst.c b/src/sst.c
index a60ad36..5d681c4 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 != 9
+#if VERSION_MAJOR != 0 || VERSION_MINOR != 14
#error Need to change this manually, since gluegen requires it to be spelled \
out in DEF_CVAR - better yet, can we get rid of this yet?
#endif
-DEF_CVAR(__sst_0_9_beta, "", 0, CON_HIDDEN | CON_DEMO)
+DEF_CVAR(__sst_0_l4_beta, "", 0, CON_HIDDEN | CON_DEMO)
// most plugin callbacks are unused - define dummy functions for each signature
static void VCALLCONV nop_v_v(void *this) {}
@@ -270,28 +270,27 @@ static bool already_loaded = false, skip_unload = false;
// auto-update message. see below in do_featureinit()
static const char *updatenotes = "\
-* Fixed the plugin crashing on game exit\n\
-* Fixed a crash under Wine/Proton\n\
-* Fixed sst_l4d_quickreset in L4D1 No Mercy\n\
-* Added sst_inputhud to visualise inputs in-game or in demo playback\n\
-* Increased sst_mouse_factor limit from 20 to 100\n\
-* sst_l4d_testwarp now performs the take-control unsticking step by default\n\
-* Added sst_l4d_previewwarp to visualise warp unsticking logic\n\
-* sst_l4d_quickreset now fixes the Swamp Fever/Crash Course \"god mode glitch\"\n\
-* Added a fix for lag/stuttering in newer L4D2 versions caused by addon loading\n\
-* Added a fix for disabling all addons in L4D2 requiring a game restart\n\
-* Removed multiplayer chat rate limit in L4D series and Portal 2\n\
-* Made L4D1 demo playback backwards-compatible for Steam version demos (1022+)\n\
-* plugin_unload now displays an error when used incorrectly (without a number)\n\
-* Rewrote and optimised a whole bunch of internal stuff\n\
+* Added sst_portal_resetisg as as stopgap solution for Portal runners\n\
";
-enum { // used in generated code, must line up with
+enum { // used in generated code, must line up with featmsgs arrays below
REQFAIL = _FEAT_INTERNAL_STATUSES,
NOGD,
NOGLOBAL
};
-static const char *const featmsgs[] = { // "
+#ifdef SST_DBG
+static const char *const _featmsgs[] = {
+ "%s: SKIP\n",
+ "%s: OK\n",
+ "%s: FAIL\n",
+ "%s: INCOMPAT\n",
+ "%s: REQFAIL\n",
+ "%s: NOGD\n",
+ "%s: NOGLOBAL\n"
+};
+#define featmsgs (_featmsgs + 1)
+#else
+static const char *const featmsgs[] = {
" [ OK! ] %s\n",
" [ FAILED! ] %s (error in initialisation)\n",
" [ unsupported ] %s (incompatible with this game or engine)\n",
@@ -299,6 +298,7 @@ static const char *const featmsgs[] = { // "
" [ unsupported ] %s (missing required gamedata entry)\n",
" [ FAILED! ] %s (failed to access engine)\n"
};
+#endif
static inline void successbanner() { // called by generated code
con_colourmsg(&(struct rgba){64, 255, 64, 255},
@@ -335,6 +335,19 @@ static void do_featureinit() {
}
// ... and now for the real magic! (n.b. this also registers feature cvars)
initfeatures();
+#ifdef SST_DBG
+ struct rgba purple = {192, 128, 240, 255};
+ con_colourmsg(&purple, "Matched gametype tags: ");
+ bool first = true;
+#define PRINTTAG(x) \
+if (GAMETYPE_MATCHES(x)) { \
+ con_colourmsg(&purple, "%s%s", first ? "" : ", ", #x); \
+ first = false; \
+}
+ GAMETYPE_BASETAGS(PRINTTAG)
+#undef PRINTTAG
+ con_colourmsg(&purple, "\n"); // xkcd 2109-compliant whitespace
+#endif
// if we're autoloaded and the external autoupdate script downloaded a new
// version, let the user know about the cool new stuff!
@@ -355,16 +368,16 @@ static void do_featureinit() {
}
}
-typedef void (*VCALLCONV VGuiConnect_func)(void *this);
+typedef void (*VCALLCONV VGuiConnect_func)(struct CEngineVGui *this);
static VGuiConnect_func orig_VGuiConnect;
-static void VCALLCONV hook_VGuiConnect(void *this) {
+static void VCALLCONV hook_VGuiConnect(struct CEngineVGui *this) {
orig_VGuiConnect(this);
do_featureinit();
fixes_apply();
- unhook_vtable(*(void ***)vgui, vtidx_VGuiConnect, (void *)orig_VGuiConnect);
+ unhook_vtable(vgui->vtable, vtidx_VGuiConnect, (void *)orig_VGuiConnect);
}
-DECL_VFUNC_DYN(bool, VGuiIsInitialized)
+DECL_VFUNC_DYN(struct CEngineVGui, bool, VGuiIsInitialized)
// --- Magical deferred load order hack nonsense! ---
// VDF plugins load right after server.dll, but long before most other stuff. We
@@ -385,13 +398,13 @@ static bool deferinit() {
// CEngineVGui::IsInitialized() which works everywhere.
if (VGuiIsInitialized(vgui)) return false;
sst_earlyloaded = true; // let other code know
- if_cold (!os_mprot(*(void ***)vgui + vtidx_VGuiConnect, ssizeof(void *),
+ if_cold (!os_mprot(vgui->vtable + vtidx_VGuiConnect, ssizeof(void *),
PAGE_READWRITE)) {
errmsg_warnsys("couldn't make CEngineVGui vtable writable for deferred "
"feature setup");
goto e;
}
- orig_VGuiConnect = (VGuiConnect_func)hook_vtable(*(void ***)vgui,
+ orig_VGuiConnect = (VGuiConnect_func)hook_vtable(vgui->vtable,
vtidx_VGuiConnect, (void *)&hook_VGuiConnect);
return true;
@@ -405,7 +418,7 @@ 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 con_cmdcbv2 orig_plugin_load_cb, orig_plugin_unload_cb;
static int ownidx; // XXX: super hacky way of getting this to do_unload()
@@ -413,63 +426,71 @@ 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) &&
+ return (plugin->basename[0] == 0 || plugin->basename[0] == 1) &&
plugin->v1.theplugin && plugin->v1.ifacever < 256 &&
plugin->v1.ifacever;
}
static void hook_plugin_load_cb(const struct con_cmdargs *args) {
- if (args->argc == 1) return;
- if (!CHECK_AllowPluginLoading(true)) return;
+ if (args->argc > 1 && !CHECK_AllowPluginLoading(true)) return;
+ int prevnplugins = pluginhandler->plugins.sz;
orig_plugin_load_cb(args);
- EMIT_PluginLoaded();
+ // 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;
- }
- if (*end) {
- errmsg_errorx("unexpected trailing characters "
- "(plugin_unload takes a number)");
- return;
- }
- 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;
+ if (args->argc > 1) {
+ if (!CHECK_AllowPluginLoading(false)) return;
+ if (!*args->argv[1]) {
+ errmsg_errorx("plugin_unload expects a number, got an empty string");
+ 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 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(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;
+ }
+ if (*end) {
+ errmsg_errorx("unexpected trailing characters "
+ "(plugin_unload takes a number)");
+ return;
}
+ 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;
+ 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
+ // thanks clang for forcing use of return here and ALSO warning!
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wpedantic"
- __attribute__((musttail)) return orig_plugin_unload_cb(args);
+ __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
+ }
+ // if it's some other plugin being unloaded, we can keep doing stuff
+ // after, so we raise the event.
+ orig_plugin_unload_cb(args);
+ EMIT_PluginUnloaded();
+ return;
+ }
}
- // if it's some other plugin being unloaded, we can keep doing stuff after
+ // if the index is either unspecified or out-of-range, let the original
+ // handler produce an appropriate error message
orig_plugin_unload_cb(args);
- EMIT_PluginUnloaded();
}
static bool do_load(ifacefactory enginef, ifacefactory serverf) {
@@ -496,11 +517,11 @@ 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;
+ orig_plugin_load_cb = cmd_plugin_load->cb_v2;
+ cmd_plugin_load->cb_v2 = &hook_plugin_load_cb;
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 = cmd_plugin_unload->cb_v2;
+ cmd_plugin_unload->cb_v2 = &hook_plugin_unload_cb;
}
return true;
}
@@ -508,8 +529,8 @@ 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;
+ cmd_plugin_load->cb_v2 = orig_plugin_load_cb;
+ cmd_plugin_unload->cb_v2 = orig_plugin_unload_cb;
#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
diff --git a/src/trace.c b/src/trace.c
index 2fe18c6..1118bdf 100644
--- a/src/trace.c
+++ b/src/trace.c
@@ -21,6 +21,7 @@
#include "gametype.h"
#include "intdefs.h"
#include "trace.h"
+#include "vcall.h"
FEATURE()
// TODO(compat): limiting to tested branches for now; support others as needed
@@ -35,10 +36,9 @@ struct ray {
bool isray, isswept;
};
-static void *srvtrace;
-
-DECL_VFUNC(void, TraceRay, 5, struct ray *, uint /*mask*/, void */*filter*/,
- struct CGameTrace *)
+static struct IEngineTraceServer *srvtrace;
+DECL_VFUNC(struct IEngineTraceServer, void, TraceRay, 5,
+ struct ray *, uint /*mask*/, void */*filter*/, struct CGameTrace *)
static inline bool nonzero(struct vec3f v) {
union { struct vec3f v; struct { unsigned int x, y, z; }; } u = {v};
diff --git a/src/vcall.h b/src/vcall.h
index 285bc79..f3a1f02 100644
--- a/src/vcall.h
+++ b/src/vcall.h
@@ -109,32 +109,33 @@
#define _VCALL_UNUSED
#endif
-#define _DECL_VFUNC_DYN(ret, conv, name, ...) \
- typedef typeof(ret) (*conv name##_func)(void * __VA_OPT__(,) __VA_ARGS__); \
- static inline _VCALL_UNUSED typeof(ret) name(void *this __VA_OPT__(,) \
- _VCALL_ARGLIST(__VA_ARGS__)) { \
+#define _DECL_VFUNC_DYN(class, ret, conv, name, ...) \
+ typedef typeof(ret) (*conv name##_func)(typeof(class) * __VA_OPT__(,) \
+ __VA_ARGS__); \
+ static inline _VCALL_UNUSED typeof(ret) name( \
+ typeof(class) *this __VA_OPT__(,) _VCALL_ARGLIST(__VA_ARGS__)) { \
_VCALL_RET(ret) VCALL(this, name __VA_OPT__(,) \
_VCALL_PASSARGS(__VA_ARGS__)); \
}
-#define _DECL_VFUNC(ret, conv, name, idx, ...) \
+#define _DECL_VFUNC(class, ret, conv, name, idx, ...) \
enum { vtidx_##name = (idx) }; \
- _DECL_VFUNC_DYN(ret, conv, name __VA_OPT__(,) __VA_ARGS__)
+ _DECL_VFUNC_DYN(class, ret, conv, name __VA_OPT__(,) __VA_ARGS__)
-/* Define a virtual function with a known index */
-#define DECL_VFUNC(ret, name, idx, ...) \
- _DECL_VFUNC(ret, VCALLCONV, name, idx __VA_OPT__(,) __VA_ARGS__)
+/* Define a virtual function with a known index. */
+#define DECL_VFUNC(class, ret, name, idx, ...) \
+ _DECL_VFUNC(class, ret, VCALLCONV, name, idx __VA_OPT__(,) __VA_ARGS__)
/* Define a virtual function with a known index, without thiscall convention */
-#define DECL_VFUNC_CDECL(ret, name, idx, ...) \
- _DECL_VFUNC(ret, , name, idx __VA_OPT__(,) __VA_ARGS__)
+#define DECL_VFUNC_CDECL(class, ret, name, idx, ...) \
+ _DECL_VFUNC(class, ret, , name, idx __VA_OPT__(,) __VA_ARGS__)
-/* Define a virtual function with an index defined elsewhere */
-#define DECL_VFUNC_DYN(ret, name, ...) \
- _DECL_VFUNC_DYN(ret, VCALLCONV, name __VA_OPT__(,) __VA_ARGS__)
+/* Define a virtual function with an index defined elsewhere (e.g. gamedata) */
+#define DECL_VFUNC_DYN(class, ret, name, ...) \
+ _DECL_VFUNC_DYN(class, ret, VCALLCONV, name __VA_OPT__(,) __VA_ARGS__)
/* Define a virtual function with an index defined elsewhere, without thiscall */
-#define DECL_VFUNC_CDECLDYN(ret, name, ...) \
- _DECL_VFUNC_DYN(ret, , name __VA_OPT__(,) __VA_ARGS__)
+#define DECL_VFUNC_CDECLDYN(class, ret, name, ...) \
+ _DECL_VFUNC_DYN(class, void, ret, , name __VA_OPT__(,) __VA_ARGS__)
#endif
diff --git a/src/version.h b/src/version.h
index fc82f66..6e60474 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 9
-#define VERSION "0.9"
+#define VERSION_MINOR 14
+#define VERSION "0.14"
diff --git a/src/wincrt.c b/src/wincrt.c
index 177ce45..d8111ba 100644
--- a/src/wincrt.c
+++ b/src/wincrt.c
@@ -10,29 +10,59 @@
//
// Is it actually reasonable to have to do any of this? Of course not.
-// TODO(opt): this feels like a sad implementation, can we do marginally better?
-int memcmp(const void *x_, const void *y_, unsigned int sz) {
+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"
+ "repz cmpsb\n"
+ : "+D" (x), "+S" (y), "+c" (sz), "=@cca"(a), "=@ccb"(b)
+ :
+ : "ax", "memory"
+ );
+ return b - a;
+#else
const char *x = x_, *y = y_;
for (unsigned int i = 0; i < sz; ++i) {
if (x[i] > y[i]) return 1;
if (x[i] < y[i]) return -1;
}
return 0;
+#endif
}
void *memcpy(void *restrict x, const void *restrict y, unsigned int sz) {
-#ifdef __clang__
+#if defined(__GNUC__) || defined(__clang__)
+ void *r = x;
__asm__ volatile (
- "rep movsb\n" :
- "+D" (x), "+S" (y), "+c" (sz) :
+ "rep movsb\n"
+ : "+D" (x), "+S" (y), "+c" (sz)
:
- "memory"
+ : "memory"
);
-#else // terrible fallback just in case someone wants to use this with MSVC
+ return r;
+#else
char *restrict xb = x; const char *restrict yb = y;
for (unsigned int i = 0; i < sz; ++i) xb[i] = yb[i];
+ return x;
#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,
diff --git a/src/x86.c b/src/x86.c
index 5bd9e4c..b017a70 100644
--- a/src/x86.c
+++ b/src/x86.c
@@ -18,7 +18,7 @@
#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
+ // 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
diff --git a/src/x86.h b/src/x86.h
index 04418d6..92e4ccb 100644
--- a/src/x86.h
+++ b/src/x86.h
@@ -558,7 +558,7 @@ enum {
* 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!,
- * SSE, MMX, AVX, and such. Aims to be small and fast rather than comprehensive.
+ * SSE3+, MMX, AVX, and such. Aims to be small and fast, not comprehensive.
*/
int x86_len(const void *insn);
diff --git a/src/xhair.c b/src/xhair.c
index e0017ba..9d1ee34 100644
--- a/src/xhair.c
+++ b/src/xhair.c
@@ -26,7 +26,7 @@
FEATURE("custom crosshair drawing")
REQUIRE(hud)
-DECL_VFUNC_DYN(bool, IsInGame)
+DECL_VFUNC_DYN(struct VEngineClient, bool, IsInGame)
DEF_FEAT_CVAR(sst_xhair, "Enable custom crosshair", 0, CON_ARCHIVE)
DEF_FEAT_CVAR(sst_xhair_colour,
diff --git a/test/hook.test.c b/test/hook.test.c
index 9e7cfa9..625fdbf 100644
--- a/test/hook.test.c
+++ b/test/hook.test.c
@@ -30,16 +30,28 @@ __attribute__((noinline)) static int func2(int a, int b) { return a - b; }
static int (*orig_func2)(int, int);
static int hook2(int a, int b) { return orig_func2(a, b) + 5; }
+// basic reimplementation of old API to support existing test cases.
+// XXX: we could probably have tests at the boundaries of the new API too,
+// although the current tests are only testing for regressions in x86 jmp logic.
+static inline void *test_hook_inline(void *func, void *target) {
+ void *trampoline;
+ struct hook_inline_prep_ret prep = hook_inline_prep(func, &trampoline);
+ if (prep.err) return 0;
+ if (!hook_inline_mprot(prep.prologue)) return 0;
+ hook_inline_commit(prep.prologue, target);
+ return trampoline;
+}
+
TEST("Inline hooks should be able to wrap the original function") {
if (!hook_init()) return false;
- orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ orig_func1 = (testfunc)test_hook_inline((void *)&func1, (void *)&hook1);
if (!orig_func1) return false;
return func1(5, 5) == 15;
}
TEST("Inline hooks should be removable again") {
if (!hook_init()) return false;
- orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ orig_func1 = (testfunc)test_hook_inline((void *)&func1, (void *)&hook1);
if (!orig_func1) return false;
unhook_inline((void *)orig_func1);
return func1(5, 5) == 10;
@@ -47,9 +59,9 @@ TEST("Inline hooks should be removable again") {
TEST("Multiple functions should be able to be inline-hooked at once") {
if (!hook_init()) return false;
- orig_func1 = (testfunc)hook_inline((void *)&func1, (void *)&hook1);
+ orig_func1 = (testfunc)test_hook_inline((void *)&func1, (void *)&hook1);
if (!orig_func1) return false;
- orig_func2 = (testfunc)hook_inline((void *)&func2, (void *)&hook2);
+ orig_func2 = (testfunc)test_hook_inline((void *)&func2, (void *)&hook2);
if (!orig_func2) return false;
return func2(5, 5) == 5;
}
diff --git a/tools/mkbindist.bat b/tools/mkbindist.bat
index 215f92d..924430b 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 2024, 8, 26, 0, 0, 0
-powershell (Get-Item TEMP-%name%\LICENCE).LastWriteTime = new-object DateTime 2024, 8, 26, 0, 0, 0
+powershell (Get-Item TEMP-%name%\sst.dll).LastWriteTime = new-object DateTime 2025, 6, 27, 0, 0, 0
+powershell (Get-Item TEMP-%name%\LICENCE).LastWriteTime = new-object DateTime 2025, 6, 27, 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..fe0f62f
--- /dev/null
+++ b/tools/windbg/initcmds
@@ -0,0 +1,6 @@
+.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 to the caller.
+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"
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..11cf29c
--- /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 /g /c $^<tools\windbg\initcmds
+
+:end
+exit /b %errorlevel%
+
+:: vi: sw=4 ts=4 noet tw=80 cc=80