v1.4.9: Validation fixes, Profiler fix, Quest link duplicate fix, Ascension fixes

This commit is contained in:
Xurkon
2026-03-26 06:47:54 -05:00
parent 284f0c3794
commit 1fc9727fee
194 changed files with 21008 additions and 743 deletions
+484
View File
@@ -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.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 |