Files
Questie-X/.agent/rules/lua-patterns.md
T

485 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
paths:
- "**/*.lua"
---
# Lua Patterns
> This file extends [common/patterns.md](../common/patterns.md) with Lua specific content.
## Module Pattern
The standard Lua module returns a table as its public API. All internal state and helpers are file-local. This is the foundation of all Lua architecture.
```lua
-- mymodule.lua
local M = {}
-- Private state — invisible outside this file
local _cache = {}
local _initialized = false
-- Private helper — underscore prefix signals internal use
local function _buildCacheKey(id)
return "key_" .. tostring(id)
end
-- Public API — the only things callers can access
function M.get(id)
return _cache[_buildCacheKey(id)]
end
function M.init()
if _initialized then return end
_initialized = true
-- one-time setup
end
return M
```
### Legacy Module Styles (5.0 and 5.1)
In Lua 5.0, `setfenv` was used directly to create module environments (there was no `module()` function). In Lua 5.1, the `module()` built-in was introduced. Both are deprecated/removed in 5.2+:
```lua
-- LEGACY 5.0 style — setfenv only (no module() function exists in 5.0)
local M = {}
setfenv(1, M) -- Sets the current function's environment to M
function get(id) -- Automatically scoped to M
return cache[id]
end
return M
-- LEGACY 5.1 style — module() function (introduced in 5.1, deprecated in 5.2)
module("mymodule") -- Creates a global table, sets the function env
function get(id) -- Automatically added to the module table
return cache[id]
end
-- MODERN style (5.0+) — return-table pattern works on ALL versions
local M = {}
function M.get(id) return cache[id] end
return M
```
**Rule**: Always use the return-table pattern. It works on Lua 5.05.4, does not depend on `setfenv` or `module()`, and does not pollute the global namespace.
## Loader / ImportModule Pattern
In large addon systems, a central loader avoids circular `require()` chains by acting as a module registry with lazy resolution:
```lua
-- QuestieLoader.lua
local Loader = {}
local _modules = {}
function Loader:CreateModule(name)
local mod = {}
_modules[name] = mod
return mod
end
function Loader:ImportModule(name)
return _modules[name]
end
-- Usage in a module file:
local QuestieDB = QuestieLoader:CreateModule("QuestieDB")
-- Usage as a consumer:
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
```
This pattern breaks circular dependencies because modules register themselves at parse time but only call `ImportModule` at runtime (inside function bodies).
## OOP / "Class" Pattern
Simulate classes and inheritance with metatables. This is the most common OOP approach in Lua.
### Basic Class
```lua
local Animal = {}
Animal.__index = Animal
function Animal:New(name, sound)
return setmetatable({
name = name,
sound = sound,
}, self) -- 'self' is Animal here (or a subclass)
end
function Animal:Speak()
return self.name .. " says " .. self.sound
end
```
### Inheritance
Chain `__index` through the class hierarchy. Call parent constructors with dot-notation (not colon) to avoid double-wrapping `self`:
```lua
-- Dog extends Animal
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog
function Dog:New(name)
-- CORRECT: dot-notation passes 'self' (Dog) explicitly
local instance = Animal.New(self, name, "Woof")
return setmetatable(instance, Dog)
end
function Dog:Fetch(item)
return self.name .. " fetches the " .. item
end
-- BAD: colon-notation would pass Dog as 'self' twice
-- local instance = Animal:New(name, "Woof") -- WRONG
```
### Mixins
Add behavior from multiple sources without full multiple inheritance:
```lua
local Serializable = {}
function Serializable:Serialize()
local parts = {}
for k, v in pairs(self) do
table.insert(parts, tostring(k) .. "=" .. tostring(v))
end
return table.concat(parts, ";")
end
-- Mixin application
local function applyMixin(class, mixin)
for k, v in pairs(mixin) do
if class[k] == nil then -- Don't overwrite existing methods
class[k] = v
end
end
end
applyMixin(Dog, Serializable)
-- Now Dog instances have :Serialize()
```
## Singleton Pattern
Wrap initialization in a one-time guard. Common for managers, registries, and services:
```lua
local ConfigManager = {}
local _config = nil
function ConfigManager:Get(key)
if not _config then
_config = self:_load()
end
return _config[key]
end
function ConfigManager:_load()
-- expensive one-time load
return { debug = false, maxLevel = 80 }
end
```
## Registry / Plugin Architecture
Use a central registry for runtime plugin discovery without hard coupling. This is the pattern used by `QuestiePluginAPI`:
```lua
local PluginRegistry = { _plugins = {} }
function PluginRegistry:Register(name, pluginData)
assert(type(name) == "string", "Plugin name must be a string")
assert(not self._plugins[name], "Plugin already registered: " .. name)
local plugin = {
name = name,
data = pluginData or {},
stats = { QUEST = 0, NPC = 0, OBJECT = 0, ITEM = 0 },
}
self._plugins[name] = plugin
return plugin
end
function PluginRegistry:Get(name)
return self._plugins[name]
end
function PluginRegistry:IsAnyLoaded()
return next(self._plugins) ~= nil
end
```
### Plugin-Side Registration
Plugins register at parse time (top of file, outside any event handler) so the core can discover them immediately:
```lua
-- In plugin Loader.lua (top level, not inside PLAYER_LOGIN)
local plugin = PluginRegistry:Register("WotLKDB", addonTable)
```
## Observer / Event Bus Pattern
Decouple producers from consumers. Essential for addon systems where modules load in unpredictable order:
```lua
local EventBus = { _listeners = {} }
function EventBus:On(event, fn)
self._listeners[event] = self._listeners[event] or {}
-- Use table.insert for 5.0 compat (no # operator)
table.insert(self._listeners[event], fn)
end
function EventBus:Off(event, fn)
local listeners = self._listeners[event]
if not listeners then return end
-- Reverse iterate; table.getn for 5.0, # for 5.1+
local n = table.getn and table.getn(listeners) or #listeners
for i = n, 1, -1 do
if listeners[i] == fn then
table.remove(listeners, i)
return
end
end
end
function EventBus:Emit(event, ...)
local listeners = self._listeners[event]
if not listeners then return end
-- ipairs works on all versions (5.0+)
for _, fn in ipairs(listeners) do
fn(...) -- 5.1+: ... is an expression; 5.0: use unpack(arg) instead
end
end
```
## Coroutine-Based Lazy Iterator
Turn expensive database scans into resumable, lazy iterations without building the full result set in memory:
```lua
local function filteredNPCs(npcData, predicate)
return coroutine.wrap(function()
for id, data in pairs(npcData) do
if data and predicate(id, data) then
coroutine.yield(id, data)
end
end
end)
end
-- Usage: processes NPCs one at a time, no intermediate table
for npcId, npcData in filteredNPCs(QuestieDB.npcData, function(id, d)
return d[2] and d[2] >= 70 -- level >= 70
end) do
processNPC(npcId, npcData)
end
```
**Version note**: `coroutine.wrap` is available in Lua 5.0+. However, in 5.0 you cannot yield from inside an iterator used in a generic `for` loop as part of a C boundary. The wrap-based pattern above works because the `for` loop drives `resume` directly.
## Chunked Processing (Frame-Budgeted Work)
For operations that must not freeze the game client, split work across frames:
```lua
local function createChunkedProcessor(items, processFunc, chunkSize)
chunkSize = chunkSize or 100
local keys = {}
-- Use table.insert for 5.0 compat
for k in pairs(items) do table.insert(keys, k) end
local index = 1
local total = table.getn and table.getn(keys) or #keys
return function() -- Call this each frame/tick
local budget = math.min(index + chunkSize - 1, total)
for i = index, budget do
processFunc(keys[i], items[keys[i]])
end
index = budget + 1
return index > total -- Returns true when done
end
end
-- Usage with WoW C_Timer
local processor = createChunkedProcessor(rawData, compileRecord, 200)
local ticker
ticker = C_Timer.NewTicker(0.01, function()
if processor() then
ticker:Cancel()
print("Compilation complete!")
end
end)
```
## Memoization / Caching
Cache expensive computations. Support cache invalidation:
```lua
local _zoneCache = {}
local _zoneCacheDirty = false
function ZoneDB:GetZoneAreaId(uiMapId)
if not _zoneCacheDirty and _zoneCache[uiMapId] ~= nil then
return _zoneCache[uiMapId]
end
local result = self:_computeZoneAreaId(uiMapId)
_zoneCache[uiMapId] = result
return result
end
function ZoneDB:InvalidateCache()
wipe(_zoneCache)
_zoneCacheDirty = false
end
```
**Caution**: Use `~= nil` for the cache check, not truthiness. Cached `false` or `0` values are valid and must not trigger recomputation.
## AddonTable Pattern (WoW-Specific)
The WoW client passes a private shared table to every file listed in the addon's `.toc`:
```lua
-- File 1: data.lua
local addonName, addonTable = ...
addonTable.npcData = { [1] = { "Ragnaros", 63, 1 } }
-- File 2: init.lua — same addonTable reference
local addonName, addonTable = ...
local npc = addonTable.npcData[1]
print(npc[1]) -- "Ragnaros"
```
**Critical rule**: Use `addonTable` as the **sole** inter-file communication channel. Never use `_G` for data sharing between files — it pollutes the namespace, causes taint, and is visible to every addon in the client.
**Lua 5.0 note**: The vararg `...` syntax to capture `addonName, addonTable` works differently in 5.0. In 5.0, you must use the implicit `arg` table:
```lua
-- Lua 5.1+:
local addonName, addonTable = ...
-- Lua 5.0:
local addonName = arg[1]
local addonTable = arg[2]
-- Universal (works on 5.05.4):
local addonName = select and select(1, ...) or (arg and arg[1])
local addonTable = select and select(2, ...) or (arg and arg[2])
```
## Proxy / Facade Pattern
Wrap a complex subsystem behind a simplified interface:
```lua
local QuestieAPI = {}
function QuestieAPI:GetQuestName(questId)
local QuestieDB = QuestieLoader:ImportModule("QuestieDB")
local data = QuestieDB and QuestieDB:GetQuest(questId)
return data and data.name or "Unknown Quest"
end
```
## Functional Patterns
Lua supports higher-order functions. Use them for data transformations:
```lua
-- map: Transform each element (uses ipairs for 5.0 compat)
local function map(t, fn)
local result = {}
for i, v in ipairs(t) do result[i] = fn(v, i) end
return result
end
-- filter: Keep elements matching predicate
local function filter(t, predicate)
local result = {}
for i, v in ipairs(t) do
if predicate(v, i) then table.insert(result, v) end
end
return result
end
-- reduce: Fold elements into a single value
local function reduce(t, fn, initial)
local acc = initial
for i, v in ipairs(t) do acc = fn(acc, v, i) end
return acc
end
-- Compose: Right-to-left function composition
-- Note: Uses arg.n in 5.0 since select() doesn't exist
local function compose(...)
local fns = select and { ... } or arg
local n = select and select("#", ...) or fns.n
return function(x)
for i = n, 1, -1 do x = fns[i](x) end
return x
end
end
```
## Enumerations
Lua has no native enums. Simulate with constant tables. Optionally freeze with `readOnly()` (see coding-style.md):
```lua
local DebugLevel = {
CRITICAL = 1,
INFO = 2,
DEVELOP = 3,
}
local QuestFlags = {
SHARABLE = 0x0008,
DAILY = 0x1000,
WEEKLY = 0x8000,
AUTO_ACCEPT = 0x80000,
}
```
## Weak Tables
Use weak references for caches that should not prevent garbage collection:
```lua
-- Values are weak: GC can collect them when no other references exist
local textureCache = setmetatable({}, { __mode = "v" })
function getTexture(path)
local cached = textureCache[path]
if cached then return cached end
local tex = loadTexture(path)
textureCache[path] = tex
return tex
end
```
| Mode | Meaning |
|------|---------|
| `__mode = "v"` | Weak values — GC collects values with no other refs |
| `__mode = "k"` | Weak keys — GC collects keys with no other refs |
| `__mode = "kv"` | Both weak — GC collects either direction |
**Ephemeron tables (5.2+)**: In Lua 5.2, weak tables with weak keys behave as **ephemeron tables**. In an ephemeron table, a value is considered reachable only if its key is reachable. If the only reference to a key comes through its value (e.g., a table used as both key and value), the entry is removed. This prevents reference cycles from keeping entries alive and is the correct behavior for cache patterns. In 5.0/5.1, a strong value could keep a weak-keyed entry alive even if the key was unreachable.
**Mode reference**:
| Mode | When entry is removed |
|------|----------------------|
| `"v"` | Value is garbage-collectable and no other references exist |
| `"k"` | Key is garbage-collectable and no other references exist |
| `"kv"` | Either key or value is independently collectable |