Rules

This page records the runtime rules ownership boundary. Gameplay rule authoring lives in the selected package's SmallS modules and propsets; C++ retains engine storage, native catalogs, and the narrow protocols consumed by native code.

Original Goals

  • Rules should be overridable, expandable, or removable through configuration or programmatic registration.
  • Rules should be queryable for a concrete situation, such as the modifiers affecting one creature attacking one target with one weapon.
  • Rule constants should avoid being permanently tied to 2da row numbers when a ruleset-specific mapping is the real policy.

Useful Definitions

Attribute: A feature inherent to an object, such as creature ability scores.

Profile package: The selected ruleset package. Its files declare resource layout, and its SmallS modules own schema, interpretation, and behavior; it is not a C++ profile object.

Type: A typed rule value, such as an armor class category. The rule system defines the type and invalid value; profiles define valid values.

Flag: A compact flag built from a rule type.

Imported Table Boundary

A BioWare 2DA is a legacy source container, not an ownership unit. One table may contain native engine/resource facts and SmallS gameplay inputs. Ownership is assigned per consumed field from its actual readers and invariants; the name of the table or column does not decide the owner.

The imported-data boundary is fixed:

  • The selected package's resources.json owns install-layout declarations. C++ validates paths, constructs resource containers, and stores raw facts consumed by the engine, renderer, native persistence machinery, or editor.
  • SmallS exclusively owns gameplay legality, interpretation, formulas, and composition. A source field read only by gameplay policy is a SmallS gameplay input.
  • A field has exactly one semantic owner. If both runtimes need a raw fact, C++ owns it and exposes a narrow copied lookup or batch. A cold native tool may call a narrow SmallS profile rule directly; hot native consumers receive an already resolved flat native row.
  • Unused source fields are explicitly ignored. They do not enter either runtime speculatively.

The package-local data spec is the decomposition authority. Runtime and smalls-datagen apply the same shared batch transform to their selected resolved source. The offline sink can emit one local, uncommitted review snapshot per valid source row; the runtime sink materializes the active batch directly. A mixed definition has an info group using a C++-registered native value type and a rules group using a profile SmallS value type. The source row index remains their stable parent key; sparse rows are not compacted.

The mandatory NWN1 SmallS profile initializer is the import adapter: it loads combined definitions once, publishes complete Info[] batches to canonical contiguous C++ storage, and retains Rules[] for gameplay. Runtime C++ and SmallS do not maintain independent converter lists for the same source.

The accepted NWN1 field partitions begin with:

Source Native facts SmallS gameplay inputs
appearance.2da identity, labels, model/assembly, attachment scales, weapon scale, arm availability, personal space size category, base movement category
wingmodel.2da, tailmodel.2da identity, label, model none
placeables.2da identity, label, model, lighting/static scene facts none for current consumers
baseitems.2da identity, model/icon and inventory-layout facts; remaining overlap fields require a reader/invariant audit cost, requirements, armor/damage, weapon behavior, feat mappings

Materialized definitions preserve the source row extent. A malformed row-local value emits a diagnostic and leaves that indexed slot empty with an index of -1, while valid rows remain in the batch. A missing required source or column is a structural failure and produces a typed empty config array so the rest of the profile and module can continue loading. Native publication validates and atomically replaces the resulting indexed Info[] array, including an empty array that marks only that domain unavailable. Native profile arrays and SmallS gameplay arrays live for the selected profile lifetime and use explicit invalid-ID behavior at lookup boundaries.

The nine NWN1 base movement rates are profile rules, not imported configuration. nwn1.creature_speeds.get_base_moverate contains the complete mapping and returns 0.0 outside its 0..8 input domain. During creature initialization and appearance changes, nwn1.creature.update_movement_rate resolves the creature and appearance policy once and pushes the resulting float into the native spatial row. Native preview reads that row without calling into SmallS.

Creature body parts use the same ownership rule but a variable-length native resource protocol rather than a fixed NWN1 slot array. An assembly publishes only its supported part rows and each part's available model choices. Stable part and option IDs, anchors, labels, mirror relationships, and model associations are native facts. NWN1 naming and 0/255 sentinel conversion stay inside the NWN1 adapter. Profile SmallS owns persisted selection schema and gameplay legality. A future model may expose fewer, different, or zero parts without padding to NWN1's shape or teaching generic code to infer model names.

The implemented private data-spec contract is documented in smalls/docs/load-config.md.

Persistence, authored overrides, and long-lived reload behavior remain separate work in issues/persist-native-authoring-tables.md.

First native-data tranche report (2026-09-02)

The first tranche migrated the NWN1 creature body-part catalog and the mixed appearance.2da domain. Its implementation commit changed 58 files with 4,818 insertions and 984 deletions. The appearance transform required 16 field operations: two row_index, eleven column, and three enum operations. It retains 1,301,487 VM bytes for the test resource set.

The current body-part popup benchmark is the start of the performance series; no older revision was rebuilt and no speedup is claimed. Ten repetitions over a representative 74-option humanoid selector measured:

Measurement Wall time CPU time
Mean 17.966 us 17.915 us
Median 17.952 us 17.907 us
Standard deviation 0.288 us 0.293 us
Coefficient of variation 1.60% 1.64%

The benchmark recorded zero resource_exists calls. CPU frequency scaling was enabled and host load was elevated, so the result is suitable as a same-machine baseline, not as a universal latency figure.

The local appearance snapshot transform emitted 838 rows and omitted 14,262 from 15,100 source rows. The snapshot is an ignored local review artifact, not runtime authority and not a committed game-data copy. The follow-up audit removed the existing 2,111-file, approximately 8.3 MB game-derived corpus from the repository. All 17 production NWN1 config paths now materialize from active resources through package data specs; snapshots are optional local output.

After the all-config migration, the complete test binary passed 1,906 of 1,906 tests in 395.8 seconds. Covered behavior includes sparse row publication, row-local failure recovery, base-game whole-column fallback, module/hak resource precedence, appearance and body-part mutation/undo/redo, preview camera and root-transform preservation across appearance refresh, and combobox focus, cycling, close, and scroll behavior.

The reusable pieces were the package spec parser and diagnostics, StaticTwoDA row access, flat batch materializer, runtime/datagen sinks, canonical config binding and provenance, and native indexed publication. A baseitems.2da migration reused those pieces and added one observed five-column feat-requirement aggregate plus its contiguous storage and VM writer. The subsequent ownership review approved migration of all remaining production config paths. It did not make data specs the owner of native-only facts or of authored SmallS policy.

Runtime Rules

Gameplay modifiers and master-feat associations live in the selected package's SmallS modules. C++ retains only the native catalogs and storage protocols required by native consumers, plus the qualifier protocol that delegates its interpretation to the mandatory package matcher. nwn1.constants is the sole named NWN rules-value declaration; C++ constructs typed numeric IDs only at native storage, traversal, and test-fixture boundaries.