日本語 | English
A Mixin/Harmony-style modding framework for Python using AST injection.
Universal Modloader is not just a simple plugin loader.
It parses the source code of the target application (and its libraries) at runtime, injects code directly into the Abstract Syntax Tree (AST), and rebuilds it.
This allows you to modify function logic and access/rewrite local variables safely via simple decorators, without editing original source files.
Warning
Alpha Version / Experimental Technical Preview
This project is currently in Alpha. It serves as a Proof of Concept exploring the limits of Python's dynamic nature.
By design, this tool utilizes Runtime AST Injection to bypass standard safety mechanisms (such as scope and immutability) to enable "impossible" modifications.
Not intended for production use.
This tool prioritizes Power and Flexibility over Safety and Stability. APIs and internal structures are subject to change without notice. Please treat this as a research tool or a modding framework, not as a standard dependency.
- Runtime AST Injection:
No need to overwrite.pyfiles. All modifications take place in memory. - Local Variable Manipulation:
Access and modify variables inside functions using thectxobject. - Decorator-based API:
Simple and intuitive syntax inspired by Java's Mixin and C#'s Harmony (Unity). - High Versatility:
Can hook into not only the main script but also imported libraries (standard libraries or 3rd party packages).
Injects code at the start of the function. This is useful for modifying arguments or local variables before the main logic runs.
Target Code (main.py)
def take_damage(amount):
print(f"Ouch! Took {amount} damage.")Mod Code (mods/my_mod.py or mods/my_mod/__init__.py)
@uml.Inject("main", "take_damage", at=uml.At.HEAD())
def on_take_damage(ctx):
# Overwrite the local variable 'amount' before it is used
print("[Mod] Nullifying damage!")
ctx["amount"] = 0Injects code at the end of the function (before the return). Useful for logging or reading the final state of variables.
Target Code (main.py)
def heal_player():
hp = 100
print("Player healed.")Mod Code (mods/my_mod.py or mods/my_mod/__init__.py)
@uml.Inject("main", "heal_player", at=uml.At.TAIL())
def on_heal_player(ctx):
# Read the local variable 'hp'
current_hp = ctx["hp"]
print(f"[Mod] Player HP is now: {current_hp}")Injects code to override the return value.
Target Code (main.py)
def calculate_damage():
return random.randint(5, 15)Mod Code (mods/my_mod.py or mods/my_mod/__init__.py)
import universal_modloader as uml
@uml.Inject("main", "calculate_damage", at=uml.At.RETURN())
def on_calculate_damage(ctx):
print("[Mod] System: Damage calculation overridden!")
# Setting "__return__" forces the function to return this value
ctx["__return__"] = 0In this case, __return__ forces an overwrite of the original return value, so 0 is returned instead of the random integer.
Intercepts a specific function call inside the target function.
This is powerful for modifying arguments passed to a function before it executes, or changing its return value after it returns.
Target Code (main.py)
def main():
# The mod wants to change this name "Hero"
player = Player("Hero")
print(f"Welcome, {player.name}!")Mod Code (mods/my_mod.py or mods/my_mod/__init__.py)
CUSTOM_NAME = "ModdedHero"
# Hooks the 'Player(...)' call inside the 'main' function
@uml.Inject("main", "main", at=uml.At.INVOKE("Player"))
def on_create_player(ctx):
# ctx['args'] is a list of positional arguments passed to Player()
original_name = ctx['args'][0]
# Overwrite the argument
ctx['args'][0] = CUSTOM_NAME
print(f"[Mod] Player name changed from '{original_name}' to '{CUSTOM_NAME}'")By default, INVOKE triggers before the function is called, allowing argument modification. You can also use shift=uml.Shift.AFTER to modify the return value.
To install mods for a game or application:
- Copy the
modsfolder andloader.pyfrom this repository into the target application's folder. - Run
loader.pyusing Python.
By default, the loader attempts to launch main.py.
python loader.pyYou can specify a different target script or pass arguments to the game itself.
Syntax:
python loader.py [target_script] [game_arguments...]Examples:
-
Launch a specific script:
python loader.py my_game.py
Note: When loading
my_game.py, the target module name for@Injectbecomes"my_game"instead of"main". -
Pass arguments to the game:
python loader.py main.py --debug --windowed
(The arguments
--debug --windowedare passed directly tomain.py)
The applications in the examples folder do not have mods installed by default upon cloning.
You can install them using the method described above, or simply run the initialization script to automatically install mods for all examples:
- Windows:
initialize.bat - Linux/Mac:
initialize.sh
unittest.mock is a module primarily used for testing purposes, temporarily replacing the behavior of functions or objects.
Universal Modloader, on the other hand, is a framework for dynamically modifying code at runtime, aimed at program modding and customization.
unittest.mock is used only for testing, while Universal Modloader is used to change the behavior of programs in real life.
For example:
- When you want to change the behavior of a function in a third-party Pypi package:
Usually, changing the behavior of a library requires overriding or wrapping the function, or directly rewriting the library, but Universal Modloader allows you to change the behavior externally. - When you want to distribute only the patch portion without the proprietary code:
You can distribute only the patch portion without including the proprietary code that is legally restricted from redistribution. - If you want to apply multiple patches non-destructively:
Simple overwriting can result in conflicts between multiple patches, but Universal Modloader can apply multiple changes non-destructively. - If you want to allow modders to freely change code without creating additional plugin APIs for your application:
Adding a plugin API to an application can require refactoring and involve extensive changes. Universal Modloader allows modders to freely change code, reducing the burden on developers. - If you want to change behavior for purposes other than testing:
If you want to change code behavior for purposes other than testing (plugins, cheats, mods), Universal Modloader is more suitable.
It's true that unittest.mock is very powerful when it comes to writing test cases. However, it is primarily intended for testing purposes.
If you want to change code behavior for purposes other than testing, unittest.mock has design limitations and can lack flexibility.
Also, unittest.mock is intended as a temporary replacement and is not suitable for permanent changes or complex modding.
Universal Modloader is a framework for dynamically modifying code at runtime, specialized for modding and customization.
You can, but it is not recommended.
Universal Modloader is currently in Alpha, prioritizing power and flexibility over stability and security.
Also, AST injection itself exploits the dynamic nature of Python, which means unexpected behavior and security risks exist.
Please consider the risks similar to those of using tools such as Cheat Engine.
Currently, only Python 3.12 has been tested.
Due to the nature of ASTs, the AST structure may change when the Python version changes, so operation with other versions is not guaranteed.
- Injection Points
-
HEAD(Start of function) -
TAIL(End of function) -
RETURN(Rewrite return value) -
INVOKE(Before/After specific function calls)
-
- Mod Metadata (Manifest): Support for
__manifest__dict ormanifest.jsonto define name, version, author, and description. - Mod Load Order / Priority: Ability to define the order in which mods are applied (e.g., using integer priority or "load_after" directive).
- Dependency Management: Define prerequisite mods and ensure they are loaded first.
- Library Management: Automatically install required PyPI packages defined by mods (e.g.,
requirements.txtorpyproject.tomlper mod). - Conflict Detection: Warn when multiple mods try to hijack the same function/variable in conflicting ways.
- Configuration API: A standard way for mods to save/load settings (JSON/TOML/INI) without users editing code directly.
- Lifecycle Hooks: Event hooks for
on_load,on_ready,on_shutdown, etc. - Hot Reloading: Reload mods without restarting the target application.
- Error Isolation: Prevent a single crashing mod from bringing down the entire application (Safe Mode).
- Version Compatibility: Check if a mod is compatible with the current version of the target application or loader.