I've been working to add a few new systems to the Unified SDK to make modding easier to do through configuration files instead of relying on hard-coded settings so much.
Here are most of them in action:
These are all of the systems i've added, though some are not entirely finished:
String pool
This replaces the engine's
ALLOC_STRING
engine function, and changes behavior to allocate memory once per string, so calling it multiple times with the same string doesn't allocate more memory. Very efficient, this change also frees up a little memory in the engine's available memory pool.
It also no longer performs escape character parsing which required
game_text
to be changed to perform this parsing by itself. This behavior was inconsistent and could cause difficult to debug problems otherwise.
Better support for writing code that works on both client and server
The server's engine functions interface now has better support on the client which makes it easier to write code that works on both sides.
This also includes helper functions like
Con_Printf
which will unconditionally print to the console, and supports all of printf's format options (the engine's version is limited to C89 printf options).
Access to the engine's filesystem
The engine has a filesystem interface used to load files from game and mod directories. I've provided access to it, as well as a couple helper functions to easily load files without the risk of leaking memory.
You can use this to load files from the mod directory only if needed, for example server configuration files should never load from other directories to prevent custom and downloaded content from overriding it. Conversely you can also load files from all of those directories if needed, such as map configuration files.
Support for creating console variables and commands using unified syntax on client and server
The server requires you to create cvars whereas the client has the engine create them for you; i've created an abstraction that does this for you. This abstraction also prepends an
sv_
or
cl_
prefix automatically so you can have the same variables and commands on both sides.
You can set variables created this way using the new command line syntax
:command_name command_value
or
:(sv_|cl_)command_name command_value
.
If specified without the prefix it will apply to both the server and client versions. This also works properly for server variables which the engine will not initialize from the command line if you launch a listen server manually through the main menu.
Command functions can also be object methods by using a lambda to wrap it:
g_ConCommands.CreateCommand("my_command", [this](const auto& args) { MyCommandHandler(args); });
void MyClass::MyCommandHandler(const CCommandArgs& args)
{
Con_Printf("%d arguments:\n", args.Count());
for (int i = 0; i < args.Count(); ++i)
{
Con_Printf("d\n", args.Argument(i)));
}
}
CCommandArgs
is a thin wrapper around engine functions that makes it easier to work with commands by indicating which functions are available in all libraries.
Improved logging
I've added the spdlog library and set up the functionality to create loggers for subsystems. This allows you to log output with more control over how much is visible and what kind of output it is. As you can see in the screenshot above you can easily distinguish which system is logging something, and what kind of information it is.
This system loads settings from a configuration file. You can specify default logger settings so you can enable more debug output for all loggers, or even disable them all. You can also configure each logger individually.
There are console commands to list all of the loggers that exist, as well as to manually change the log level at runtime.
Angelscript-based scripting functionality
Bare bones Angelscript support has been added. The creation of script engines, contexts and modules is provided with error reporting for failure, as well as engine message logging, exception logging and proper handling for C++ exceptions including problematic behavior regarding longjmp (essentially C's version of exceptions), which could put the game engine or the script engine in an invalid state otherwise (note that longjmp does not seem to be caught by catch-all statements in VS2019, this used to happen in VS2012 but fail-safe logic should prevent the problem from occurring).
There is currently no scripting support for plugins and map scripts, but it's something i'd like to add if there's interest.
JSON-based configuration files
I've added support for loading JSON configuration files and parsing them. Error handling is done for you so if there's invalid JSON it'll be reported automatically.
I've also added JSON Schema-based extended error reporting for debugging purposes. This provides additional information when needed to indicate which part of the given JSON is invalid. It's only enabled with a debug cvar since it adds a lot of overhead.
Here's an example of the errors it will log (given an array instead of an object):
[startup] [error] Error validating JSON "/Defaults" with value "[{"LogLevel":"trace"}]": unexpected instance type
The JSON schemas can also be written to a file and used as input to tools that can generate JSON editors. This makes it easier to edit JSON and shows what kind of options you have.
I've also allowed the use of comments in JSON. This is a non-standard extension to the format, but is supported in most JSON libraries and the benefits are too good to ignore.
Game configuration files
Building on JSON support, there is a system for loading game configuration files. These are files that contain configuration data that needs to be applied every time a new map is loaded. This replaces server.cfg, listenserver.cfg and map change cfg files, which are mostly handled by the engine.
Additionally maps can have a config file as well. A file called
cfg/maps/<mapname>.json
will be loaded if the map with that name is started.
Here's an example of such a file used to configure the server for a new map:
{
"Includes": [
"cfg/shared.json"
],
"Sections": [
{
"Name": "Echo",
"Message": "Hello World!"
},
{
//Commands to configure multiplayer server
"Name": "Commands",
"Condition": "Multiplayer",
"Commands": [
"echo Hello World from command!",
// disable autoaim
"sv_aim 0",
// player bounding boxes (collisions, not clipping)
"sv_clienttrace 3.5",
// disable clients' ability to pause the server
"pausable 0",
// maximum client movement speed
"sv_maxspeed 270",
// load ban files"
"exec listip.cfg",
"exec banned.cfg"
]
}
]
}
The
Includes
value is a list of files to include before the current one, and makes sharing settings between servers and maps very easy. You could for instance make a map series where each map includes a file that contains the shared configuration before changing some for specific maps.
Here are the contents of the included file:
{
"Sections": [
{
"Name": "Echo",
"Message": "Hello World from shared.json!"
}
]
}
The
Sections
value is a list of section objects that apply to the game. Each section can have a condition associated with it evaluated at load time to determine whether the section should be used or not.
This condition is evaluated using Angelscript by wrapping it in a function:
bool Evaluate()
{
return condition;
}
To prevent abuse this evaluation will time out after 1 second, so the server won't lock up due to some clever infinite loop trick.
There are currently only two conditions to check:
Singleplayer
and
Multiplayer
. I plan to expand on this with gamemode detection (deathmatch, coop, teamplay, etc) as well as checking the map name and the value of cvars.
The
Echo
section simply prints the message to the console, useful for debugging to see if your file is getting loaded properly.
The
Commands
section provides the same functionality as the cfg files this system replaces. It lets you provide a list of console commands to execute.
For security purposes map config files are checked against a whitelist to limit the commands that can be executed. This prevents maps from doing things like changing the RCON password or the listen server host's name.
This whitelist is loaded from another JSON file:
[
"sv_gravity"
]
So server operators can manage this whitelist themselves if needed.
Though the configuration files for servers, maps and map changes are currently identical they are actually defined separately. Some will get sections exclusive to one or two of them in which case trying to use them in a format that doesn't have them will cause an error to be logged the console, but otherwise it will continue loading the file.
With this stuff implemented i can get to work implementing skill.json functionality and its map-specific equivalent. There are also a few changes needed to support things like Xen aliens fighting with Race X aliens (they're treated as allies by the game's code). Black ops have the same problem when it comes to fighting human military types (also allies).
I've also been updating the other SDKs to include bug fixes and improvements. There are some mistakes i made in Opposing Force that should be merged into any other projects that use that code.
There isn't much more work to do before a first release can happen. Since this is a lot of work i'll be doing an alpha release first to get some feedback, then at least one beta before a full release can be done.
That's all for now, feedback and suggestions are always welcome.