Containers and Assets
Containers are the fundamental storage abstraction in Asset Core. They hold assets with different spatial characteristics depending on their kind. The key idea is that the same transaction model applies across all container types, so you can model diverse domains without changing infrastructure.
Problem this concept solves
Real-world asset management involves diverse storage patterns. A one-size-fits-all data model either overfits or loses critical semantics, especially in spatial systems.
- Aggregated quantities (account balances, inventory counts)
- Ordered sequences (equipment slots, ranked lists)
- Spatial arrangements (warehouse grids, lab plates)
A single storage model cannot efficiently represent all these patterns. Asset Core solves this by providing multiple container kinds, each optimized for its spatial dimension while sharing a common operation interface. This gives you both fidelity and consistency.
Core ideas
Dimensional Classification
The hardest part of building spatial state systems is handling the dimensional mismatch between storage patterns. Account balances are scalar (0D): you care about quantity, not position. Equipment slots are sequential (1D): position matters, but only in a line. Warehouse grids are planar (2D): adjacency and collision matter in two dimensions.
Most systems pick one model and stretch it uncomfortably. Store everything in a relational table and lose spatial semantics (no collision detection, no adjacency queries). Store everything in a graph and pay for structure you don’t need (a balance doesn’t need coordinates). Store everything in a generic key-value store and implement spatial logic in application code (reinventing collision detection for every domain).
AssetCore takes a different approach: provide the RIGHT container for each dimensional need, but keep the operation interface consistent. Containers are classified by the dimensionality of their address space. Each kind adds spatial semantics without changing the transaction envelope:
| Kind | Dimension | Address Space | Example Use Cases |
|---|---|---|---|
Balance (balance) | 0D | None (aggregation only) | Balances, totals |
Slots (slots) | 1D | Sequential indices | Equipment, ordered lists |
Grid (grid) | 2D | Lattice coordinates | Plates, warehouses |
Continuous Line (continuous_line_1d) | 1D | Fixed-point coordinate (x) | Rails, actuators |
Continuous Plane (continuous_grid_2d) | 2D | Fixed-point coordinates (x, y) | Robot workcells, pick-and-place |
This dimensional typing solves a real problem: it prevents you from accidentally treating a scalar balance like a grid (nonsensical) or querying slot positions from a continuous plane (type error). The container kind encodes the spatial contract, and operations validate against it at execution time.
What you gain: the same operations (AddFungible, MoveInstance, etc.) adapt to the container’s geometry automatically. What you give up: the cognitive overhead of learning 5 container kinds instead of 1. We believe this trade makes you faster in the long run because your domain model matches your mental model.
Balances (0D)
Balance containers hold fungible balances. Use them when you only care about quantity and not spatial placement—account ledgers, currency totals, resource pools.
Balances are 0-dimensional: they have no coordinates, no positions, no spatial footprint. A balance is a pure scalar counter. This makes them the right tool for “how much” questions, not “where” questions.
- Identified by
(class_id, key)pairs - Quantities aggregate without spatial position (no x/y coordinates)
- Support add, remove, and transfer operations
Why this matters: If you stored currency in a Grid container, you’d have to pick arbitrary coordinates for each unit. If you stored it in Slots, you’d waste a slot per unit and hit capacity limits instantly. Balances give you unbounded scalar aggregation, which is what currency actually is.
Example use case: A reagent inventory in a lab where you track milliliters of liquid nitrogen. You don’t care WHERE the nitrogen is (it’s in the tank), you care HOW MUCH you have. That’s a balance.
{
"op": "AddFungible",
"args": {
"class_id": 100,
"key": 1,
"quantity": 500,
"location": {
"container_id": 1001,
"kind": "balance"
}
}
}
What you gain: aggregation without spatial overhead. A balance holding 1,000,000 units is the same cost as a balance holding 1 unit—it’s just a counter. What you give up: spatial queries. You can’t ask “what’s at position (3,5)” for a balance because positions don’t exist. If you need spatial semantics, use Grid or Continuous containers instead.
Slots (1D)
Slot containers hold unique instances in sequential positions. They are ideal for equipment, loadouts, or ordered lists where exclusivity matters more than geometry.
- Indices from 1 to N (configurable capacity)
- Each slot holds at most one instance
- Support place, remove, and swap operations
Example: Equipment slots on a character or positions in a processing queue.
{
"op": "PlaceInSlot",
"args": {
"container_id": 1001,
"instance_id": 9001,
"slot_index": 1
}
}
Grid (2D)
Grid containers add spatial geometry. They are the right choice for warehouses, lab plates, and any environment where adjacency and collision matter.
- Width x Height lattice of cells
- Items occupy multiple cells based on shape
- Support collision detection and adjacency
Example: A 96-well plate or warehouse storage grid.
Grid containers can hold both fungible stacks (with spatial placement) and unique instances.
Continuous Line (1D)
Continuous line containers (continuous_line_1d) store instances along a single fixed-point axis. Fixed-point coordinates guarantee deterministic replay without floating-point drift.
- Coordinates are deterministic fixed-point integers (no floats)
- Placements must remain within min/max bounds
- Collision checks enforce non-overlapping spans
Use cases: linear rails, conveyors with sub-cell precision, and single-axis robotics.
Continuous Plane (2D)
Continuous plane containers (continuous_grid_2d) store instances in a bounded, continuous workspace. They are designed for robotics workcells and continuous planning tasks.
- Fixed-point x/y coordinates with deterministic rounding
- Oriented-rectangle collision checks (rotation in millidegrees)
- Bounds-checked placements, moves, and rotations
- Instance-only placements (no fungible stacks)
Use cases: robot workcells, pick-and-place tasks, and metric collision planning.
Instances vs. Fungibles
This distinction mirrors the real world: currency is fungible (your $20 bill is interchangeable with mine), equipment is unique (your specific sword has durability, enchantments, and a history). The type system enforces this at the operation level.
Fungible assets are interchangeable quantities. They are represented by class and key pairs and can be split or merged freely.
- Identified by class and key
- Operations specify quantities
- Can be split and merged freely
Why it matters: fungibles can be split and merged freely, which enables operations like Distribute (split quantity across targets) and ConsolidateStacks (merge into one). Trying to “split” an instance is a type error caught at validation time.
Unique instances are individually tracked. They are represented by stable IDs and can be attached or detached to form hierarchies.
- Have globally unique IDs
- Cannot be split or merged
- Can form parent-child hierarchies via attach/detach
Unique instances cannot be split or merged—MoveInstance relocates the entire instance atomically. The parent-child hierarchy for instances (Attach/Detach) models real nesting: a backpack contains a pouch, which contains potions. The hierarchy is reflected in queries, making “what’s in this container recursively?” a first-class operation.
Classes and Shapes
Classes define asset types. They are the canonical identifiers for both fungibles and instances, and they must be registered before use.
- Provide metadata like names and optional flags
- Serve as stable identifiers for fungible balances or unique instances
- Must be registered before use
Classes are namespace-scoped: a class_id only has meaning within its namespace. The same numeric ID in a different namespace is a different class. Once registered, a class ID cannot be reused for a different definition. This keeps identity stable for audit and replay.
Classes can optionally carry behavior constraints that are enforced at operation validation time. These constraints are how you encode “real world” limits into deterministic rules:
- Balance scale (fixed-point quantization for balances)
- Minimum and maximum balance floors/ceilings
- Maximum stack size for fungible stacks
- Maximum stacks per container for a class
- Negative-balance capability (explicitly allowed or not)
Shapes define spatial footprints. They allow the runtime to enforce collision rules deterministically, which is essential for correctness.
- Width and height in grid cells
- Associated with a class and optional variant key
- Required for grid container placement
Variants are expressed with stack_key. The (class_id, stack_key) pair defines fungible identity and determines which shape (if any) is required for placement. This lets you model variants (size, tier, condition) without multiplying class IDs.
Compatibility recap:
- Balance: Fungible quantities only (fixed-point scale; no positions)
- Slots: Instances only (one instance per slot)
- Grid: Fungible stacks or instances (shapes required for multi-cell placement)
- Continuous 1D/2D: Instances only (fixed-point coordinates; shapes/spans required)
{
"op": "RegisterClass",
"args": {
"request": {
"class_id": 200,
"flags": 2,
"name": "Sample Tube"
}
}
}
How it fits into the system
Validation and dispatch
Container kind is part of the operation contract. During L2 validation, the runtime checks container kind, class registration, and shape requirements before any mutation. Preflight uses the same checks, so a successful preflight implies the commit will pass if the world has not changed.
Examples of how this plays out:
AddFungibleon balance increments a scalar balance.AddFungibleon grid requires shape registration and collision checks.PlaceInSloton balance is rejected (wrong container kind).
Registry and shapes
Class and shape registration live in the namespace registry. Grid and continuous placements consult the registry for footprints or spans, so placement is deterministic and replay-safe instead of ad hoc.
Read projections and query surfaces
Read projections keep container-specific indexes (balances by class/key, slot occupancy, grid anchors). Read responses return typed payloads by container kind, so “container contents” queries always match the geometry the container enforces.
See also
- Transactions and Operations - How operations work
- Action Reference - Complete operation reference
- Runtime Model - How containers fit in the architecture