Resources is a type-keyed dependency injection container for world-level shared state. It holds configuration, services, and context that processors need but that is not entity data.

class Resources:
    def insert(self, resource: T) -> None: ...
    def get(self, resource_type: type[T]) -> T | None: ...
    def require(self, resource_type: type[T]) -> T: ...
    def remove(self, resource_type: type[T]) -> T | None: ...
    def contains(self, resource_type: type[T]) -> bool: ...
    def items(self) -> ItemsView[type, object]: ...

How It Works

Resources are the shared state layer of the ECS. Where Components hold per-entity data in DataFrame columns, Resources hold per-world singletons that live outside the columnar storage path entirely.

The container is a dict[type, object] -- each resource is keyed by its concrete Python type. This means there is exactly one instance of any given type at a time. Inserting a second instance of the same type overwrites the first.

Every AsyncWorld owns a .resources instance. During tick execution, AsyncSystem.execute() passes it into each processor's process() method as a keyword argument.

world.resources.insert(SimConfig(...))
       |
AsyncSystem.execute(resources=world.resources)
       |
processor.process(df, resources=resources, tick=tick)

API

insert

Store a resource, keyed by its type:

world.resources.insert(SimConfig(gravity=9.8))
world.resources.insert(broker)

Calling insert() with a second instance of the same type replaces the first.

get

Retrieve a resource by type, returning None if absent:

broker = resources.get(CommandBroker)
if broker:
    await broker.enqueue(world_id, cmd)

require

Retrieve a resource by type, raising KeyError if absent:

config = resources.require(SimConfig)

Use require() when the processor cannot function without the resource. Use get() when the resource is optional.

remove

Remove and return a resource, or None if not present:

old_config = resources.remove(SimConfig)

contains

Check whether a resource type is registered. Also supports the in operator:

resources.contains(SimConfig)   # True
SimConfig in resources          # True

What Goes in Resources

Resources are not entity data. They are the scaffolding around it:

Category Examples
Environment parameters SimConfig(gravity=9.8), PhysicsConfig(...)
Shared services CommandBroker, LabelingConfig
Simulation context SamplingConfig, budget trackers

In RL terms: MDP parameters, hyperparameters, shared infrastructure.

Usage in Processors

Processors receive resources as a keyword argument in process():

class PhysicsProcessor(AsyncProcessor):
    components = (Position, Velocity)
    priority = 5

    async def process(self, df: DataFrame, resources: Resources = None, **kwargs) -> DataFrame:
        config = resources.require(SimConfig) if resources else SimConfig()
        return df.with_column(
            "velocity__vy",
            col("velocity__vy") - config.gravity,
        )

Submitting Commands from Processors

Processors can access the CommandBroker through resources to submit commands (spawn entities, send messages) from within the simulation loop:

class SpawnerProcessor(AsyncProcessor):
    components = (Agent,)
    priority = 50

    async def process(self, df, resources=None, tick=0, **kwargs):
        broker = resources.get(CommandBroker) if resources else None
        if broker:
            cmd = Command(
                type=CommandType.SPAWN,
                tick=tick,
                payload={"components": [Agent(name="child").to_payload()]},
            )
            await broker.enqueue("my_world", cmd)
        return df

World Forking

When a world is forked, resources are copied to the new world. This lets you override parameters per-fork for counterfactual experiments:

fork = await world.fork()
fork.resources.insert(PhysicsConfig(gravity=0.0))  # zero-G variant

The fork runs with its own resource set while the original world is unaffected.

Source Reference

Resources: src/archetype/core/resources.py