-
-
Notifications
You must be signed in to change notification settings - Fork 3
Writing Your First Mod
A SacredSDK mod is a Lua file that returns a list of FunkCode records. The bake
runs it once at game startup and writes the result to a .bin the game reads.
This page walks you from the smallest possible mod to modding vanilla, then to
running real Lua callbacks at game-time.
Verified on Sacred Gold Steam, build 2.0.2.28 (2006-10-13).
New to the SDK? Quick Start builds three working runtime mods step by step: a message, a hero class with an NPC's body, a companion with an ambush. It is also published as a Steam guide.
-- save as: custom/lua/bin/TYPE_NPC_GLADIATOR/QuestCode.lua
local q = require "quest"
return q.script {
q.set_hero_qbit(1101),
q.set_hero_qbit(31),
q.var "DaemonTotFranz",
}Launch Sacred. It loads the mod, generates
custom/bin/TYPE_NPC_GLADIATOR/QuestCode.bin from your Lua, and uses it instead
of the vanilla file. The vanilla bin/ tree is never modified.
A mod's path mirrors the game's bin/ tree: a file under
custom/lua/bin/<rel>/<name>.lua bakes to custom/bin/<rel>/<name>.bin and
overrides bin/<rel>/<name>.bin. The per-class folders are TYPE_NPC_*
(TYPE_NPC_GLADIATOR, TYPE_NPC_SERAPHIM, ...); inside each, QuestCode,
FunkCode, StartCode, and QuestPoolCode are the script files.
What a mod returns replaces the whole file at its path. A runtime mod, one that
only registers callbacks and ends in return {}, bakes to an empty file: put it
in custom/lua/mods/<name>.lua, a folder that mirrors no game file. Saved as
custom/lua/bin/TYPE_NPC_GLADIATOR/FunkCode.lua, it would hand the game an empty
FunkCode.bin for that class.
There are three layers, top to bottom. Use the highest one that covers your case and drop down only when it doesn't.
One Lua line per quest/dialog primitive.
local q = require "quest"
local d = require "dialog"
return q.script {
q.var "campaign_progress",
q.assign("campaign_progress", 1),
q.set_hero_qbit(1101),
d.trigger "HQ_3_2_1_DLG_START",
d.line ("res:1037", "btn_ok"),
d.emit (31, "9511"),
q.log_entry(9511,
"HQ_3_2_1_Log_Title",
"HQ_3_2_1_Log_Header",
"HQ_3_2_1_Log_Qstart"),
}The full set of quest/dialog/text/state builders is documented on
Quests and Dialog Authoring.
When no high-level helper exists yet, build records by hand.
local raw = require "raw"
local fc = require "funkcode"
return {
-- a ResRef record by hand
raw.rec(0x3c, 0x00, fc.dialog("res:1042", "trigger9999")),
-- a "ConditionalEval" record (encoding not fully understood yet)
raw.rec(0x3a, 0x00, {"STACK_96"}),
}Last resort for opcodes not yet modeled in the helpers — paste raw bytes.
local raw = require "raw"
return {
-- an opcode not yet named — paste the raw bytes
raw.rec(0x67, 0x00, raw.hex("0b 27 25 00 00")),
}raw.hex (and the _HEX pseudo-op underneath) copy bytes verbatim into the
payload. The round-trip stays byte-perfect.
Instead of authoring from scratch, you can load a vanilla script, mutate it, and
return the result. Nothing to prepare: v.load decompiles the game's own .bin
on the spot through sacred.disasm, and hands you the same records the baker
takes back. Measured on a whole shipped script — 125,236 records out of 3.97 MB
in 721 ms, and baking them back unchanged produced a byte-identical file.
-
Write a transformer mod in
custom/lua/bin/<rel>/<name>.lua:local v = require "vanilla" local recs = v.load "bin/TYPE_NPC_SERAPHIM/FunkCode" v.gsub_bytes(recs, "HQ_3_2_1_sera_", "HQ_3_1_4_glad_") return recs
-
Launch Sacred. The bake decompiles, transforms and re-bakes in a second or two; restart Sacred to apply edits.
| Function | Use |
|---|---|
v.load(rel) |
The records of a shipped script: the game's own .bin, decompiled live, or a snapshot under _vanilla/ if you made one |
v.gsub_strings(recs, pat, repl) |
Lua-gsub over every decoded string arg |
v.gsub_bytes(recs, pat, repl) |
Same, plus inside _HEX fallbacks (full coverage) |
v.for_each_op(recs, fn) |
Visit every op of every record |
v.for_each_string(recs, fn) |
Visit every string arg of every op |
v.records_with_tag(recs, tag) |
Filter records by tag byte |
v.label_set(recs) |
Counter map of opcode-labels in use |
v.have(rel) |
Whether a script is readable at all, so a mod can fall back |
The vanilla -> Lua -> vanilla round-trip is byte-perfect (verified on 132 of 132
vanilla .bin files), so v.load + return with no edits produces an
identical file.
Bake-time records emit native Sacred bytecode the engine runs on its own. For logic that needs to run your Lua at game-time, attach a callback to a trigger name. It fires when Sacred dispatches a matching trigger from an NPC dialog or quest state change.
-- React to the player meeting Leandra in the Seraphim main quest
sacred.on_trigger("HQ_3_2_1_sera_DLG_OFFEN", function(ctx)
sacred.log("Leandra greeted the hero!")
ctx:notify("Quest discovered!") -- gold banner, top-center
ctx:give_gold(100) -- live hero-struct write
end)
-- Multiple handlers per trigger stack; all run in registration order.
sacred.on_trigger("HQ_3_2_1_sera_DLG_START", function(ctx)
-- maybe layered logic from a different mod
end)
-- Drop all handlers (for clean re-registration on a re-bake).
sacred.clear_triggers()sacred.on_trigger registers the handler during bake; the persistent Lua state
survives the bake and the handler fires during play. Every handler receives a
ctx table — see the ctx reference
for its methods (ctx:gold(), ctx:give_gold(N), ctx:has_item(res),
ctx:notify(text), ctx:get_var/set_var, and the ctx.trigger_name field).
For multi-step quests with order and guards, prefer the declarative state
machine in lib/questfsm.lua (fsm.define{...}) — see
Quests and Dialog Authoring.
For spawning living NPCs, companions, and dialog at runtime, see
Runtime NPCs and the npcobj wrapper in the
Lua API Reference.
- Edit
custom/lua/bin/<rel>/<name>.luain any editor. - Save.
- Relaunch Sacred (or click Rebake all .lua in the SacredSDK overlay).
- The new
custom/bin/<rel>/<name>.binis generated and read by Sacred. - Observe in-game.
The bake runs once, on DLL attach. There is no hot reload yet — relaunch Sacred to pick up Lua edits.
Copy-paste starters live in sdk/custom/lua/examples/ (the framework tree).
Files under examples/ are never baked, so copying is the point. The runtime
ones that end in return {} (07, 08, 09, 14, 15) go to
custom/lua/mods/<name>.lua. The ones that return records replace the whole
game script at their path under custom/lua/bin/.
| File | Purpose |
|---|---|
01_hello.lua |
The smallest meaningful mod — just declares state |
02_text_swap.lua |
Bulk-rewrite vanilla text via gsub
|
03_dialog_block.lua |
Author one NPC dialog scene from scratch |
04_full_quest.lua |
A complete (small) quest from scratch |
05_conditional_dialog.lua |
Native Sacred branching skeleton |
06_sidequest.lua |
Full side-quest template, copy-paste ready |
07_runtime_triggers.lua |
sacred.on_trigger patterns |
08_questfsm.lua |
Declarative multi-step quest state machines |
09_state_vars.lua |
Read/write Sacred's named-state store at runtime |
10_register_quest.lua |
Register a brand-new quest_id with the engine |
11_classes.lua |
The 8 playable hero classes as constants |
12_npc_classes.lua |
Every creature/NPC class as constants (474) |
13_spawn_npc.lua |
Spawn NPCs via the engine CreateNPC bake path |
14_npc_runtime.lua |
Runtime NPC spawning & behavior |
15_storyline.lua |
The runtime storyline layer (quest-giver, items, teleport) |
The mod didn't apply in game.
- Check
sdk/logs/sdk_loaded.logfor[lua_bake] baked '...' -> '...'. - Check that
custom/bin/<rel>/<name>.binexists and has a plausible size. A runtime mod incustom/lua/mods/bakes to an emptycustom/mods/<name>.bin; that is expected. - If your mod transforms vanilla, make sure the pre-decompiled snapshot is at
custom/lua/_vanilla/<rel>.lua. - Restart Sacred — the bake runs once, on attach.
The log has no line for my file. Windows hides file extensions by default,
so a file saved from Notepad as hello.lua can be hello.lua.txt, which the
bake ignores. Turn on View > File name extensions in File Explorer, and save
from Notepad with Save as type: All files.
The game ignores the mouse. The SDK overlay has the input; press F11 (the overlay).
Game crashes after I save my mod. You probably emitted invalid bytecode;
Sacred's interpreter is not forgiving. Roll back, relaunch, and iterate in
smaller steps. If using raw.rec with unknown payloads, make sure the bytes
don't reference NPC/item/resource ids that don't exist.
I get a Lua syntax error. sdk/logs/sdk_loaded.log shows
[lua_bake] <file>: lua error: <msg> with the file path and line number.
Getting started
Authoring
- Quests and Dialog Authoring
- Native Quests
- Runtime NPCs
- Hero Classes
- Dialog Text
- Dialog Nodes (catalogue)
The engine's own verbs
Reference