home / skills / dojoengine / book / dojo-system
This skill helps you generate Cairo Dojo system contracts that implement game logic, modify models, and emit events.
npx playbooks add skill dojoengine/book --skill dojo-systemReview the files below or copy the command above to add this skill to your agents.
---
name: dojo-system
description: Create Dojo systems that implement game logic, modify model state, and handle player actions. Use when implementing game mechanics, player commands, or automated logic.
allowed-tools: Read, Write, Edit, Glob, Grep
---
# Dojo System Generation
Create Dojo systems (smart contracts) that implement your game's logic and modify model state.
## When to Use This Skill
- "Create a spawn system"
- "Add a move system that updates position"
- "Implement combat logic"
- "Generate a system for [game action]"
## What This Skill Does
Generates Cairo system contracts with:
- `#[dojo::contract]` attribute
- Interface definition with `#[starknet::interface]`
- System implementation
- World access (`world.read_model()`, `world.write_model()`)
- Event emissions with `#[dojo::event]`
## Quick Start
**Interactive mode:**
```
"Create a system for player movement"
```
I'll ask about:
- System name
- Functions and their parameters
- Models used
- Authorization requirements
**Direct mode:**
```
"Create a move system that updates Position based on Direction"
```
## System Structure
A Dojo contract consists of an interface trait and a contract module:
```cairo
use dojo_starter::models::{Direction, Position};
// Define the interface
#[starknet::interface]
trait IActions<T> {
fn spawn(ref self: T);
fn move(ref self: T, direction: Direction);
}
// Dojo contract
#[dojo::contract]
pub mod actions {
use super::{IActions, Direction, Position};
use starknet::{ContractAddress, get_caller_address};
use dojo_starter::models::{Vec2, Moves};
use dojo::model::{ModelStorage, ModelValueStorage};
use dojo::event::EventStorage;
// Define a custom event
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct Moved {
#[key]
pub player: ContractAddress,
pub direction: Direction,
}
#[abi(embed_v0)]
impl ActionsImpl of IActions<ContractState> {
fn spawn(ref self: ContractState) {
let mut world = self.world_default();
let player = get_caller_address();
// Read current position (defaults to zero if not set)
let position: Position = world.read_model(player);
// Set initial position
let new_position = Position {
player,
vec: Vec2 { x: position.vec.x + 10, y: position.vec.y + 10 }
};
world.write_model(@new_position);
// Set initial moves
let moves = Moves {
player,
remaining: 100,
last_direction: Direction::None(()),
can_move: true
};
world.write_model(@moves);
}
fn move(ref self: ContractState, direction: Direction) {
let mut world = self.world_default();
let player = get_caller_address();
// Read current state
let position: Position = world.read_model(player);
let mut moves: Moves = world.read_model(player);
// Update moves
moves.remaining -= 1;
moves.last_direction = direction;
// Calculate next position
let next = next_position(position, direction);
// Write updated state
world.write_model(@next);
world.write_model(@moves);
// Emit event
world.emit_event(@Moved { player, direction });
}
}
// Internal helper to get world with namespace
#[generate_trait]
impl InternalImpl of InternalTrait {
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
self.world(@"dojo_starter")
}
}
}
// Helper function outside the contract
fn next_position(mut position: Position, direction: Direction) -> Position {
match direction {
Direction::None => { return position; },
Direction::Left => { position.vec.x -= 1; },
Direction::Right => { position.vec.x += 1; },
Direction::Up => { position.vec.y -= 1; },
Direction::Down => { position.vec.y += 1; },
};
position
}
```
## Key Concepts
### World Access
Get the world storage using your namespace:
```cairo
let mut world = self.world(@"my_namespace");
```
Create a helper function to avoid repeating the namespace:
```cairo
#[generate_trait]
impl InternalImpl of InternalTrait {
fn world_default(self: @ContractState) -> dojo::world::WorldStorage {
self.world(@"my_namespace")
}
}
```
### Reading Models
```cairo
let position: Position = world.read_model(player);
```
### Writing Models
```cairo
world.write_model(@Position { player, vec: Vec2 { x: 10, y: 20 } });
```
### Emitting Events
Define events with `#[dojo::event]`:
```cairo
#[derive(Copy, Drop, Serde)]
#[dojo::event]
pub struct PlayerMoved {
#[key]
pub player: ContractAddress,
pub from: Vec2,
pub to: Vec2,
}
// Emit in your function
world.emit_event(@PlayerMoved { player, from: old_pos, to: new_pos });
```
### Getting Caller
```cairo
use starknet::get_caller_address;
let player = get_caller_address();
```
### Generating Unique IDs
```cairo
let entity_id = world.uuid();
```
## System Design
### Single Responsibility
Each system should have one clear purpose:
- `MovementSystem`: Handles player/entity movement
- `CombatSystem`: Manages battles and damage
- `InventorySystem`: Manages items
### Stateless Design
Systems should be stateless, reading state from models:
```cairo
fn attack(ref self: ContractState, target: ContractAddress) {
let mut world = self.world_default();
let attacker = get_caller_address();
// Read current state
let attacker_stats: Combat = world.read_model(attacker);
let mut target_stats: Combat = world.read_model(target);
// Apply logic
target_stats.health -= attacker_stats.damage;
// Write updated state
world.write_model(@target_stats);
}
```
### Input Validation
Validate inputs before modifying state:
```cairo
fn move(ref self: ContractState, direction: Direction) {
let mut world = self.world_default();
let player = get_caller_address();
let moves: Moves = world.read_model(player);
assert(moves.remaining > 0, 'No moves remaining');
assert(moves.can_move, 'Movement disabled');
// Proceed with movement
}
```
## Permissions
Systems need writer permission to modify models.
Configure in `dojo_dev.toml`:
```toml
[writers]
"my_namespace" = ["my_namespace-actions"]
```
Or grant specific model access:
```toml
[writers]
"my_namespace-Position" = ["my_namespace-actions"]
"my_namespace-Moves" = ["my_namespace-actions"]
```
## Next Steps
After creating systems:
1. Use `dojo-test` skill to test system logic
2. Use `dojo-review` skill to check for issues
3. Use `dojo-deploy` skill to deploy your world
4. Use `dojo-client` skill to call systems from frontend
## Related Skills
- **dojo-model**: Define models used by systems
- **dojo-test**: Test system logic
- **dojo-review**: Review system implementation
- **dojo-deploy**: Deploy systems to network
This skill generates Dojo systems (Cairo contracts) that implement game logic, handle player actions, and modify model state. It creates interface traits, contract modules, world access helpers, event definitions, and complete system functions ready for integration into your Dojo world. Use it to quickly scaffold movement, combat, spawning, inventory, or any game mechanic with correct model reads/writes and event emissions.
Given a description of the desired system and its functions, the skill produces a Cairo contract annotated with #[dojo::contract] and an interface trait with #[starknet::interface]. It adds world access helpers, model read/write calls, input validation, and event definitions that follow Dojo conventions. The output includes sample implementations for actions (spawn, move, attack), helper functions (next_position, uuid usage), and permissions notes for dojo_dev.toml.
What permissions are required for a system to modify models?
Systems need writer permission for the target namespace or model. Configure writers in dojo_dev.toml per namespace or per model (e.g., "my_namespace-Position" = ["my_namespace-actions"]).
How do I make the system use a consistent namespace?
Create a world_default helper via #[generate_trait] that returns self.world(@"my_namespace"). Use that helper in every function to avoid repeating the namespace.