-
Notifications
You must be signed in to change notification settings - Fork 5
Authoring Lua Callbacks
EmoTracker calls a fixed set of well-known function names when major events happen during a pack's lifetime. These callbacks let your script react to the runtime — set up state once the pack is fully loaded, persist data when a save file is being read, react to memory state changes, etc.
See Authoring Lua Scripts for the broader scripting overview, and Authoring Lua — init.lua for where these callbacks typically live.
You implement a callback by defining a Lua global function with the matching name anywhere in your scripts — typically in init.lua or in a file it loads via ScriptHost:LoadScript. The runtime looks up the function by name at the moment the event fires (via mLua[functionName]), so you can replace or hot-add callbacks at any time and the next event will pick up the new version.
If a callback isn't defined, the runtime simply doesn't call anything. There's no error for "missing" callbacks, so you only implement the ones you actually need.
function tracker_on_pack_ready()
print("Pack is ready")
endThat's the entire contract — define a global with the right name and the runtime takes care of invoking it.
Implemented in EmoTracker.Data/ScriptManager.cs:InvokeStandardCallback. Most callbacks are no-arg, no-return functions. The exceptions are the location callbacks, which receive a section argument (see below). No callback's return value is used by the runtime.
| Function name | When the runtime calls it |
|---|---|
tracker_on_pack_ready |
Once, after the pack's items / locations / layouts have all finished loading and the runtime is ready to be used. The right place to do "set up everything that needs to happen exactly once". |
tracker_on_accessibility_updating |
Right before the runtime begins refreshing accessibility state across the location database. Useful for caching whatever you need to read during the refresh, or recording the previous state for diffing. |
tracker_on_accessibility_updated |
Right after the runtime finishes refreshing accessibility state. Useful for any cross-location bookkeeping that needs the latest accessibility values to be in place. |
tracker_on_location_updating(section) |
Right before any values on a location are changed. Useful for caching whatever you need to read during the update, or recording the previous state for diffing. Only triggers on AvailableChestCount and CapturedItem changes. Callback passes in the corresponding Location Section object: section.Owner.Name and section.Name can be referenced to retrieve the full location name. |
tracker_on_location_updated(section) |
Right after the location has changed. Useful for any additional actions needing to be performed following a change to a location. Only triggers on AvailableChestCount and CapturedItem changes. Callback passes in the corresponding Location Section object: section.Owner.Name and section.Name can be referenced to retrieve the full location name. |
tracker_on_begin_loading_save_file |
Just before the runtime starts loading state from a save file. Use it to clear any in-Lua caches that might collide with the loaded state. |
tracker_on_finish_loading_save_file |
Just after a save file finishes loading. Use it to recompute anything derived from item / location state. |
autotracker_started |
When the user has connected the autotracker. Not the place to register memory watches — those have to be registered at pack init for the autotracker icon to appear at all. Use this for per-connection setup like notifications, logging, or resetting session-specific Lua state. Optional. See Authoring Lua — Autotracking for the full lifecycle. |
autotracker_stopped |
When the autotracker disconnects. Use it for matching teardown of anything autotracker_started set up. Your registered memory watches are not removed — they stay registered and resume polling the next time a provider connects. |
The runtime guards against callbacks invoking themselves with a mbInPostLogicUpdate flag. If your callback (or anything it triggers) leads to another standard callback firing while you're still executing the first one, the nested call is silently dropped.
In practice this means:
- A callback can modify item/location state and trigger an accessibility refresh — but the resulting
tracker_on_accessibility_updating/_updatedcallbacks for that refresh are skipped while you're still inside the original callback. - If you want to react to "the state I just changed", do that work directly in the callback rather than expecting another callback to fire afterward.
- This guard exists per-callback-call, not per-callback-name, so the next time something legitimately triggers an accessibility refresh, the callbacks fire normally.
function tracker_on_pack_ready()
-- Compute one-time derived data and stash it for later use
DUNGEON_PRIZES = {
["ep"] = "ep_prize",
["dp"] = "dp_prize",
["th"] = "th_prize",
}
-- Force any layout / item state that should be initialised here
print("Pack initialised with " .. tostring(#DUNGEON_PRIZES) .. " dungeons")
endfunction tracker_on_accessibility_updated()
-- Maybe a script item that summarises overall progress
local total = count_cleared_dungeons()
local item = Tracker:FindObjectForCode("dungeon_count")
if item ~= nil then
item.AcquiredCount = total
end
endlocal STATE_KEY = "my_pack_custom_state"
local custom_state = {}
function tracker_on_begin_loading_save_file()
custom_state = {}
end
function tracker_on_finish_loading_save_file()
-- Custom save data is normally serialized via LuaItem's Save/Load
-- callbacks; this is the right place to refresh anything else
-- you derive from the just-loaded item state.
refresh_progress_summary()
endFor per-item save data, use the LuaItem SaveFunc / LoadFunc instead — they integrate with the standard save/load pipeline cleanly.
local previous_chest_count = {}
function tracker_on_location_updating(section)
-- Record the state before the change so we can diff it afterward
local key = section.Owner.Name .. "/" .. section.Name
previous_chest_count[key] = section.AvailableChestCount
end
function tracker_on_location_updated(section)
local key = section.Owner.Name .. "/" .. section.Name
local prev = previous_chest_count[key]
if prev ~= nil and section.AvailableChestCount ~= prev then
print(key .. " chest count changed from " .. tostring(prev)
.. " to " .. tostring(section.AvailableChestCount))
end
end-- Register watches at pack init time, NOT inside autotracker_started.
-- The autotracker icon won't even appear until at least one watch exists.
ScriptHost:AddMemoryWatch("Items", 0x7EF340, 0x100, items_callback)
ScriptHost:AddMemoryWatch("Dungeons", 0x7EF4C0, 0x040, dungeons_callback)
function items_callback(segment)
-- Read bytes via segment:ReadUInt8/16/etc. and update items
local bow = segment:ReadUInt8(0x7EF340)
-- ...
return true
end
-- Optional: do per-connection setup (logging, notifications, session resets)
function autotracker_started()
print("Autotracker connected")
end
function autotracker_stopped()
print("Autotracker disconnected")
-- Watches stay registered; nothing to clean up unless you have
-- session-specific Lua state of your own.
endSee Authoring Lua — Autotracking for the full memory-watch lifecycle.
-
Most callbacks are no-arg, but
tracker_on_location_updatingandtracker_on_location_updatedeach receive asectionargument. For all other callbacks, pull the data you need from the global objects (Tracker,LocationDatabase, etc.) directly. -
Define callbacks as Lua globals, not as table fields. The runtime looks them up via
mLua[functionName], which only resolves globals. -
Don't write callbacks with the same name as a built-in Lua function. Avoid
print,pairs,ipairs, etc. as callback names. -
Be careful with
tracker_on_accessibility_updating/_updated. They fire on every accessibility refresh, which can be many times per second during normal play. Keep them fast — don't do heavy work in them, and use the re-entrancy guard to your advantage instead of fighting it. -
autotracker_starteddoesn't fire until a device is selected and connected. It's not "the user opened the autotracker menu" — it's "memory reads are now actually happening". -
Don't put
AddMemoryWatchcalls insideautotracker_started. The autotracker icon depends on at least one watch existing — if you wait untilautotracker_startedfires to register watches, the icon never appears, the user can't connect, and the callback never gets called. Register watches ininit.lua(or scripts loaded from it). - Save/load callbacks fire around the whole save file, not per-item. They're for global state, not per-item persistence; for per-item state see Lua Items.
- Authoring Lua Scripts — top-level overview of the Lua sandbox
- Authoring Lua — init.lua — where you'll typically define these callbacks
- Authoring Lua — Autotracking — heavier guide to the autotracker_* callbacks and memory watches
-
Authoring Lua — Lua Items — per-item save/load callbacks via
LuaItem.SaveFunc/LoadFunc -
Authoring Lua — API Reference — full API for
Tracker,ScriptHost, and the other globals
- Installation
- Installing and Loading Packages
- Item Types and Mouse Controls
- Map Locations
- Map Location Colors
- Saving and Loading
- Multi-Tab and Window
- Autotracking
- NDI Broadcasting
- Twitch Chat HUD
- Note Taking
- Voice Control
- Keyboard Shortcuts