Sensei MCP
Official
by dojoengine
- sensei-mcp
- resources
Models
Models = Data
TL;DR
Models store structured data in your world.
Models are Cairo structs with automatic on-chain introspection.
Use the #[dojo::model] attribute to define them.
Custom enums and types are supported if they implement Introspect trait.
Define the key(s) using the #[key] attribute.
Models must have at least one key.
What are models?
Models are Cairo structs annotated with the #[dojo::model] attribute. Consider these models as a key-value store, where the #[key] attribute is utilized to define the key. While models can contain any number of fields, adhering to best practices in Entity-Component-System (ECS) design involves maintaining small, isolated models.
This approach fosters modularity and composability, enabling you to reuse models across various entity types.
#[derive(Drop, Serde)]
#[dojo::model]
struct Moves {
#[key]
player: ContractAddress,
remaining: u8,
}
The #[derive(Drop, Serde)] traits are required, respectively used by cairo ownership system then for serializing model. Missing them will automatically lead to a compilation error. You can add additional traits as needed, for example, the Copy trait.
The #[key] attribute
The #[key] attribute indicates to Dojo which fields must be used to index the model. In the previous example, the model is indexed by the player field. A field that is identified as a #[key] is not stored. It is used by the Dojo database system to uniquely identify the storage location of the model.
You need to define at least one key for each model, as this is how you query the model. However, you can create composite keys by defining multiple fields as keys. If you define multiple keys, they must all be provided to query the model. All keys must come before any non-key members in the struct
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Resource {
#[key]
player: ContractAddress,
#[key]
location: ContractAddress,
balance: u8,
}
In this case you would then use the read_model command with both the player and location fields:
let player = get_caller_address();
let location = 0x1234;
world.read_model((player, location));
Example Game Setting models
Suppose we need a place to keep a global value with the flexibility to modify it in the future. Take, for instance, a global combat_cool_down parameter that defines the duration required for an entity to be primed for another attack. To achieve this, we can craft a model dedicated to storing this value, while also allowing for its modification via a decentralized governance model.
To establish these models, you'd follow the usual creation method. However, when initializing them, employ a constant identifier, such as GAME_SETTINGS_ID.
const GAME_SETTINGS_ID: u32 = 9999999999999;
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct GameSettings {
#[key]
game_settings_id: u32,
combat_cool_down: u32,
}
In practice with modularity in mind
Consider a tangible analogy: Humans and Goblins. While they possess intrinsic differences, they share common traits, such as having a position and health. However, humans possess an additional model. Furthermore, we introduce a Counter model, a distinct feature that tallies the numbers of humans and goblins.
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Potions {
#[key]
id: u32,
quantity: u8,
}
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Health {
#[key]
id: u32,
health: u8,
}
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Position {
#[key]
id: u32,
x: u32,
y: u32
}
// Special counter model
#[derive(Copy, Drop, Serde)]
#[dojo::model]
struct Counter {
#[key]
counter: u32,
goblin_count: u32,
human_count: u32,
}
So the Human will have a Potions, Health and Position model, and the Goblin will have a Health and Position model. By doing we save having to create Health and Position models for each entity type.
So then a contract would look like this:
#[dojo::contract]
mod spawnHuman {
use array::ArrayTrait;
use box::BoxTrait;
use traits::Into;
use core::poseidon::poseidon_hash_span;
use dojo::world::Context;
use dojo_examples::models::Position;
use dojo_examples::models::Health;
use dojo_examples::models::Potions;
use dojo_examples::models::Counter;
// we can set the counter value as a const, then query it easily!
// This pattern is useful for settings.
const COUNTER_ID: u32 = 9999999999999;
// As `human_count` and `goblin_count` may have the same value,
// we can have a same id for models like `Health` and `Position`,
// leading to a same storage location for both goblin and human.
// To avoid this storage location conflict, we compute an unique
// id by hashing the id with one of these constants.
const HUMAN : felt252 = 'HUMAN';
const GOBLIN : felt252 = 'GOBLIN';
// impl: implement functions specified in trait
#[abi(embed_v0)]
impl GoblinActionsImpl of IGoblinActions<ContractState> {
fn goblin_actions(ref self: ContractState, id: u32) {
let mut world = self.world(@"dojo_starter");
let counter: Counter = world.read_model(COUNTER_ID);
let human_count = counter.human_count + 1;
let goblin_count = counter.goblin_count + 1;
// spawn a human
let human_id = poseidon_hash_span([id, HUMAN].span());
world.write_model(@Health { id: human_id, health: 100 });
world.write_model(@Position { id: human_id, x: 0, y: 0 });
world.write_model(@Potions { id: human_id, quantity: 10 });
// spawn a goblin
let goblin_id = poseidon_hash_span(
[goblin_count, GOBLIN].span()
);
world.write_model(
@Health { id: goblin_id, health: 100 }
);
world.write_model(
@Position {
id: goblin_id,
x: position.x + 10,
y: position.y + 10
}
);
// increment the counter
world.write_model(
@Counter {
counter: COUNTER_ID, human_count, goblin_count
}
);
}
}
}
A complete example can be found in the Dojo Starter
Upgrading Models and Data Migration
Upgrading a model is safe from a storage perspective as soon as the changes do not affect the existing data layout and schema. If the layout or the schema has changed, the upgrade will fail.
Suppose we have a basic model called Player that represents player information:
#[dojo::model]
struct Player {
#[key]
player_id: u64,
username: String,
score: u32,
}
In this model: player_id, serves as the primary key. We store the player’s username and their score. Now, let’s say we want to enhance our system by adding a new field called level to the Player model. We’ll do this without modifying the existing fields (player_id, username, and score).
#[dojo::model]
struct Player {
#[key]
player_id: u64,
username: String,
score: u32,
level: u8, // New field
}
Our existing data remains intact. Retrieving player information using the original fields (player_id, username, and score) continues to work seamlessly. For example, querying Player 1 (ID: 123) with username “Alice” and score 100 still provides accurate results. so this is for risk free upgrade.
Introspection
In Dojo, every model automatically implements the Introspect trait. This trait outlines the data structure of the model, which is utilized by both the world database engine and Torii for automatic data indexing.
The dojo-core library already implements the Introspect trait for Cairo built-in types.
Custom Types
For user defined types, it's crucial to implement the Introspect trait if you plan to use those types inside a model.
Two possible cases:
If the user-defined type contains only Cairo built-in types and is defined within your project, simply derive Introspect and the implementation for the type will be handled automatically by Cairo.
#[derive(Drop, Serde, Introspect)]
struct Stats {
atk: u8,
def: u8,
}
If the user-defined type includes a type that is either defined outside of your project or is an unsupported type, you will need to manually implement the Introspect trait.
Implement the trait
The trait has this signature:
trait Introspect<T> {
fn size() -> Option<usize>;
fn layout() -> Layout;
fn ty() -> Ty;
}
The size function should return None if your model, or any type within it, includes at least one dynamic type such as ByteArray or Array.
Here are the definition of Ty and Layout.
As an example of implementation, consider the following:
// My project.
#[derive(Drop, Serde)]
#[dojo::model]
struct Player {
#[key]
token_id: u256,
stats: Stats,
}
// From an other project imported as a dependency.
struct Stats {
atk: u8,
def: u8,
}
impl StatsIntrospect of dojo::database::introspect::Introspect<Stats> {
#[inline(always)]
fn size() -> Option<usize> {
Option::Some(2)
}
fn layout() -> dojo::database::introspect::Layout {
dojo::database::introspect::Layout::Struct(
array![
dojo::database::introspect::FieldLayout {
selector: selector!("atk"),
layout: dojo::database::introspect::Introspect::<u8>::layout()
},
dojo::database::introspect::FieldLayout {
selector: selector!("def"),
layout: dojo::database::introspect::Introspect::<u8>::layout()
},
]
.span()
)
}
#[inline(always)]
fn ty() -> dojo::database::introspect::Ty {
dojo::database::introspect::Ty::Struct(
dojo::database::introspect::Struct {
name: 'Stats',
attrs: array![].span(),
children: array![
dojo::database::introspect::Member {
name: 'atk',
attrs: array![].span(),
ty: dojo::database::introspect::Introspect::<u8>::ty()
},
dojo::database::introspect::Member {
name: 'def',
attrs: array![].span(),
ty: dojo::database::introspect::Introspect::<u8>::ty()
},
]
.span()
}
)
}
}
Use #[inline(always)] wisely to avoid hidden bugs during the cairo to sierra compilation. Usually it's fine to use it with dojo utils functions. In case you're using a function you don't know the complexity of, you should avoid using it.
IntrospectPacked trait
In some situations, you might need to store a model in a packed way. This is useful when you know the size of the model and want to save some storage space.
For this, you can derive the IntrospectPacked trait, which will force the use of the Fixed layout.
#[derive(Drop, Serde, IntrospectPacked)]
struct Stats {
atk: u8,
def: u8,
}
Dynamic types such as ByteArray and Array are prohibited in a packed model. However, you can include other Cairo structs within a packed model, provided these structs also implement the IntrospectPacked trait.
Old Dojo versions (before 0.7.0) used to implement only the IntrospectPacked trait. Hence, you should use this trait if you're upgrading from an old version of Dojo.
Storage and layout
The Layout enum describes the storage layout of the model in the Dojo database engine.
When the Fixed layout is used, all the fields of the model are stored in a single storage location, in the order of the fields in the struct. This has the advantage of saving storage space, but it also means that the fields are stored contiguously in memory, which can be a disadvantage if you need to upgrade your model ending up with a new storage layout.
For other layouts, each field is stored independently in a different storage location, computed from the field's selector (like Starknet does with regular contract's storage). This has the advantage of allowing for more flexible storage layouts, but it also means more gas to compute the storage location of a field as hash computation is involved.
Entities
Entities are the primary key value within the world, to which models can be attached.
In Dojo, entities are treated as a primary key value within the world, to which models can be attached. To illustrate this concept, consider a simple example of a character in a game that has a Moves and a Position model.
When defining the models for the character entity, it is important to note that we do not reference the entity directly. Instead, we simply provide two structs that the entity will be related with.
#[derive(Drop, Serde)]
#[dojo::model]
struct Moves {
#[key]
player: ContractAddress,
remaining: u8,
}
#[derive(Drop, Serde)]
#[dojo::model]
struct Health {
#[key]
player: ContractAddress,
x: u32,
y: u32
}
ECS Theory: Plenty has been written on ECS systems, to go deeper read ECS-FAQ
Enum
What is Enum
Enums, short for "enumerations," are a way to define a custom data type that consists of a fixed set of named values, called variants. Enums are useful for representing a collection of related values where each value is distinct and has a specific meaning. Enums are particularly useful in game development for representing game states, player actions, or any other set of related constants that a game might need to track.
In this example, we've defined an enum called PlayerCharacter with four variants: Godzilla, Dragon, Fox, and Rhyno. The naming convention is to use PascalCase for enum variants. Each variant represents a distinct value of the PlayerCharacter type. In this particular example, variants don't have any associated value.
#[derive(Serde, Drop, Introspect)]
enum PlayerCharacter {
Godzilla,
Dragon,
Fox,
Rhyno
}
Now let's imagine that our variants have associated values, We can define a new PlayerCharacter enum:
#[derive(Serde, Drop, Introspect)]
enum PlayerCharacter {
Godzilla: u128,
Dragon: u32,
Fox: (u8, u8),
Rhyno: ByteArray
}
Explore more about enums, including variants with associated values. Click here for detailed examples and insights.
In Dojo, all enum variants can use different variant data types. In this case, as variant data have different sizes, the enum cannot derive IntrospectPacked but must derive Introspect. If all variant data share the same type, the enum can be packed using the IntrospectPacked derive attribute.
Trait Implementations for Enums
Traits are a way to define shared behavior across types. By defining traits and implementing them for your custom enums, you can encapsulate common behaviors and methods that are relevant to the enum. This approach enhances code reusability and maintainability, especially in complex systems like game development.
Consider the GameStatus enum, which represents the various states a game can be in. This enum is a simple yet powerful example of how enums can be used to model game states.
# [derive(Serde, Copy, Drop, Introspect, PartialEq, Print)]
// Define an enum representing different states of a game
enum GameStatus {
NotStarted,
Lobby,
InProgress,
Finished
}
impl GameStatusFelt252 of Into<GameStatus, felt252> {
// Converts a GameStatus variant to its corresponding `felt252` value
fn into(self: GameStatus) -> felt252 {
match self {
GameStatus::NotStarted => 0,
GameStatus::Lobby => 1,
GameStatus::InProgress => 2,
GameStatus::Finished => 3,
}
}
}
Structuring Game Logic with Traits and enum
Building upon the GameStatus enum, we can define a Game struct that includes a GameStatus field. By implementing a custom trait for the Game struct, we can encapsulate game-specific logic and assertions.
#[derive(Copy, Drop, Serde)]
#[dojo::model]
// Define the Game struct to represent a game object
struct Game {
#[key]
id: u64,
status: GameStatus,
}
// Implement the trait for the Game struct
#[generate_trait]
impl GameImpl of GameTrait {
// Asserts that the game is in progress
fn assert_in_progress(self: Game) {
assert(self.status == GameStatus::InProgress, "Game not started");
}
// Asserts that the game is in the lobby
fn assert_lobby(self: Game) {
assert(self.status == GameStatus::Lobby, "Game not in lobby");
}
}
Enum Placement
Regarding the placement of the enum, it's generally best practice to define enums in the same file or module where they are used, especially if they are closely tied to the functionality of that system. This makes the code easier to understand and maintain, as the enum is defined in context. However, if the enum is used across multiple systems or components, it might make sense to define it in a common module that can be imported wherever needed. This approach helps to avoid duplication and keeps the codebase organized.
Given that Eternum has one enum in each system, it suggests a design where each system is self-contained and has its own set of related events or states. This design can help to encapsulate the logic of each system, making the codebase easier to navigate and understand.
Important Of Enums
Semantic Clarity: Enums provide semantic clarity by giving meaningful names to specific values. Instead of using arbitrary integers or strings, you can use descriptive identifiers. For example, consider an enum representing different player states: Idle, Running, Jumping, and Attacking. These names convey the purpose of each state more effectively than raw numeric values.
Avoiding Magic Numbers: Magic numbers (hard-coded numeric values) in your code can be confusing and error-prone. Enums help you avoid this pitfall. Suppose you have an event system where different events trigger specific actions. Instead of using 0, 1, 2, etc., you can define an enum like this:
enum Event {
PlayerSpawned,
EnemyDefeated,
ItemCollected,
}
Now, when handling events, you can use Event::PlayerSpawned instead of an arbitrary number.
Type Safety: Enums provide type safety. Each enum variant has a type, preventing accidental mixing of incompatible values. For instance, if you have an enum representing different power-ups. you can’t mistakenly assign a PowerUp value to a variable expecting a different type.
enum PowerUp {
Health,
SpeedBoost,
Invincibility,
}
Pattern Matching: Enums shine when used in pattern matching (also known as switch/case statements). You can handle different cases based on the enum variant, making your code more expressive and concise. Example:
fn handle_power_up(power_up: PowerUp) {
match power_up {
PowerUp::Health => println!("Restored health!"),
PowerUp::SpeedBoost => println!("Zooming ahead!"),
PowerUp::Invincibility => println!("Invincible!"),
}
}
Extensibility: Enums allow you to add new variants without breaking existing code. Suppose you later introduce a DoubleDamage power-up. You can simply extend the PowerUp enum:
enum PowerUp {
Health,
SpeedBoost,
Invincibility,
DoubleDamage,
}
Enums serve as powerful tools for creating expressive, self-documenting code. They enhance readability, prevent errors, and facilitate better software design.
Read more about Cairo enums here
Upgrades
In Dojo, all models are upgradeable, which means that when their code changes, they are redeployed to the same address, preserving the existing model storage and therefore the existing model data.
However, to be able to preserve existing model data, there are some limitations.
General rules
To be upgradeable, the layout of a model must not be packed (using IntrospectPacked).
For composite data structures like struct, enum, tuple and array
they are upgreadable as soon as all their elements are upgreadable,
existing elements cannot be removed, they can only be modified according to the rules described on this page. New elements can be freely added.
Each element of a data structure must keep the same type (i.e a tuple must remain a tuple), the same name and the same attributes if any (such as #[key] for model members).
A primitive type can be upgraded to a larger primitive type as soon as its felt252 representation uses the same number of felts (for example, u8 to u128 but not u128 to u256). See the next chapter for more details.
A key model member is upgradeable only if its type is an upgreadable primitive or an enum with new variants only (existing variants cannot be modified for a key member).
Primitive upgrades
This table lists the allowed upgrades for every primitive types. The type usize is not supported since it is a architecture-dependent type.
Current Allowed upgrades
bool bool, felt252
u8 u8 to u128, felt252
u16 u16 to u128, felt252
u32 u32 to u128, felt252
u64 u64 and u128, felt252
u128 u128, felt252
u256 u256
i8 i8 to i128, felt252
i16 i16 to i128, felt252
i32 i32 to i128, felt252
i64 i64 and i128, felt252
i128 i128, felt252
felt252 felt252, ClassHash, ContractAddress
ClassHash felt252, ClassHash, ContractAddress
ContractAddress felt252, ClassHash, ContractAddress
EthAddress felt252, ClassHash, ContractAddress, EthAddress
Allowed primitive types
bool
u8
u16
u32
u64
u128
u256
i8
i16
i32
i64
i128
felt252
ClassHash
ContractAddress
EthAddress
ByteArray - mostly used for dynamica sized strings
Tuples - like (u32, u32)
Array - like Array<u32>
You should refer to this most up to date example here
use starknet::ContractAddress;
#[derive(Serde, Copy, Drop, Introspect, PartialEq, Debug)]
pub enum Direction {
None,
Left,
Right,
Up,
Down,
}
impl DirectionIntoFelt252 of Into<Direction, felt252> {
fn into(self: Direction) -> felt252 {
match self {
Direction::None => 0,
Direction::Left => 1,
Direction::Right => 2,
Direction::Up => 3,
Direction::Down => 4,
}
}
}
#[derive(Drop, Serde)]
#[dojo::model]
pub struct Message {
#[key]
pub identity: ContractAddress,
#[key]
pub channel: felt252,
#[key]
pub salt: felt252,
pub message: ByteArray,
}
#[derive(Copy, Drop, Serde, Debug)]
#[dojo::model]
pub struct Moves {
#[key]
pub player: ContractAddress,
pub remaining: u8,
pub last_direction: Direction,
}
#[derive(Copy, Drop, Serde, Debug)]
#[dojo::model]
pub struct MockToken {
#[key]
pub account: ContractAddress,
pub amount: u128,
}
#[derive(Copy, Drop, Serde, IntrospectPacked, Debug)]
pub struct Vec2 {
pub x: u32,
pub y: u32,
}
// If `Vec2` wasn't packed, the `Position` would be invalid,
// and a runtime error would be thrown.
// Any field that is a custom type into a `IntrospectPacked` type
// must be packed.
#[derive(Copy, Drop, Serde, IntrospectPacked, Debug)]
#[dojo::model]
pub struct Position {
#[key]
pub player: ContractAddress,
pub vec: Vec2,
}
// Every field inside a model must derive `Introspect` or `IntrospectPacked`.
// `IntrospectPacked` can also be used into models that are only using `Introspect`.
#[derive(Copy, Drop, Serde, Introspect, PartialEq)]
pub struct PlayerItem {
pub item_id: u32,
pub quantity: u32,
pub score: i32,
}
#[derive(Drop, Serde)]
#[dojo::model]
pub struct PlayerConfig {
#[key]
pub player: ContractAddress,
pub name: ByteArray,
pub items: Array<PlayerItem>,
pub favorite_item: Option<u32>,
}
#[derive(Drop, Serde)]
#[dojo::model]
pub struct ServerProfile {
#[key]
pub player: ContractAddress,
#[key]
pub server_id: u32,
pub name: ByteArray,
}
trait Vec2Trait {
fn is_zero(self: Vec2) -> bool;
fn is_equal(self: Vec2, b: Vec2) -> bool;
}
impl Vec2Impl of Vec2Trait {
fn is_zero(self: Vec2) -> bool {
if self.x - self.y == 0 {
return true;
}
false
}
fn is_equal(self: Vec2, b: Vec2) -> bool {
self.x == b.x && self.y == b.y
}
}
#[cfg(test)]
mod tests {
use super::{Vec2, Vec2Trait};
#[test]
#[available_gas(100000)]
fn test_vec_is_zero() {
assert(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), 'not zero');
}
#[test]
#[available_gas(100000)]
fn test_vec_is_equal() {
let position = Vec2 { x: 420, y: 0 };
assert(position.is_equal(Vec2 { x: 420, y: 0 }), 'not equal');
}
}