Propset Architecture
Version: 0.1.0 Last Updated: 2026-02-20
Overview
The propset system is an ECS-inspired component architecture for game objects. Data schemas are defined in Smalls script, giving ruleset authors full control over data layout without engine recompilation. The engine remains blind to game-logic data and communicates with scripts exclusively through native function boundaries.
The design is intentionally shaped around the access pattern of RPG game logic: discrete event simulation, not continuous batch processing. Combat resolves one event at a time — an attack fires, two specific creatures' data is fetched, a result is computed. You never need to iterate all creatures in combat simultaneously. This means the optimization target is fast per-object lookup, not cache-coherent iteration across thousands of entities, and the slab-based pool with O(1) handle lookup is exactly right for that.
Two Worlds
Script Propsets
Game-logic data — ability scores, feats, HP, class levels — that the engine has no need to know about. Schemas are defined in .smalls files using the [[propset]] annotation. Memory is managed by PropsetPoolManager in slab-based pools keyed by ObjectHandle. Scripts access propsets through the get_propset intrinsic.
[[propset]]
type CreatureStats {
abilities: int[6];
save_fort: int;
save_reflex: int;
save_will: int;
skills: array!(int);
feats: array!(int);
race: int;
gender: int;
good_evil: int;
lawful_chaotic: int;
ac_natural_bonus: int;
cr: float;
cr_adjust: int;
perception_range: int;
disarmable: int;
immortal: int;
interruptable: int;
lootable: int;
pc: int;
plot: int;
chunk_death: int;
bodybag: int;
};
The engine registers propset types and populates them at object load time, but does not read or write propset fields directly thereafter. Game logic that previously lived in C++ (AC calculation, feat checks, skill rolls) migrates to script and accesses data directly through propset fields.
Native Arrays
Engine-critical data — position, visual appearance, physics bodies — that subsystems need to iterate efficiently every frame. These are plain C++ arrays indexed by object slot, owned by the relevant subsystem. The renderer owns Transform transforms[MAX_OBJECTS]. The physics system owns its own arrays. The object manager is the authority on slot lifetime.
Scripts have no direct access to native arrays. They are purely a C++ implementation detail for cache-coherent batch iteration by engine subsystems.
The Boundary: Bridge Functions
All communication between the script world and the engine-owned data goes through native function signatures. Scripts call get_position(obj) — not get_propset(obj): Transform. This is deliberate:
- Engine systems protect their own invariants (
set_positioncan update spatial queries, trigger area transitions, notify pathfinding) - No shared memory or type system leakage in either direction
- The script-engine boundary is entirely legible as a set of function signatures
- No "native propset" concept needs to exist in the script type system at all
Native arrays are a C++ implementation pattern, not a script-visible abstraction.
Script Propset Schemas
Creature
Four propsets covering distinct data lifetimes and computational domains. Common operations touch at most two propsets.
// "What this creature is" — rarely changes, consulted by most RPG calculations
[[propset]]
type CreatureStats {
abilities: int[6]; // STR=0, DEX=1, CON=2, INT=3, WIS=4, CHA=5
save_fort: int;
save_reflex: int;
save_will: int;
skills: array!(int); // indexed by Skill newtype value
feats: array!(int); // Feat newtype values
race: int;
gender: int;
good_evil: int; // 0=evil, 50=neutral, 100=good
lawful_chaotic: int; // 0=chaotic, 50=neutral, 100=lawful
ac_natural_bonus: int;
cr: float;
cr_adjust: int;
perception_range: int;
disarmable: int;
immortal: int;
interruptable: int;
lootable: int;
pc: int;
plot: int;
chunk_death: int;
bodybag: int;
};
// Vital state — changes in combat, persisted to disk
[[propset]]
type CreatureHealth {
hp: int;
hp_current: int;
hp_max: int;
hp_temp: int;
faction_id: int;
starting_package: int;
};
// Class progression — changes on level-up, source of BAB and spell slots
[[propset]]
type CreatureLevels {
classes: int[8]; // Class IDs per slot
class_levels: int[8]; // Level count per slot
xp: int;
walkrate: int;
};
// Transient combat state — resets between sessions, not persisted
[[propset]]
type CreatureCombat {
attack_current: int;
attacks_onhand: int;
attacks_offhand: int;
attacks_extra: int;
combat_mode: int;
ac_armor_base: int;
ac_shield_base: int;
size_ab_modifier: int;
size_ac_modifier: int;
target_distance_sq: float;
target_state: int;
hasted: int;
size: int;
};
Common operation join cost:
| Operation | Propsets |
|---|---|
| Ability check | CreatureStats |
| Has feat | CreatureStats |
| Skill check | CreatureStats |
| Apply damage | CreatureHealth |
| Death check | CreatureHealth |
| Save roll | CreatureStats |
| BAB / attack roll | CreatureLevels + CreatureCombat |
| AC calculation | CreatureStats + CreatureCombat |
| Spell slot check | CreatureLevels |
Nothing requires more than two propsets. The ceiling is AC and attack resolution, which are the most complex calculations and still only need the combat state alongside the sheet.
Excluded from propsets (v1):
- SpellBook — slot arrays per class per level, too complex for v1 field types; remains as native functions (get_spell_slots, get_remaining_slots)
- LevelHistory — per-level-up detail (feat/skill/ability choices); player-character only, rarely accessed from script; remains serialized in C++
- CreatureScripts — all Resref fields (strings); event handler names, bridge-function territory
Item
[[propset]]
type ItemStats {
base_item: int;
cost: int;
cost_additional: int;
stack_size: int;
charges: int;
cursed: int;
identified: int;
plot: int;
stolen: int;
// item properties: deferred — requires structured array representation
};
Item properties (the ItemProperty list) are deferred. They require either a structured encoding into array!(int) or v2 propset field types. Native functions cover them in the interim.
Door
[[propset]]
type DoorState {
hp: int;
hp_current: int;
hardness: int;
locked: int;
lock_dc: int;
bash_dc: int;
open_state: int;
plot: int;
interruptable: int;
};
Placeable
[[propset]]
type PlaceableState {
hp: int;
hp_current: int;
hardness: int;
locked: int;
plot: int;
useable: int;
has_inventory: int;
static_: int;
};
Other Objects
Triggers, waypoints, sounds, encounters — these carry minimal runtime state. Their script interaction is primarily through event handlers and native bridge functions. Propsets are not a priority for these types.
Native Array System (Future)
This section documents the intended design for when rendering and larger simulations are in scope. Nothing here needs to be built yet.
Concept
Each engine subsystem owns flat arrays indexed by object slot. The object manager assigns slots at creation and reclaims them at destruction. Arrays are pre-allocated at startup for MAX_OBJECTS capacity.
// Renderer owns these
Transform transforms[MAX_OBJECTS];
Appearance appearances[MAX_OBJECTS];
// Physics system owns these
PhysicsBody physics_bodies[MAX_OBJECTS];
Iteration is a plain loop — no query system, no archetype machinery, no indirection:
for (uint32_t i = 0; i < active_count; ++i) {
render(transforms[active_slots[i]], appearances[active_slots[i]]);
}
Registration
A subsystem registers its array so the object manager can initialize/clear slots on object creation/destruction:
object_manager.register_native_array<Transform>(
transform_array,
MAX_OBJECTS,
/*on_create=*/[](uint32_t slot) { transform_array[slot] = {}; },
/*on_destroy=*/[](uint32_t slot) { /* cleanup if needed */ }
);
No propset pool, no hash map, no GC involvement. Slot lifetime is managed by the object manager directly.
Script Access
Scripts access native array data exclusively through bridge functions. No get_propset equivalent for native arrays:
// core/object.smalls — bridge functions, not propset access
[[native]] fn get_position(obj: object): vec3;
[[native]] fn set_position(obj: object, pos: vec3);
[[native]] fn get_facing(obj: object): float;
[[native]] fn set_facing(obj: object, facing: float);
[[native]] fn get_area(obj: object): object;
The bridge function handles side effects (spatial index update, area transition check, pathfinding notify). Scripts never touch Transform memory directly.
Separation from Script Propsets
Native arrays and script propsets are completely separate systems with no shared infrastructure:
| Script Propsets | Native Arrays | |
|---|---|---|
| Schema defined in | Smalls script | C++ struct |
| Memory managed by | PropsetPoolManager |
Subsystem directly |
| GC integration | Yes | No |
| Access from script | get_propset intrinsic |
Bridge functions only |
| Access from C++ | Via propset pool API | Direct array index |
| Iteration pattern | Per-object lookup | Contiguous loop |
| Optimization target | Fast single-object fetch | Cache-coherent batch |
Implementation Plan
Phase 1 — Script Propset Schemas (Current)
Define propset schemas and wire them to existing C++ data as a bootstrap layer. The C++ data remains authoritative during this phase; propsets are populated from it at load time and flushed back at save time.
- Define
CreatureStats,CreatureHealth,CreatureLevels,CreatureCombatincore/creature.smalls - Define
ItemStatsincore/item.smalls - Define
DoorState,PlaceableStatein respective script modules - Register propset types from C++ at runtime startup (via
ModuleBuilderor equivalent) - In
Creature::instantiate()— allocate and populate propsets from existing C++ struct fields - In serialization path — flush propset data back to GFF/JSON on save
- Write tests: load creature → check propset fields match GFF data → save → reload → compare
Key constraint: C++ structs (CreatureStats, CombatInfo, etc.) are not modified in this phase. The propset is a parallel copy, not yet canonical.
Phase 2 — Game Logic Migration (Medium-term)
Move ruleset calculations from C++ into script. Propsets become canonical; C++ structs become load/save intermediaries only.
- Implement combat system in script using propset direct field access
- Replace native
get_ability_score,has_feat,get_skill_rankwith script functions readingCreatureStats - Replace native
get_total_levels,get_level_by_classwith script functions readingCreatureLevels - C++ combat event dispatcher calls script functions rather than executing game logic itself
- Deprecate C++ game-logic functions as script equivalents cover them
- Schema migration story: when a propset field is added, existing saves get default zero; document the policy
Phase 3 — Native Array Infrastructure (Pre-rendering)
Build the subsystem array infrastructure before the renderer needs it.
- Object manager gets slot assignment and reclamation (if not already present)
register_native_array<T>()API on object managerTransformandAppearancearrays registered at startup- Bridge functions in
core/object.smallsreplace any direct field access patterns - Validate that no script code assumes position/appearance data is in a propset
Phase 4 — Renderer Integration (Future)
Renderer and physics iterate native arrays directly. No per-object virtual dispatch.
- Renderer reads
transforms[slot]andappearances[slot]in its draw loop active_slots[]maintained by object manager (compacted list of live slots)- Physics system registers its own arrays
- Spatial query structures (area octree, etc.) update through
set_positionbridge function
What Is Already Built
PropsetPoolManager— slab pool, slot management, dirty tracking, unmanaged array support[[propset]]annotation — parsed and validated byTypeResolver- Propset field validation — only allows POD types,
[[value_type]]structs, fixed arrays,array!(int|float|bool) get_propsetintrinsic — declared incore/prelude.smallsarray!(int)as propset field — IArray bridge infrastructure forfeatsandskills
What Is Not Yet Built
- Propset schema definitions in
.smallsfiles (Phase 1) - C++ registration of propset types at startup (Phase 1)
- Bootstrap population of propsets from existing C++ object data (Phase 1)
- Serialization round-trip through propsets (Phase 1)
- Script-side ruleset functions using propset fields (Phase 2)
- Native array registration API (Phase 3)
- Renderer/physics integration (Phase 4)
Deferred / Out of Scope (v1)
SpellBookin propsets — slot-per-level-per-class structure needs richer field types or a compact encodingLevelHistoryin propsets — complex nested structure, player-character onlystringfields in propsets — requires GC root management within unmanaged slab memory; tracked as v2objecthandle fields in propsets —TypedHandleis POD and could work inline, but the policy and type-system representation need designItemPropertylist in propsets — structured encoding TBD- Query system (iterate all objects with propset X) — not needed at current scale; revisit if simulation scope grows