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.jsonowns 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.