Official Modding Guide
RetroEngine Modding Guide
Section titled “RetroEngine Modding Guide”A mod is a folder (or a packaged .rmod file) with a mod.json manifest. A mod can:
- override vanilla assets and shaders,
- patch vanilla scenes without editing them,
- run Java code, compiled in memory by Janino, that registers new ECS components and listens to engine events.
Start from StarterPack/example_mod/, or create a new mod from the editor (see Creation Kit mode).
Mods/example_mod/ mod.json manifest Scripts/ModScript.java entry point (extends com.retro.engine.modding.ModScript) Scripts/WobbleScript.java EntityScript used by the patch Patches/example.rpatch scene delta for Assets/Scenes/Main.rscene (hand-written) Patches/*.editor.rpatch scene deltas saved by Creation Kit mode Assets/ VFS overlay: files here replace vanilla Assets/ files at the same path Shaders/vulkan|metal/ shader overridesWhere mods load
Section titled “Where mods load”| Host | Mods folder | When |
|---|---|---|
| Exported game | Game_Data/Mods/ (the exporter creates it, with a README) |
Always (start with --no-mods to disable) |
| Editor, Creation Kit mode | <Project>/Mods/, only the authored mod and its dependencies |
File > Open Mod Workspace… |
| Editor | <Project>/Mods/ |
Only when launched with --mods |
- What
Mods/can contain: mod folders and.rmodpackages. Packages are extracted once into~/.retroengine/cache/and re-extracted when the file changes. - Dev mods aren’t shipped: a project’s own
Mods/folder is not copied into exports. - Saving under
--mods: scene saving is disabled, because the open scene contains patched content and saving would bake it into the vanilla.rscene. Use Creation Kit mode to author scene changes instead.
mod.json
Section titled “mod.json”| Key | Default | Meaning |
|---|---|---|
mod_id (required) |
a-z 0-9 _ . -, unique. Also the mod://<mod_id>/ prefix. |
|
name, version, author |
Display only; version is compared by dependencies. |
|
target_engine |
Logged if it differs from the running engine. Not enforced. | |
load_priority |
0 |
Higher loads later and wins conflicts. Aliases: priority, loadOrder. |
dependencies |
[] |
"other_mod" or {"mod_id": "...", "version": "1.2.0", "optional": true}. |
entry_point |
Fully qualified ModScript subclass. |
|
script_sources |
[] |
Java files compiled in this order. A file can use classes from files before it. |
asset_overlays |
["Assets/"] |
VFS overlay roots inside the mod folder. |
patches |
["Patches/"] |
.rpatch files, or folders scanned recursively for them. |
shaders |
["Shaders/"] |
Shader override roots, mirroring the engine’s shaders/ folder. |
// line comments are allowed in mod.json and .rpatch files.
Load order and the VFS
Section titled “Load order and the VFS”-
Discover: every
Mods/*/mod.jsonis parsed. A malformed manifest skips only that mod. -
Resolve order: dependencies always load first. Among mods that are ready to load, lower
load_prioritygoes first, with ties broken bymod_id. A missing or too-old required dependency, or a dependency cycle, disables the affected mods and everything that requires them. -
Compile: each mod’s
script_sourcescompile on one class-loader chain that runs through all mods in load order, so a mod can use classes from its dependencies. -
Mount: overlays stack in load order, with the last mod on top:
Mods/super_weapons/Assets/ load_priority 200 checked firstMods/hd_textures/Assets/ load_priority 100Assets/ vanilla fallbackWhen a scene or prefab references
Assets/Textures/Wall.png, the top-most mod that ships<overlay>/Textures/Wall.pngwins. Vanilla scenes pick up the override with no edits. Usemod://<mod_id>/<path>for new files that only your mod uses. -
Initialize: each
entry_pointrunsonInitialize(ModContext)in load order. The default implementation callsonModInit(). Once every mod is up, each runsonModEnable(), thenModsLoadedEventfires. On unload, mods runonModDisable()and thenonShutdown(), in reverse order. If a mod throws during initialization, it is unloaded (its handlers and components are removed) along with its dependents. -
Shaders: overrides are mounted and the pipelines are rebuilt (see Shaders).
-
Patch:
.rpatchfiles are applied each time their target scene loads, in mod load order. Within one mod they run in file-path order, with*.editor.rpatchfiles last.
Mod assets stream in and are evicted exactly like vanilla ones. An asset that no live entity has referenced for 600 frames (about 10 s at 60 FPS) is unloaded from VRAM.
Scene delta patches (.rpatch)
Section titled “Scene delta patches (.rpatch)”{ "format": "rpatch", "version": 1, "target_scene": "Assets/Scenes/Main.rscene", "operations": [ { "op": "ADD_ENTITY", "id": "my_mod.chest", "name": "Chest", "parent": "entity_1", "components": [ { "type": "Transform", "properties": { "posX": 2 } }, { "type": "MeshFilter", "properties": { "meshAssetPath": "mod://my_mod/Meshes/Chest.obj" } } ] }, { "op": "MODIFY_COMPONENT", "entity": "light_1", "component": "Light", "properties": { "intensity": 2.0 } }, { "op": "ATTACH_COMPONENT", "entity_name": "Cube", "component": "WobbleComponent", "properties": { "speed": 3 } }, { "op": "DETACH_COMPONENT", "entity": "entity_2", "component": "ScriptComponent", "index": 0 }, { "op": "MODIFY_ENTITY", "entity": "my_mod.chest", "name": "Big Chest", "parent": null }, { "op": "REMOVE_ENTITY", "entity": "entity_3" } ]}- Targeting:
"entity"matches an entity id;"entity_name"matches the first entity with that name.REMOVE_ENTITYalso removes the entity’s descendants."index"selects among several components of the same type and defaults to 0. - Components: you can name built-ins (
Transform,Light,MeshFilter,Material,ScriptComponent, …), their…Componentaliases, or mod components you registered. - Properties: keys are
@InspectorPropertyfield names or Inspector labels, matched case-insensitively. Values can be numbers, booleans, strings, enum names ("point"), or objects for string maps. Asset fields resolve through the VFS. - New entities: every new entity starts with a
Transform. ListingTransformincomponentsedits it rather than adding a second one. Prefix new ids with yourmod_idso they never collide with vanilla ids or other mods. - Errors: a bad operation (unknown entity, component or property, or a type mismatch) is logged and skipped, and the rest of the patch still applies. Patches from several mods stack. Each one sees the result of the patches that ran before it.
Custom components
Section titled “Custom components”public static class WobbleComponent extends Component { @InspectorProperty(order = 0, label = "AMPLITUDE", min = 0f, max = 5f) public float amplitude = 0.25f; public String getComponentName() { return "Wobble"; }}
public void onInitialize(ModContext context) { context.registerComponent(WobbleComponent.class); // now usable as "WobbleComponent" / "Wobble"}- A component needs a public no-arg constructor and a unique
orderfor each@InspectorProperty. - Supported field types are
float,int,boolean, enums,String(asset paths usetype = PropertyType.ASSET_PICKER), andMap<String,String>. - Registering a name that is already taken is rejected, so pick names that won’t collide.
- Components only hold data. Per-frame logic goes in an
EntityScriptattached through aScriptComponent. EntityScripts compile against the mod class-loader chain, so they can reference your component classes directly (seeWobbleScript.java).
Events
Section titled “Events”Register handlers inside onInitialize. Handlers run in mod load order on the main thread. A handler that throws
is logged and skipped.
context.registerEvents(this); // public @SubscribeEvent methods
@SubscribeEventpublic void onSpawn(EntitySpawnEvent e) { if ("Boss".equals(e.entity.name)) e.setCanceled(true); }
context.eventBus().subscribe(ModsLoadedEvent.class, new ModEventListener() { public void onEvent(Object event) { ModsLoadedEvent e = (ModsLoadedEvent) event; }});| Event | Fired | What you can do |
|---|---|---|
ModsLoadedEvent |
Once, after every mod has initialized | Read loadOrder |
SceneLoadEvent |
After a scene is deserialized and patched, before its entities join the world | Edit the mutable entities list |
EntitySpawnEvent |
Once per entity on scene load, prefab spawn and SceneWorld.spawnEntity (source says which) |
Cancel to drop the entity and its descendants, or add components |
CollisionEvent |
Every solid contact in a 60 Hz physics step, before the bodies are separated | Change restitution, or cancel so the bodies pass through |
TriggerEvent |
Each trigger pair per step, with phase ENTER, STAY or EXIT |
React to trigger / other |
RenderPassEvent |
Once per frame, after draws and lights are collected, before sorting | drawMesh(...) extra meshes, add lights, tweak this frame’s environment (fog, ambient) |
- Your own events: any object can be posted with
eventBus().post(obj), which lets mods talk to each other.@SubscribeEventmethods receive any event class. OnlyModEventsubclasses can be canceled. - Reused events: the engine reuses event instances, so don’t keep references to them after your handler returns.
- Per-frame events:
CollisionEvent,TriggerEventandRenderPassEventrun every frame or every physics step. Keep their handlers allocation-free. - Authoring: in Creation Kit mode,
SceneLoadEventandEntitySpawnEventdon’t fire, so the scene you edit is pure patch content.
Shaders
Section titled “Shaders”A file in a mod’s Shaders/ folder replaces the engine shader with the same name. Higher-priority mods win per
file.
| Backend | File | Build |
|---|---|---|
| Vulkan | Shaders/vulkan/<name>.vert / .frag |
Compiled to SPIR-V with glslc on a background thread. Results are cached in ~/.retroengine/cache/. Alternatively ship <name>.frag.spv. |
| Metal | Shaders/metal/<name>.metal |
Compiled by the Metal driver when the pipelines rebuild |
- Applying: after the mods load, the renderer rebuilds its pipelines at the next frame boundary, the same path the editor uses for shader hot reload. Unloading the mods restores the built-in shaders.
- Failure: a shader that fails to compile, or that the GPU rejects, is logged, and the engine falls back to its built-in version.
- Compatibility: start from a copy of the engine’s shader and keep its vertex inputs, uniform and push-constant blocks, and entry points.
- Post-processing: there is no separate post-processing pass. Full-screen effects are done by overriding the shaders the scene is drawn with.
Creation Kit mode
Section titled “Creation Kit mode”The editor doubles as the modding tool:
- Choose Open Mod Workspace… from the project launcher or the FILE menu.
- Pick a mod folder inside
<Project>/Mods/. To make a new mod, pick an empty folder there (orMods/itself). The editor writes a mod skeleton withmod.json,Assets/,Patches/,Scripts/andShaders/. - The project opens with only that mod and its dependencies loaded. The title bar shows
[MOD <id>]. - Edit the vanilla scene normally, then save it. The scene file is not overwritten. Instead the editor writes the
difference to
Patches/<Scene>.editor.rpatch. That delta is computed against vanilla plus every other patch, including your hand-written ones, so it holds only your edits. Saving an unchanged scene deletes the file. - Scenes saved inside the mod folder are ordinary mod-owned
.rscenefiles. Saving to any other vanilla path is refused. - Choose File > Close Mod Workspace to go back to vanilla.
Limitations:
- Only scenes are protected. Assets you import through the asset browser still land in the vanilla
Assets/folder. Put mod assets in the mod’sAssets/overlay or another mod subfolder instead. - New ids are written as you created them. Prefix them with your
mod_idso they can’t clash with other mods.
Packaging (.rmod)
Section titled “Packaging (.rmod)”Use File > Package .rmod…, or PACKAGE .RMOD… in the Export Standalone Build window. A folder picker
opens straight away (starting in the mod you are authoring, else <Project>/Mods/). Pick the mod folder and it is
packaged in the background in these steps; a dialog reports the finished file or the first error:
- Validates
mod.jsonand requires that the mod’s dependencies are installed next to it. - Compiles
script_sourcesagainst the dependencies’ classes and checks that the entry point extendsModScript. Every other.javafile (your EntityScripts) must compile too. - Parses every
.rpatchfile. - Cooks each mesh and texture into a
.rpacksidecar for fast loading. The raw files are kept. - Writes a canonical
mod.jsonwith comments stripped. - Zips everything into
<Project>/Build/Mods/<mod_id>-<version>.rmod, leaving out dot-files,.tmpand.bak.
Players drop the .rmod into the game’s Game_Data/Mods/.
API reference
Section titled “API reference”| Class | Methods |
|---|---|
ModScript |
onInitialize(ModContext), onModInit(), onModEnable(), onModDisable(), onShutdown(), context(), logInfo/Warn/Error |
ModContext |
registerComponent(Class), registerEvents(Object), eventBus(), vfs(), modFile(path), modUri(path), manifest() |
ModEventBus |
register(Object), subscribe(Class, ModEventListener), unregister(Object), post(Object) |
ComponentSchema |
registerModComponent(Class, modId) |
AssetDatabase |
mountModVFS(String modFolder) mounts an extra mod folder and reloads the mod set. resolveAsset(String path) returns the final file for a path through mod://, the overlays, then vanilla. |
SceneWorld |
spawnEntity(id, name, meshPath), findEntityByName(name), findEntityById(id), raycastExcluding(entity, origin..., dir..., maxDistance) |
EntityScript |
getTransform(), getComponent(Class), raycast(...), loadScene(path), lifecycle and collision/trigger callbacks |
Janino limitations
Section titled “Janino limitations”Mod and script sources are compiled by Janino, not javac:
- Lambdas and method references are not supported. Use
@SubscribeEventmethods or anonymous classes. - Generic type arguments are ignored, so cast generic results:
(WobbleComponent) getComponent(WobbleComponent.class). - Keep a mod’s
script_sourcessmall and list files in dependency order.