ModLoader Operation Principles and Mod Loading Process
SugarCube2 is a fully synchronous rendering engine that dynamically translates (assembles) game scripts (such as twee) into HTML in a completely synchronous manner (without any asynchronous operations) and displays them.
The game scripts (twee, JS, css) are embedded in the compiled webpage HTML (tw-storydata node) as text.
To modify the game, we have several methods:
- Modify the game scripts in the tw-storydata node of the webpage HTML before SugarCube2 compiles the game scripts into HTML, making SugarCube2 believe that our game was originally like this.
- Participate in the compilation process while SugarCube2 is translating the game scripts, dynamically changing the input and output of the compilation.
- After SugarCube2 compiles the game into HTML and displays it on the webpage, directly modify the displayed webpage content, making users feel that the game was originally like this.
For the above three methods, there are three corresponding implementations:
- To be compatible with the past method of directly modifying HTML to create mods, ModLoader modifies the game scripts in the tw-storydata node of the webpage HTML, making the script data format in memory exactly the same as after directly modifying the webpage. This function is exported to mod authors in the form of two addon mods, TweeReplacer and ReplacePatch, and their corresponding derivative addon mods from ModLoader.
- By making some invasive modifications to SugarCube2's Wikifier, ModLoader achieves reading, hooking, and intercepting the input and output of the compilation engine, providing the possibility to dynamically change the input and output of the compilation. This function is exported to mod authors in the form of the addon mod TweePrefixPostfixAddonMod.
- Since SugarCube2 comes with JQuery event messages for rendering completion, by listening to SugarCube2's passage rendering completion messages, we can accurately know the time point when SugarCube2's rendering is completed, and then we can modify the HTML content immediately after the passage is displayed by listening to this message. For this, emicoto implemented a tool called Simple Framework to bypass the complex and chaotic architecture of DoL itself and directly insert display content into HTML.
Focusing on the operation principles and mod loading process of ModLoader, the following introduction mainly involves Method 1 and its related content.
Since SugarCube2 is a fully synchronous rendering engine, to modify the game scripts in the tw-storydata node of the webpage before SugarCube2 starts, we need to execute all the pre-game loading tasks of ModLoader before it starts.
After carefully reading the source code of SugarCube2, we can find that the startup code of SugarCube2 is located in a jQuery(() => {})
closure function in sugarcube.js#L111
, which starts after the webpage is loaded. This means that if we can make some modifications in this closure and insert our startup script, then we can let ModLoader start before SugarCube2 starts.
Considering the design requirements of ModLoader, we find that ModLoader needs to perform a large number of asynchronous operations, including loading mods from remote, reading mod zip files from localStorage/indexDB, reading mod information from zip files, etc.
Therefore, we need to insert a Promise before the startup code of SugarCube2 that allows us to execute asynchronous code. After reviewing the source code of SugarCube2 and jQuery, we find that the only and most reliable method is to add a Promise and wrap the original startup code of SugarCube2, so that the startup code of SugarCube2 can wait for our asynchronous operations to complete.
We execute Modloader's startInit() function before SugarCube2 starts and begin initializing ModLoader.
First, we save all the content in the tw-storydata node of the original unmodified webpage HTML. initSC2DataInfoCache()
Since startInit() is a member function of SC2DataManager, this means that all internal objects and functional plugins in SC2DataManager will be initialized at the same time. Link This includes all functions implemented by ModLoader and made available to advanced mod authors.
After completing the above initialization process, the most important mod loading process begins.
The mod loading process is initiated by calling ModLoader.loadMod() from startInit()
.
The overall steps involved in mod loading are as follows:
- Read the mod's zip file from a source.
- Execute
scriptFileList_inject_early
andscriptFileList_earlyload
while also performing complex loading trigger logic. - Register the mod with Addon.
- Rebuild the
tw-storydata
node. - Execute
scriptFileList_preload
. - Start the normal execution process of SugarCube2.
The detailed process is as follows:
- Mods are loaded in the order of embedded HTML, remote servers, LocalStorage, and IndexDB. Dependency checks are performed using
DependenceChecker.checkFor()
. - The
boot.json
file within the mod is read using ModZipReader to understand the subsequent actions for the mod. - All JavaScript files listed in
scriptFileList_inject_early
are directly injected into the HTML by calling initModInjectEarlyLoadInDomScript(). Mods should complete their initialization here, but only synchronous operations are allowed. - Hooks such as
AddonPluginHookPoint.afterInjectEarlyLoad
,ModLoadControllerCallback.afterModLoad
, andAddonPluginHookPoint.afterModLoad
are triggered to notify all mods that the current mod has been loaded. This is where mods requiring very early execution can operate, and the hook calls will wait for any asynchronous operations to complete. - initModEarlyLoadScript() is called to execute all single-line commands in
scriptFileList_earlyload
. This uses JsPreloader.JsRunner(), which wraps the original JavaScript code in a function and waits for any asynchronous calls to finish. It is particularly important to note that the executor used here is JsPreloader.JsRunner(). The real implementation of this executor involves wrapping the code from the original JS file into a function that looks like(async () => {return ${jsCode}\n})()
and waits for the asynchronous call returned by the function to finish. This code, by adding areturn
instruction at the beginning of the entire file's first line, will, according to the semantics of JS'sreturn
, only execute the code on the first line of the JS file, or a closure function starting from the first line. - During
initModEarlyLoadScript()
, tryInitWaitingLazyLoadMod() is continuously called to check for mods that have added lazy-load mods and to load these mods. Encrypted mods use the lazy-load feature to decrypt and release the loaded mods at this stage. - Lazy-load mods, which are only read from the zip file at this point, have their
scriptFileList_inject_early
andscriptFileList_earlyload
executed simultaneously here, with continuous triggering of thecanLoadThisMod
hook. - After loading and executing the mod's JavaScript scripts, the
AddonPluginHookPoint.afterEarlyLoad
hook is triggered. - registerMod2Addon() is called to register all mods declared in
boot.json
withaddonPlugin
to their corresponding Addon Mods. - At this point, Addon Mods receive the mod registration callback from
AddonPluginHookPointExMustImplement.registerMod
, allowing them to record or perform actions based on their design. - The
AddonPluginHookPoint.afterRegisterMod2Addon
hook is triggered. allowing mods to modify the merged game script data. Mods likeTweeReplacer
andReplacePatch
perform their replacement calculations here. - This completes the loading of the mod's JavaScript functionalities.
- The
AddonPluginHookPoint.beforePatchModToGame
hook is triggered. - The
styleFileList
,scriptFileList
, andtweeFileList
data are merged into thetw-storydata
node, rebuilding it. - The
AddonPluginHookPoint.afterPatchModToGame
hook is triggered. - The
ModLoader.loadMod()
process ends, returning control to SugarCube2's code. - SugarCube2's code then calls JsPreloader.startLoad().
- Files in
scriptFileList_preload
are executed - The
AddonPluginHookPoint.afterPreload
hook is triggered. - The
ModLoadControllerCallback.ModLoaderLoadEnd
callback is triggered, marking the last hook event in the ModLoader loading process. Mods can complete their final tasks before SugarCube2 starts here. - The mod loading is complete, ModLoader has started, and the normal operation of SugarCube2 begins. From this point, all actions of ModLoader are triggered by SugarCube2.
- Modified the SugarCube2 startup point.
- For the Wikifier, added
_lastPassageQ
and corresponding data and operations to track the entire script compilation process. The purpose is to track and modify various compilation levels. This change involves all areas that touch compilation, mainly includingmacrolib.js
,parserlib.js
, andwikifier.js
. You can search usingpassageObj
as the keyword. - Intercepted img tags and svg tags to achieve the purpose of loading all images from memory without a server.