v1.4.9: Validation fixes, Profiler fix, Quest link duplicate fix, Ascension fixes
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
---
|
||||
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.0–5.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.0–5.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 |
|
||||
Reference in New Issue
Block a user