Cod3xDocs
guide

Design Genealogy: ProtectedTable

How composing storage-backed settings, transient state, and methods became a reusable manager interface.

The composition

OpenMW managers kept accumulating three neighboring namespaces: storage-backed settings, transient runtime fields, and methods. ProtectedTable makes them one thing.

Each source keeps its own ownership and lifetime:

Manager memberActual sourceRole
manager.EnabledOpenMW storage sectionSettings that can be read, cached, synchronized, and sometimes written.
manager.state.lastUpdateScript-owned Lua tableTransient runtime state.
manager.resetMethods table attached to the managerBehavior that operates on the composed view.

The important part is that the state namespace is transparent when reading. The setup writes through .state, while consumers can read the value from the manager itself:

local I = require 'openmw.interfaces'

local manager = I.S3ProtectedTable.new {
    inputGroupName = 'SettingsGlobalMyMod',
    managerName = 'MyMod',
}

manager.state.lastUpdate = 0

function manager.reset()
    manager.state.lastUpdate = 0
end

local enabled = manager.Enabled
local lastUpdate = manager.lastUpdate
if enabled and lastUpdate == 0 then
    manager.reset()
end

manager.Enabled comes from the OpenMW storage section. manager.lastUpdate falls through to the script-owned state table. manager.reset comes from the manager's method table. The three things look like one object without pretending they have the same persistence or mutation rules.

The ProtectedTable API documents the current contract. The history explains why this composition was worth keeping.

The origin story

ProtectedTable did not begin as a general manager framework. It first had to answer small, concrete questions about what callers were allowed to put in the table and how failures should be explained:

  • 7a2580ca added checks for inserting new values;
  • 94f60f76 improved diagnostics and made failed inputs produce an expected value in tostring output;
  • e6b7f1a2 made settings transparently indexable through ProtectedTable.

That third change is the real origin story. actor.lua, fatigueManager.lua, and hitChanceManager.lua no longer had to keep duplicate settings, or synchronize their copies with the storage section. A consumer could ask the manager for the setting and let the boundary own the lookup and cached value.

Then 4d78a2d5 hardened the metatable so its protection could not be replaced accidentally. Only after that sequence did the shape become a broadly reusable boundary, promoted as an installed interface in 5cbc5ed6.

The path was not “specific settings guardrail, then framework.” It was composition first: remove synchronization plumbing, make the composed view safe, then give the proven boundary a stable name.

Protection and synchronization came afterward

Composition is useful only if the sources cannot silently corrupt one another. ProtectedTable therefore accumulated rules around the composed view:

  • 36ef9e14 made .state directly readable and writable;
  • e0122c97 allowed an existing storage section to be supplied;
  • 5d2ece29 allowed writes to storage groups owned by the manager when the group was writable;
  • 27c5eb07 restored an explicit subscribeHandler = false option for consumers that own synchronization themselves.

The constructor also probes whether the supplied storage section is writable. That is a legitimate protected call: the attempted write is expected to fail for read-only sections, and that failure communicates the capability being queried. See Error Handling for the narrower rule.

The default subscription is dependency management, not decoration. 9d5c7c2d fixed cached-setting invalidation and stopped the default handler from walking an entire storage group for every individual change. The manager maintains a local view of external state, so invalidation is part of the correctness contract.

The abstraction was right. The implementation was expensive.

Once ProtectedTable became convenient enough to sit in many paths, its contract was useful but its machinery became measurable overhead. Pr0f1l3r showed the hot lookup path hammering LuaJIT VM operations. The answer was not to throw away the composed interface; it was to reduce the cost underneath it.

The optimization sequence attacked different parts of that cost:

  • 5f051eee unrolled the __index loop and removed table allocations;
  • 2499548e optimized ProtectedTable's lookup paths more aggressively;
  • e6a18a18 completed another optimization and annotation pass.

That is the useful lesson. Do not discard an abstraction because its implementation is expensive. First establish whether the contract is wrong or merely the machinery underneath it. Keep the settings/state/method composition, then make __index, allocation, and lookup behavior earn their place on the hot path.

Recognizing the earned abstraction

ProtectedTable is a good example of recognizing a reusable abstraction after the fact:

  1. settings and runtime state accumulate beside one another;
  2. duplicate synchronization makes the composition painful;
  3. transparent lookup removes that plumbing;
  4. protection and invalidation make the composition safe;
  5. the stable interface names the boundary;
  6. measured optimization reduces its cost without changing the boundary.

That is different from starting with Manager:new() and hoping a universal settings framework will emerge around it.

What to steal

When settings, transient state, and behavior repeatedly travel together, make their sources explicit before adding a manager. Then ask whether one boundary can own the lookup, mutation rules, and invalidation without lying about the different lifetimes underneath.

See Storage and Lifecycle, the ProtectedTable API, and the shadow-cache invalidation scar.

When not to use this

Use a plain storage section when there is no neighboring transient state, method surface, protection rule, or synchronization boundary to compose. Do not wrap a small table in a manager merely to make the code look architectural. The composition earns its existence when it removes real duplication and enforces real ownership.