-
Notifications
You must be signed in to change notification settings - Fork 6
Native Patch Implementation
For some reason, Firaxis decided to not release Civilization V with multiplayer modding support enabled, even though
much of the code for it already existed. Reactivating it is not possible solely from Lua code, as
Matchmaking.LaunchMultiplayerGame
eventually ends up calling CvModdingFrameworkAppSide::SetActiveDLCandMods
with an
empty mods list, hence deactivating all mods that may have been active. There seems to be no way around this, thus, a
patch to the binary is needed.
- src/patch/native
- common - Source files common to all platforms and versions
- inih - inih library, used to parse configuration
- luajit - LuaJIT library, used for... well, LuaJIT support
- win32 - Files specific to the Windows platform
- linux - Files specific to the Linux platform
- versions - Files specific to particular versions of Civilization V
The functions our patch hooks is exported to Lua code as DB.GetMemoryUsage
. The hook checks if a marker value is
passed into the function, and if it is, returns a different table that contains native patch's Lua API. Thus, to load
the injected API:
local patch = DB.GetMemoryUsage("216f0090-85dd-4061-8371-3d8ba2099a70")
if not patch.__mppatch_marker then error("Native patch not installed!") end
This exports a few functions and tables:
-
patch.version
, containing version information for the patch. -
patch.debugPrint(string)
, a function allowing a Lua script to output tomppatch_debug.log
. -
patch.shared
, a table that's shared between different script contexts. -
patch.config
, a table containing configuration for MPPatch stuff. -
patch.NetPatch
, a table contains functions that controls our modification to theSetActiveDLCAndMods
function. -
patch.getGlobals()
, a function equivalent to callinggetfenv()
-
patch.globals
, a table containing global functions normally unavailable to Lua code that our UI patch uses. Currently, therawget
andrawset
functions are exposed through this table. -
patch.luajit_version
, a string containing the contents ofjit.version
if LuaJIT is running, ornil
if C Lua is running.
Our patch to SetActiveDLCandMods
fundamentally works by replacing the arguments to the functions in a way controlled
by Lua code. These functions are exported in the patch.NetPatch
table, containing the following functions:
-
patch.NetPatch.pushMod(string modId, int version)
which pushes a mod to the list that will be loaded instead of whatever the original function passes. Notably, this list starts empty, and will not include the original list. -
patch.NetPatch.overrideModList()
which must be called to actually replace the original mod list with the one built up usingpushMod
. -
patch.NetPatch.overrideReloadMods(bool value)
, which overrides the flag passed toSetActiveDLCAndMods
which controls if mods are reloaded. Although I'm not sure of this, this flag seems to force mods to be reloaded, regardless of whether the mod list has changed at all. -
patch.NetPatch.pushDLC(int guid_1, int guid_2, int guid_3, int guid_4_high, int guid_4_low)
,patch.NetPatch.overrideDLCList()
, andpatch.NetPatch.overrideReloadDLC(bool value)
function identically to the above functions, except they operate on the DLC list instead of the mod list. The parameters topushDLC
are the 4 fields of the in-memory storage format Microsoft uses for GUIDs. TheData4
field is split into two values,guid_4_high
, containing the most significant 32 bits, andguid_4_low
, containing the least significant 32 bits. See here for documentation. -
patch.NetPatch.install()
, which causes the patch to SetActiveDLCandMods to actually be written to memory. Steam CEG or some other DRM system seems to interfere with the functioning of the patch, and this is an attempt to dodge it. -
patch.NetPatch.reset()
, which clears any overrides set. This function is automatically called after every time theSetActiveDLCAndMods
function is called for any reason to avoid any unintentional overrides.
On Windows, the patch creates a proxy around the CvGameDatabase.dll file, which gives us a foothold to patch the main binary, as well as providing our entry point to export functions to Lua. There are three versions of the main binary, so, we have to provide 3 sets of offsets for the function we override in it. Unfortunately, due to Steam's CEG DRM, we cannot use hashes of the binary to identify them, so we use executable names intead.
On Linux, however, the CvGameDatabase binary is statically compiled into the main binary, forcing us to patch the main binary instead of a shared library. Luckily, LD_PRELOAD lets us inject a patch into the main binary directly. Notably, on Linux, public symbols and many private symbols are not stripped, and are, in fact, exported. This means the patch doesn't need offsets for every function it uses hardcoded into it.