-
Notifications
You must be signed in to change notification settings - Fork 2
Creating Mods
This guide covers everything you need to know to create mods for Quest UE4 Modloader.
Every mod is a folder inside mods/ containing at least a main.lua file:
mods/
└── MyMod/
├── main.lua # Entry point (required)
└── *.pak # Optional PAK files (auto-mounted)
The modloader discovers and loads all mods/*/main.lua files on game start.
-
Discovery — Modloader scans
mods/for folders withmain.lua -
Loading — Each
main.luais executed in its own Lua environment -
Globals injected —
MOD_NAME,MOD_DIR,SharedAPI, and the full API - Hooks registered — Your hooks are installed and active
- Runtime — Hooks fire as the game runs, timers execute, bridge commands available
Every mod gets these globals automatically:
| Global | Type | Description |
|---|---|---|
MOD_NAME |
string | Folder name (e.g., "MyMod") |
MOD_DIR |
string | Full path to mod folder |
SharedAPI |
table | Cross-mod communication (shared across all mods) |
-- Hook a game function and modify behavior
RegisterPostHook("/Script/Game.SomeClass:SomeFunction", function(self, func, parms)
local obj = self:get()
if obj and obj:IsValid() then
obj:Set("Health", 100)
end
end)local TAG = "MyMod"
local state = { enabled = true }
-- Load saved config
local saved = ModConfig.Load(TAG)
if saved and saved.enabled ~= nil then
state.enabled = saved.enabled
end
-- Register in debug menu
if SharedAPI and SharedAPI.DebugMenu then
SharedAPI.DebugMenu.RegisterToggle(TAG, "My Feature", state.enabled,
function(new_state)
state.enabled = new_state
ModConfig.Save(TAG, state)
end
)
end
-- Conditional hook
RegisterPostHook("/Script/Game.SomeClass:SomeFunc", function(self, func, parms)
if not state.enabled then return end
-- Do something only when enabled
end)-- PreHook can return "BLOCK" to prevent the original from running
RegisterPreHook("/Script/Game.DamageSystem:ApplyDamage", function(self, func, parms)
return "BLOCK" -- Damage never applies
end)-- React when new objects are spawned
NotifyOnNewObject("/Script/Game.EnemyCharacter", function(obj)
if not obj or not obj:IsValid() then return end
pcall(function()
obj:Set("MaxHealth", 1)
Log("Modified enemy: " .. obj:GetName())
end)
end)-- One-shot after 5 seconds
ExecuteWithDelay(5000, function()
Log("5 seconds have passed!")
end)
-- Repeat every 2 seconds
LoopAsync(2000, function()
local player = FindFirstOf("PlayerController")
if player and player:IsValid() then
-- Periodic check/modification
end
end)
-- Next frame (game thread)
ExecuteInGameThread(function()
-- Safe to modify game objects here
end)-- Read a struct property
local actor = FindFirstOf("SomeActor")
local pos = actor:Get("Position") -- Returns LuaUStruct
-- Access fields
Log("X=" .. pos.X .. " Y=" .. pos.Y .. " Z=" .. pos.Z)
-- Modify fields (writes to live memory)
pos.X = 100
pos.Y = 200
pos.Z = 300
-- Or set from a table
actor:Set("Position", {X=100, Y=200, Z=300})
-- Or pass struct to Call()
actor:Call("SetActorLocation", {X=100, Y=200, Z=300})
-- Clone a struct (creates an independent copy)
local saved_pos = pos:Clone()Always wrap UObject operations in pcall(). Objects can become invalid at any time (garbage collected, level unloaded, etc.).
-- ❌ WRONG: crash if obj is invalid
local health = obj:Get("Health")
-- ✅ RIGHT: safe
local ok, health = pcall(function() return obj:Get("Health") end)
if ok then
Log("Health: " .. tostring(health))
endUse separate pcall blocks for independent operations:
-- ❌ WRONG: if first fails, second never runs
pcall(function()
obj:Set("Health", 100)
obj:Set("Armor", 50)
end)
-- ✅ RIGHT: independent operations in separate blocks
pcall(function() obj:Set("Health", 100) end)
pcall(function() obj:Set("Armor", 50) end)The DebugMenuAPI mod provides an in-game menu for toggles, actions, and sub-pages.
local api = SharedAPI and SharedAPI.DebugMenu
if api then
-- Toggle (on/off switch)
api.RegisterToggle("MyMod", "Feature Name", false, function(state)
Log("Feature: " .. tostring(state))
end)
-- Action (one-shot button)
api.RegisterAction("MyMod", "Do Something", function()
Log("Button pressed!")
end)
-- Selector (cycle through options)
api.RegisterSelector("MyMod", "Difficulty", {"Easy","Normal","Hard"}, function(value, index)
Log("Selected: " .. value)
end)
-- Sub-menu (opens a new page)
api.RegisterSubMenu("MyMod", "Advanced", function()
api.NavigateTo({
name = "Advanced Settings",
populate = function()
api.AddItem("Option 1", function() Log("1!") end)
api.AddItem("Option 2", function() Log("2!") end)
end
})
end)
endSee Debug Menu API for full documentation.
Use SharedAPI to expose functionality to other mods:
-- In your mod:
SharedAPI.MyMod = {
GetHealth = function() return current_health end,
SetGodMode = function(on) god_mode = on end,
}
-- In another mod:
if SharedAPI.MyMod then
SharedAPI.MyMod.SetGodMode(true)
endOr use shared variables:
SetSharedVariable("MyMod_Enabled", true)
local val = GetSharedVariable("MyMod_Enabled")Register custom commands accessible from the ADB bridge:
RegisterBridgeCommand("mymod_status", function()
return {
enabled = state.enabled,
health = current_health,
version = "1.0"
}
end)Test from your PC:
python tools/deploy.py console
> mymod_status-- Read a file
local content = ReadTextFile(MOD_DIR .. "config.txt")
-- Write a file
WriteTextFile(MOD_DIR .. "output.txt", "Hello!")
-- Check existence
if FileExists(MOD_DIR .. "data.json") then
-- ...
endFor structured config, prefer ModConfig:
-- Save
ModConfig.Save("MyMod", { enabled = true, level = 5 })
-- Load
local cfg = ModConfig.Load("MyMod")
if cfg then
Log("Level: " .. tostring(cfg.level))
end-
Always use reflection (
Get/Set/Call) — never raw memory offsets for UObject properties - Wrap everything in pcall — objects can be GC'd at any time
- Separate pcall blocks — don't combine independent operations
-
TArray is 1-indexed —
arr[1]is the first element -
Test via bridge first — validate API calls with
exec_luabefore coding -
Log generously — use
Log()with your mod's tag for debugging - Save state with ModConfig — persist settings across game restarts
- Register in debug menu — let users toggle your mod in-game
- Check IsValid() — before using any UObject reference
- Use ExecuteWithDelay — game objects aren't ready at load time