ASI files and modding
ASI files seem to be used in several video game modding communities. They're typically applied to a game by a community-made "ASI loader." However, information on what these things actually are seems to be scattered across several different sources, and on an Internet whose primary search engine is dying, it's not easy to find them all if you don't already have some idea of what you're looking for.
I want to be upfront about the fact that I'm writing about ASIs as an outsider to the scenes where they're normally used, so here's my background on the topic:
I'm a mod author in the community for The Elder Scrolls V: Skyrim, and am experienced with code injection for that game. In the Bethesda RPG modding scene, code injection is centralized through community-made "script extenders," like SKSE and SFSE, made by the xSE team. These typically extend their respective games' script APIs with numerous functions that are useful for mod authors, and can load community-made DLLs as a secondary function. DLLs that are loaded through xSE are expected to communicate with the script extender through specific entry points.
As of this writing, Starfield has only recently been released, and Bethesda has not published any modding tools; in fact, much of their mod loader has actually been stripped out of the executable. This means that SFSE is also quite new, and only offers DLL loading, with no script extensions. Some mod authors have promoted ASIs as a competing mod loading mechanism, so I — having never heard of ASIs before — went looking for info and decided to gather everything I found in one spot. I've also written about xSE plug-ins, in order to better connect ASIs to what I already know.
The origins of ASIs
The Miles Sound System is audio middleware created and owned by RAD Game Tools, Inc., the same company behind Bink Video. It was initially developed in the late 1990s[1] and named after one of its designers, John Miles[2]. In 1998[1], a new feature was added: the ability for MSS to load Audio Stream Interface[3] files. These *.asi
files were essentially just DLLs with the file extension changed, each of which gave MSS the ability to decode new audio codecs. MSS would load any ASI files located in its directory.
In 2002, Rockstar released the PC version of Grand Theft Auto III, which used the Miles Sound System. The GTA modding community soon discovered that the game (through MSS) would load *.asi
files and treat them as DLLs. The community took advantage of this, writing their own DLLs to patch the game engine and renaming those files to use the *.asi
file extension.[3] Grand Theft Auto: San Andreas and onward did not include functionality to load ASIs, so the community began building their own ASI loaders and performing code injection on their own. ASI loaders spread into other game modding communities; for example, ME3Tweaks, a mod manager for the Mass Effect series, can load ASIs.
ASIs are often incorrectly referred to as "ASI scripts," and some people mistakenly assume that the acronym stands for "arbitrary script injection." This mislabeling comes from the fact that some ASIs rely on "script hook" DLLs that expose Grand Theft Auto's scripting functions to
How ASI files run
The way ASI files work is pretty straightforward; there's not much to talk about.
ASI loaders generally don't provide any support to the ASIs they load: no APIs, no helper functions, nothing. ASI files are loaded directly and will typically execute all of their code right from DllMain
. Some will instead spawn a worker thread from DllMain
to do their patching (here's one example), generally to avoid the DllMain
loader lock. They rely on busy-waiting or Win32 timers to poll for specific parts of the game's load process.
Libraries exist to help people write ASIs for specific games, with hardcoded offsets for game engine functions. DK22Pac's Plugin SDK is one example for Grand Theft Auto: San Andreas, Vice City, and Grand Theft Auto III. Some ASIs also rely on signature scanning to locate game functions via pattern matching. However, ASI loaders themselves don't offer direct access to game functions, and don't typically come with their own libraries that do so.
Because ASI loaders are so basic, they can be added onto pretty much any game, and pretty easily. Generally, this requires shimming a DLL that the game needs — that is, impersonating a DLL like Bink Video so that the game loads your code, and then having your impostor load the real DLL along with any other DLLs you want to load. Any DLL can do this, but having everything go through a single loader can help to reduce the risk of conflicts and avoid the need to daisy-chain a bunch of DLLs. Some ASI loaders also offer extra qualify-of-life features; for example, ThirteenAG's Ultimate ASI Loader has crash logging, and Silent's ASI Loader varies which ASIs it injects into different versions of Grand Theft Auto: San Andreas by comparing the ASIs' file paths to the game's filename.
How xSE files run
xSE, the "script extenders" available for Bethesda RPGs, are more fully-featured. They offer a variety of interfaces that a loaded DLL plug-in can make use of, many of which are specific to the game being hooked. They also include a more involved load process, during which xSE runs a variety of compatibility checks against the plug-in to ensure that it's compatible with the specific version of the game that you're playing on. Of course, this also means that xSE is purpose-built for each game that it supports, and this requires game-specific research and development by its authors; supporting a new game is a non-trivial task.
Older versions of xSE would load a plug-in and call its "query" function, letting the plug-in run compatibility checks on its own. Newer versions of xSE (beginning with the newest versions of SKSE64 for The Elder Scrolls V: Skyrim Special Edition) are more robust. Modern plug-ins are supposed to export a data structure containing information about which game versions they're compatible with, and what measures they take to achieve compatibility. For example, plug-ins can declare that they rely on Address Library (SSE; SF), a community-maintained resource for allowing plug-ins to target different game patches, and in that case xSE will check whether Address Library is properly set up for the user's current game version. Plug-ins which fail these compatibility checks will never even have the opportunity to run any code, ensuring that they can't break anything.
In addition to compatibility checking, the 64-bit Skyrim Script Extender (SKSE) offers the following APIs for all loaded plug-ins:
- The messaging interface
-
SKSE allows plug-ins to register callbacks for specific messages related to the game's operation. Plug-ins can request to run code after all SKSE plug-ins have loaded, after all game data is loaded, before or after a save file is loaded, when a new save file is made, when a save file is deleted, and when a new playthrough is started. The messaging interface can also be used to dispatch messages to other SKSE plug-ins that have successfully loaded. This means that plug-ins don't have to use multi-threading, busy-waiting, and timers to wait for notable events during the game's load process.
- The Papyrus interface
-
SKSE allows plug-ins to add new functions to Papyrus, Skyrim's script engine, and it also adds plenty of new functions of its own. Plug-ins can use SKSE's Papyrus interface to register native code for their own custom script functions. Some SKSE plug-ins ship with Papyrus scripts coded in text, using
C++ for the bare minimum of their functionality; others exist as resources, adding script functions for mod authors who don't knowC++ . - The serialization interface
-
Often, plug-ins will need to save their own state alongside a player's save file. Patching the game's save file format isn't easy, and having each DLL patch it individually isn't sustainable, so xSE offers its own solution: co-save files, which are stored alongside save files with their own extension (
*.ess
, "Elder Scrolls Save," for the game's save files;*.skse
for SKSE co-saves). SKSE's serialization interface allows plug-ins to register callbacks for when a save file is loaded or updated; plug-ins can then read or write data in the co-save. - The trampoline interface
-
When you run a program, Windows copies the full compiled code for that program into RAM. Plug-ins often work by overwriting portions of that code in RAM, to alter the game engine's behavior. Unfortunately, in a 64-bit environment, there are some tricky low-level considerations that come into play.
In compiled code, there are several instructions that refer to code or data locations. Some of these can only refer to data or code within a distance of 2GB (within RAM) from where the instruction itself is located. The most notable cases are "call" and "jump" instructions, which tell a program to start executing code at a given location. These each come in "short" and "far" flavors, where the "short" flavor has that 2GB range limit, while the "far" flavor isn't limited but takes more bytes to write. This all matters because 64-bit programs can load DLLs to varying memory locations, including ones that are more than 2GB away from the program's own code. The cleanest and most interoperable way for an xSE plug-in to patch over the game's code is to find an existing jump or call and overwrite it with a jump or call that takes the same number of bytes... but traveling from Skyrim's code to the plug-in's code may require a far jump or call, and Skyrim's executable isn't 2GB large, so all of its jumps and calls are going to be short.
To solve this problem, SKSE offers the ability to create a trampoline. On startup, SKSE reserves some memory near the game's code, and a plug-in can ask SKSE for a portion of that memory to use as a trampoline. The plug-in can overwrite one of the game's short jumps or calls to point to its personal trampoline, and then write a far jump into the trampoline that points to the plug-in's code. So, the game executes its own code until it hits the short jump or call to the trampoline; execution then immediately bounces out of the trampoline and into the xSE plug-in's code, which would've been out of reach otherwise.
In addition to offering these APIs, xSE comes with sample code that one can use as a base for building plug-ins. This sample code includes basic headers and class definitions for important game types, though for Skyrim Special Edition and onward, people more often use community-maintained libraries like CommonLibSSE and Address Library.
A comparison between ASI and xSE plug-ins, in brief
Feature | ASI | xSE |
---|---|---|
Load process | Completely barebones. ASIs don't have to go through any specific API and so are not subject to compatibility or correctness checking of any kind. Adding any such checks to a loader would break compatibility with all existing ASIs, and would be a large enough design change that it'd be better not to even use "ASI" terminology at that point. | Multiple forms of version and compatibility checking, mandatory for all plug-ins, to help cope with code changes in games that are still being updated. Use of a centrally maintained load process — there is one script extender maintained by one team, and everyone depends on it — means that xSE can reject or even hotfix plug-ins that are known to be defective. |
Script extensions | None, beyond what ASIs themselves manage on their own. | xSE typically adds new script functions to the game engine, and gives its plug-ins the ability to do the same. Where necessary (i.e. pre-Skyrim, where every script function needed a unique numeric ID), xSE can help to centrally coordinate the addition of new script functions. |
APIs offered to plug-ins | None. | Callback registration for notable game engine events, including through the game's load process. A centrally managed serialization system for storing data alongside the user's save files. Memory allocation APIs to help with trampoline creation in 64-bit. |
Scope and scale | ASI loaders are trivial to bolt onto any game, and game-agnostic loaders already exist. | xSE is developed by a specialized team for a specific series of games. Compatibility checks are game-specific. Other game-specific functionality generally matures as the game's own modding scene matures. |