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