Skip to content

Architecture

Chronic Tinkerer edited this page May 6, 2026 · 1 revision

Architecture

LibCodex-1.0 is a static-catalog library with a hybrid load model. This page covers the design decisions that shape what's possible at the API layer.

Hybrid load model

Every catalog assembles itself from three sources, in priority order:

  1. Bundled seed. Each module ships a pre-baked Data/<Module>.lua snapshot generated by the bake tool from offline DBC dumps and Wowhead scrapes. The seed is the floor: every consumer gets it for free.
  2. SavedVariables. LibCodexDB is restored at PLAYER_LOGIN. It carries everything the user has discovered across previous sessions — useful for fields the bundle doesn't know (level-scaling labels, flavor-specific name overrides, NPC titles seen in chat, etc.).
  3. Runtime adapters. Functions registered via :RegisterAdapter(name, fn) run once at PLAYER_LOGIN and may also hook events for continuous capture. Adapters push entries into modules via Add.

All three sources merge into the same per-module collection. Provenance is tagged on each entry's sources array ("bundled", "savedvars", custom adapter names).

Per-module LoadOnDemand

LibCodex ships as a small core (LibCodex-1.0) plus 73 sibling LoadOnDemand sub-addons, one per data module. The naming convention is LibCodex-1.0-<ModuleName> (case-preserved). Examples:

Module LoD sub-addon
NPCs LibCodex-1.0-NPCs
Quests LibCodex-1.0-Quests
ChatChannels LibCodex-1.0-ChatChannels

The core only carries the factory, the registration plumbing, and the typed accessors. Per-module bundle data lives in the LoD addon's Data/<Module>.lua and only loads when something actually queries that module.

Auto-load on first miss. When a consumer calls :Get(id) on a module that has no data, the collection's :Get calls LibCodex:_TryLoadModule(self._name) exactly once. If the LoD sub-addon is installed, it loads, ingests its bundled data via _FeedBundledRowsLazy, and the lookup retries. If the LoD sub-addon is missing or disabled, the lookup falls through to whatever the user's SavedVariables and runtime adapters provided.

Why per-module split. Three reasons:

  • Memory. Most addons only touch a handful of modules. An NPC-tooltip addon has no reason to pay for the Spells module's hundreds of thousands of rows.
  • Bytecode constant pools. Lua 5.4 caps function-prototype constant pools. A monolithic library with every module's data inlined hits that ceiling. Per-module addons keep each Proto small.
  • Independent updates. Re-baking one module doesn't force a re-download of the whole library on CurseForge.

Lazy chunks

Bundled data files use LibCodex:_FeedBundledRowsLazy(module, columnsCSV, thunk) rather than emitting the table inline. The thunk is a zero-arg function whose body returns the rows table. Lua only constructs the table when the thunk is called.

This matters because the dominant memory cost of bundled data isn't the string interning (Lua interns string constants at parse time regardless), it's the per-table headers and hash-part allocations for each row. Wrapping in a thunk defers that cost until the consumer actually queries the module.

:Get(id) materializes only the requested row. :Search(query, opts) walks the lazy index without materializing non-matching rows. :ExpandAll() is the explicit override when a consumer needs every row in dict form.

Ingest formats

Three formats land bundled data into a collection:

  • _FeedBundled(module, entries) — table of fully-formed entry dicts. Used for small enums.
  • _FeedBundledRows(module, columnsCSV, rows) — positional row arrays plus a column header. Cheaper than dict form.
  • _FeedBundledRowsLazy(module, columnsCSV, thunk) — same as _FeedBundledRows but the rows table is wrapped in a thunk for lazy materialization. The default for catalogs.
  • _FeedBundledTSV(module, columnsCSV, blob) — legacy tab-separated string blob with byte-offset indexing. Back-compat path; new modules use the row format.

All four are no-ops if the receiving module hasn't registered yet — the data is stashed in pendingHydrate / pendingRows / pendingLazyRows / pendingTSV and drained on :RegisterModule(name, collection).

Adapters

Adapters are functions of the shape function(LC) ... end. Registered via LibCodex:RegisterAdapter(name, fn) and called once at PLAYER_LOGIN by :RunAdapters(). The library wraps each call in pcall so a misbehaving adapter doesn't break the load chain.

Adapters can also register their own event hooks for continuous data capture. The library's only job is the initial fan-out; ongoing observation is each adapter's responsibility.

The runtime adapter that ships with the library lives at Adapters/Runtime.lua and feeds basic player-context data (realm, faction, race, class) at login. Consumer addons can register additional adapters from their own code.

SavedVariables persistence

Schema:

LibCodexDB = {
  version = 1,
  modules = {
    NPCs   = { [id] = entry, ... },
    Items  = { [id] = entry, ... },
    Quests = { [id] = entry, ... },
    ...
  },
}

The library hooks PLAYER_LOGIN to restore and PLAYER_LOGOUT to persist. Persistence iterates every registered module and writes its :AllRaw() map. Modules that haven't been queried this session and are still in lazy-chunk form get materialized at logout — this is intentional, so the SV file always reflects the full known catalog.

Source provenance and locked fields

Every entry has a sources array tagging where each value came from. The merge rules in mergeEntry (in Modules/Common.lua) prevent runtime data from clobbering hand-curated bundled data:

  • _handcrafted = true — every field on the entry is locked. Incoming data merges only into fields the existing entry doesn't have.
  • _locked = { "field1", "field2" } — only the listed fields are locked.
  • _overwrite = { fieldName = true } — incoming entry forces an override on a specific field. Ignored for fields covered by _handcrafted / _locked.

This is what lets the bake tool re-emit catalogs without stomping on manual corrections committed in Data/<Module>.lua.

Version handling

The library uses LibStub for major/minor versioning. LIB_MAJOR is the literal string "LibCodex-1.0". LIB_MINOR is a sequential build number that increments by 1 on every release pass. LibStub keeps whichever copy registered with the highest minor — so an addon embedding an older copy automatically defers to a more recently-released one loaded by another consumer.

Slash command

The standalone install registers /codex for diagnostics. The legacy in-game GUI dashboard moved to a separate consumer addon (Forge_Codex); /codex gui redirects to it. The slash command primarily exposes the log window now.

Clone this wiki locally