Modding Guide

Make Ourcraft your own: new blocks, gadgets, chat commands and whole new games. This guide starts from nothing and builds a real mod step by step. You'll need to be comfortable typing commands in a terminal; a little Rust helps, but you can learn it as you go by changing the examples.

Grown-up and kid team? A great way to start is for the kid to design (the block colours, what a command should do, the rules of a game) and the grown-up to type. Pixel art for textures is just letters in a grid, which kids can draw on squared paper first.

1. What a mod can do

Mods run on the server only. When friends join your world, their game gets your new blocks and tools automatically: they don't need to install anything.

Mods can't do anything else. They can't read files, use the internet or see the clock. That's what makes it safe to try mods other people made.

2. What you need

  1. The game's source and Rust, as in Getting started.
  2. The WebAssembly target for Rust, which turns your mod into a .wasm file:
    rustup target add wasm32-unknown-unknown
    git clone https://github.com/rsantacroce/ourcraft
    cd ourcraft/mods
  3. A text editor. VS Code with the rust-analyzer extension is a good choice: it shows mistakes as you type and completes names like guest:: for you.

Everything mod-related lives in the mods/ folder:

mods/paint_blocks/A small example: paint blocks and /rainbow.
mods/snow_fun/The tutorial mod from this guide, finished.
mods/team_shooter/Splat Squad: a big example of a game mode.
mods/build.shBuilds every mod into mods/dist/.
crates/mod_api/The mod API: every event and function.

3. Your first mod

A mod that says hello to everyone who joins. In the mods folder:

  1. Make a folder hello_mod with a src folder inside it.
  2. Create hello_mod/Cargo.toml:
    [package]
    name = "hello_mod"
    version.workspace = true
    edition.workspace = true
    license.workspace = true
    
    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    ourcraft_mod_api = { path = "../../crates/mod_api" }
    The name becomes the mod's name and the .wasm file name. Use lowercase letters and underscores.
  3. Add it to the members list in mods/Cargo.toml:
    [workspace]
    resolver = "3"
    members = ["paint_blocks", "snow_fun", "team_shooter", "hello_mod"]
  4. Create hello_mod/src/lib.rs:
    use ourcraft_mod_api::guest::{self, Mod};
    use ourcraft_mod_api::{Event, EventReply};
    
    #[derive(Default)]
    struct Hello;
    
    impl Mod for Hello {
        fn on_event(&mut self, event: Event) -> EventReply {
            if let Event::PlayerJoined { name, .. } = event {
                guest::chat(None, &format!("Hello, {name}! Welcome to my world."));
            }
            EventReply::Ok
        }
    }
    
    ourcraft_mod_api::export_mod!(Hello);
  5. Build it:
    ./build.sh
    # built dist/hello_mod.wasm (... bytes)
  6. Start the game again from the top folder. It bundles everything in mods/dist/, so the new mod is included:
    cd ..
    cargo run --release
    Open a world and you'll be greeted. In the terminal you'll see:
    INFO Loaded mod 'hello_mod'
Quicker way while you're working on a mod: instead of rebuilding the game, copy the .wasm file into the mods folder of your game data (see Sharing your mod) and just reopen the world.

4. How mods work

A mod is a conversation with the game:

  1. The game tells the mod that something happened: an event (a player joined, someone typed in chat, a tick of the clock...).
  2. While handling it, the mod can ask the game to do things by calling guest:: functions (place a block, send a chat message...), or ask questions (what block is here? where are the players?).
  3. The mod answers the event with a reply: usually EventReply::Ok; Deny to stop a player changing a block; Handled to say a chat message was your command.

Your mod is a Rust type that implements Mod, with one function, on_event. Its fields are the mod's memory: they keep their values from one event to the next. It must implement Default, which is how the mod starts. export_mod! at the bottom connects it to the game.

Events come one at a time, so you never have to worry about two things happening at once.

5. Tutorial: Snow Fun

Now a bigger mod, in four steps: a greeting, two new blocks, a /snowman command, and a snowball thrower whose snowballs leave snow bricks where they land. The finished mod is in the repository at mods/snow_fun/src/lib.rs, and comes with the game, so you can try it first: type /snowman or /snowballs in a building world.

Set it up like hello_mod above, with the name snow_fun. First, what the mod needs to remember:

use std::collections::BTreeSet;
use ourcraft_mod_api::{EntityId, PlayerId};

/// Everything the mod remembers between events.
#[derive(Default)]
struct SnowFun {
    /// Off in game-mode worlds, so we don't interfere with the game.
    active: bool,
    /// Players holding the snowball thrower.
    throwers: BTreeSet<PlayerId>,
    /// Snowballs in the air.
    snowballs: BTreeSet<EntityId>,
}

A BTreeSet is a set of values, sorted. We'll fill these in steps 4.

Step 1: Say hello, but only in building worlds

Every mod is loaded in every world. Snow Fun shouldn't hand out snowballs in the middle of a Splat Squad match, so it checks the world's game mode when it starts, and ignores everything in game-mode worlds:

impl Mod for SnowFun {
    fn on_event(&mut self, event: Event) -> EventReply {
        match event {
            Event::Init { game_mode, .. } => {
                register_content();                // step 2
                self.active = game_mode.is_none(); // a plain building world?
                guest::log("Snow Fun loaded");
                EventReply::Ok
            }
            // In a game-mode world (like Splat Squad), ignore everything else.
            _ if !self.active => EventReply::Ok,

            Event::PlayerJoined { player, name } => {
                guest::chat(
                    Some(player),
                    &format!("Brrr, hi {name}! Try /snowman and /snowballs"),
                );
                EventReply::Ok
            }
            Event::PlayerLeft { player } => {
                self.throwers.remove(&player);
                EventReply::Ok
            }
            // ... steps 3 and 4 go here ...
            _ => EventReply::Ok,
        }
    }
}

The line _ if !self.active => EventReply::Ok catches every event when the mod is off. Events after it only run in building worlds. chat(Some(player), ...) sends a message only to that player; None would send it to everyone.

Step 2: New blocks

Blocks, textures, models and tools can only be added during Init. We do it in every world, even when the mod is off, so that worlds stay compatible.

const SNOW_WHITE: [u8; 4] = [240, 248, 255, 255];

fn register_content() {
    // Snow Bricks, drawn by hand: each letter is a colour from the palette.
    let rows = [
        "AAAAAAAAAAAAAAAA",
        "BBBBBBBBBBBBBBBA",
        "BBBBBBBBBBBBBBBA",
        "BBBBBBBBBBBBBBBA",
        "AAAAAAAAAAAAAAAA",
        "BBBBBBBABBBBBBBB",
        "BBBBBBBABBBBBBBB",
        "BBBBBBBABBBBBBBB",
        "AAAAAAAAAAAAAAAA",
        "BBBBBBBBBBBBBBBA",
        "BBBBBBBBBBBBBBBA",
        "BBBBBBBBBBBBBBBA",
        "AAAAAAAAAAAAAAAA",
        "BBBBBBBABBBBBBBB",
        "BBBBBBBABBBBBBBB",
        "BBBBBBBABBBBBBBB",
    ];
    let palette = [('A', [190, 210, 230, 255]), ('B', SNOW_WHITE)];
    guest::register_texture("snow_bricks", pixel_art(&rows, &palette));
    guest::register_block(BlockSpec::simple(
        "snow_fun:snow_bricks",
        "Snow Bricks",
        "snow_bricks",
    ));

    // An icy blue lantern that glows in the dark.
    guest::register_texture("ice_lantern", speckled([150, 210, 255, 255], 7));
    guest::register_block(BlockSpec {
        light: 12,
        ..BlockSpec::simple("snow_fun:ice_lantern", "Ice Lantern", "ice_lantern")
    });

    // A recipe for survival: a snow brick on top of a piece of coal makes
    // an ice lantern (on a crafting table or in the inventory's grid).
    guest::register_recipe(RecipeSpec::Shaped {
        pattern: vec!["S".into(), "C".into()],
        key: vec![
            ('S', "snow_fun:snow_bricks".into()),
            ('C', "core:coal".into()),
        ],
        result: "snow_fun:ice_lantern".into(),
        count: 1,
    });
}

The texture is drawn as text: 16 rows of 16 characters. Each character is looked up in the palette, a list of (character, [red, green, blue, alpha]). Characters not in the palette are see-through. Colours go from 0 to 255; alpha 255 is solid.

Names have two parts, yourmod:thing, so two mods never pick the same name. BlockSpec::simple makes a normal solid block. To change something, use the .. trick above, or write every field out:

guest::register_block(BlockSpec {
    name: "mymod:window".into(),
    display_name: "Fancy Window".into(),
    render: Render::Cutout,      // has fully see-through pixels
    solid: true,                 // you can't walk through it
    breakable: true,
    light: 0,                    // 0-15; above 0 glows
    texture_top: "window_frame".into(),
    texture_bottom: "window_frame".into(),
    texture_side: "window".into(),
});

Use Render::Cutout for blocks with holes (alpha 0 pixels), like leaves, and Render::Translucent for partly see-through blocks, like water. New blocks appear in the creative inventory (E) automatically.

In survival worlds players can't just pick blocks from a list, so the register_recipe at the end lets them craft an ice lantern: a snow brick on top of a piece of coal. Every block is also an item with the same name, so recipes can use your blocks and the game's (core:coal). Mod blocks drop themselves when broken.

Step 3: A chat command

Chat messages arrive as Event::Chat. Add this to on_event:

            Event::Chat { player, text } => match text.trim() {
                "/snowman" => {
                    build_snowman(player);
                    EventReply::Handled // hide the command from chat
                }
                "/snowballs" => {
                    self.toggle_thrower(player);
                    EventReply::Handled
                }
                _ => EventReply::Ok,    // normal chat: let it through
            },

And the function that builds the snowman:

/// Build a snowman three blocks in front of the player.
fn build_snowman(player: PlayerId) {
    let Some(p) = guest::players().into_iter().find(|p| p.id == player) else {
        return;
    };
    let (Some(brick), Some(lantern)) = (
        guest::block_id("snow_fun:snow_bricks"),
        guest::block_id("snow_fun:ice_lantern"),
    ) else {
        return;
    };
    let x = (p.position[0] + p.look[0] * 3.0).floor() as i32;
    let z = (p.position[2] + p.look[2] * 3.0).floor() as i32;
    let Some(ground) = guest::ground_height(x, z) else {
        return;
    };
    // Two snowy blocks for the body and a glowing head.
    let body: [(i32, u16); 3] = [(1, brick), (2, brick), (3, lantern)];
    let mut placed = 0;
    for (dy, block) in body {
        let pos: BlockPos = [x, ground + dy, z];
        // Only build in empty air. `try_set_block` says whether it worked
        // (it doesn't if that part of the world hasn't loaded yet).
        if guest::get_block(pos) == 0 && guest::try_set_block(pos, block) {
            placed += 1;
        }
    }
    if placed == 0 {
        guest::chat(Some(player), "No room for a snowman here. Try somewhere else!");
        return;
    }
    guest::effect(EffectKind::Confetti, center([x, ground + 3, z]), SNOW_WHITE);
    guest::chat(Some(player), "Ta-da! A snowman.");
}

Things to notice:

Step 4: A snowball thrower

A tool is something in the hotbar that sends your mod Event::UseTool when a player clicks. A projectile is an entity that flies by itself, falls with gravity, and sends Event::ProjectileHit when it hits something. Register a model for the snowball and the tool, in register_content:

    // (inside register_content)
    // What a snowball looks like in the air: one small white box.
    guest::register_model(ModelSpec {
        name: "snow_fun:snowball".into(),
        boxes: vec![ModelBox {
            min: [-0.15; 3],
            max: [0.15; 3],
            color: SNOW_WHITE,
        }],
        spin: true,
    });

    // The thrower, with a 16x16 icon drawn the same way as a texture.
    guest::register_tool(ToolSpec {
        name: "snow_fun:thrower".into(),
        display_name: "Snowball Thrower".into(),
        icon: pixel_art(&icon, &icon_palette),
        cooldown: 0.3, // seconds between throws while the button is held
    });

/snowballs swaps the player's hotbar for the thrower, and back:

impl SnowFun {
    fn toggle_thrower(&mut self, player: PlayerId) {
        if self.throwers.remove(&player) {
            // An empty list gives the normal block hotbar back.
            guest::set_tools(player, Vec::new());
            guest::chat(Some(player), "Snowball thrower put away");
        } else {
            self.throwers.insert(player);
            guest::set_tools(player, vec!["snow_fun:thrower".into()]);
            guest::show_title(Some(player), "Snowball time!", 2.0);
        }
    }
}

Throwing, and what happens when a snowball lands:

            Event::UseTool { player, tool, origin, direction, .. }
                if tool == "snow_fun:thrower" =>
            {
                let start = add(origin, scale(direction, 0.6)); // just past the face
                if let Some(id) = guest::spawn_entity(EntitySpec {
                    model: "snow_fun:snowball".into(),
                    position: start,
                    velocity: scale(direction, 22.0), // blocks per second
                    motion: EntityMotion::Projectile {
                        gravity: 12.0,
                        radius: 0.2,
                        hits_players: true,
                    },
                    lifetime: 4.0,
                    owner: Some(player),
                }) {
                    self.snowballs.insert(id); // remember it's ours
                }
                EventReply::Ok
            }

            // Every mod hears about every projectile, so only handle our own.
            Event::ProjectileHit { entity, hit, .. } if self.snowballs.remove(&entity) => {
                match hit {
                    Hit::Block { pos, normal } => {
                        // Put a snow brick on the face it hit, if that's air.
                        let target = [pos[0] + normal[0], pos[1] + normal[1], pos[2] + normal[2]];
                        if guest::get_block(target) == 0
                            && let Some(brick) = guest::block_id("snow_fun:snow_bricks")
                        {
                            guest::set_block(target, brick);
                        }
                        guest::effect(EffectKind::Splat, center(target), SNOW_WHITE);
                    }
                    Hit::Player { at, .. } => {
                        guest::effect(EffectKind::Poof, at, SNOW_WHITE);
                        guest::sound("pop", Some(at));
                    }
                }
                EventReply::Ok
            }

Build with ./build.sh, start a world, type /snowballs, and have a snowball fight. ๐ŸŽ‰

6. More recipes

Do something regularly

Tick arrives 20 times a second; tick counts up from the start.

Event::Tick { tick, .. } => {
    // 20 ticks = 1 second. Do something every 10 seconds:
    if tick % 200 == 0 {
        guest::chat(None, "Ten more seconds of fun!");
    }
    EventReply::Ok
}

Protect an area

Reply Deny to a BlockChange and the block goes back as it was.

// No building or breaking within 20 blocks of the spawn point.
Event::BlockChange { player, pos, .. } => {
    if let Some(s) = guest::world_spawn() {
        let (dx, dz) = (pos[0] - s[0], pos[2] - s[2]);
        if dx * dx + dz * dz < 20 * 20 {
            guest::chat(Some(player), "This area is protected!");
            return EventReply::Deny;
        }
    }
    EventReply::Ok
}

Talk to players: HUD, titles, health and rules

use ourcraft_mod_api::{HudLine, PlayerRules};

// The panel in the top right corner.
guest::set_hud(Some(player), vec![
    guest::hud_line("Treasure hunt"),
    HudLine { text: format!("Gems found: {gems}"), color: [120, 220, 255, 255] },
]);

// A big message in the middle of the screen, for 3 seconds.
guest::show_title(None, "Go!", 3.0);

// A health bar (pass max = 0.0 to hide it).
guest::set_health(player, 75.0, 100.0);

// Rules for one player: speed, flying, building, shirt colour, name tag.
guest::set_rules(player, PlayerRules {
    speed: 1.3,
    can_fly: false,
    can_build: true,
    color: Some([255, 200, 0, 255]),
    tag: Some("[Gold]".into()),
});

The world, time, rays, teleports and sounds

guest::set_time_of_day(0.5);                 // 0 = midnight, 0.5 = noon
let noon = guest::time_of_day() > 0.25 && guest::time_of_day() < 0.75;

let stone = guest::block_id("core:stone").unwrap();
let y = guest::ground_height(10, -4).unwrap_or(64);
guest::set_block([10, y + 1, -4], stone);

// What is the player looking at (up to 8 blocks away)?
if let Some(Hit::Block { pos, .. }) = guest::raycast(origin, direction, 8.0, false, Some(player)) {
    guest::chat(Some(player), &format!("That's block {:?}", pos));
}

guest::teleport(player, [0.5, 80.0, 0.5]);
guest::sound("cheer", None);                 // for everyone

Crafting recipes

Recipes are registered in Init, like blocks. A Shaped recipe is a little picture of up to 3ร—3 squares; it works anywhere in the grid and mirrored. A Shapeless one just needs the ingredients, in any squares. Recipes that fit in 2ร—2 work in the inventory, bigger ones need a crafting table.

// Any arrangement: a log and a coal make 8 torches.
guest::register_recipe(RecipeSpec::Shapeless {
    ingredients: vec!["core:log".into(), "core:coal".into()],
    result: "core:torch".into(),
    count: 8,
});

// Shaped, 3 by 3: ' ' is an empty square.
guest::register_recipe(RecipeSpec::Shaped {
    pattern: vec!["GGG".into(), "G G".into(), "GGG".into()],
    key: vec![('G', "core:glass".into())],
    result: "mymod:fish_tank".into(),
    count: 1,
});

A new item

Every block is already an item. For things that aren't blocks (a gem, a coin, a snack), register an item with a picture. Snow Fun adds a snow cone you can eat:

// In register_content: a picture (16 by 16, drawn like the bricks)...
guest::register_texture("snow_cone", pixel_art(&cone, &cone_colours));
// ...and the item. ItemSpec::simple stacks to 64 and does nothing else.
guest::register_item(ItemSpec {
    food: 3, // fills 3 of the 20 hunger points
    ..ItemSpec::simple("snow_fun:snow_cone", "Snow Cone", "snow_cone")
});
// A way to make it: a snow brick and an apple give two snow cones.
guest::register_recipe(RecipeSpec::Shapeless {
    ingredients: vec!["snow_fun:snow_bricks".into(), "core:apple".into()],
    result: "snow_fun:snow_cone".into(),
    count: 2,
});

An ItemSpec can also be a fuel (fuel: seconds of burning) or turn into something in the furnace (smelts_into). Food is eaten by holding "place", like apples.

A new animal

Animals walk around by themselves, follow players holding their food, have babies, and can be petted. You give them a model and a few numbers; the game does the rest.

// In register_content: a model (facing +z, feet at y = 0)...
guest::register_model(ModelSpec {
    name: "mymod:penguin".into(),
    boxes: vec![
        ModelBox { min: [-0.25, 0.0, -0.2], max: [0.25, 0.7, 0.2], color: [30, 30, 40, 255] },
        ModelBox { min: [-0.18, 0.1, 0.15], max: [0.18, 0.6, 0.22], color: [245, 245, 245, 255] },
        ModelBox { min: [-0.05, 0.45, 0.2], max: [0.05, 0.5, 0.32], color: [255, 160, 30, 255] },
    ],
    spin: false,
});
// ...then the animal itself.
guest::register_mob(MobSpec {
    name: "mymod:penguin".into(),
    display_name: "Penguin".into(),
    model: "mymod:penguin".into(),
    size: [0.5, 0.75],                     // width, height in blocks
    speed: 0.8,                            // blocks per second
    spawns_on: Some("core:snow".into()),   // appears by itself on snowy grass
    food: Some("core:wheat".into()),       // its favourite (apples work too); two fed ones have a baby
    sound: "pop".into(),
});

// Later, e.g. for a "/penguin" chat command:
if let Some(me) = guest::players().into_iter().find(|p| p.id == player) {
    guest::spawn_mob("mymod:penguin", me.position);
    guest::effect(EffectKind::Hearts, me.position, [255, 120, 160, 255]);
}

Random numbers

Mods can't ask the computer for random numbers, so make your own. This little generator is plenty for games:

/// A tiny random number generator (xorshift). Keep one in your mod state.
#[derive(Default)]
struct Rng(u32);

impl Rng {
    fn next(&mut self) -> u32 {
        if self.0 == 0 { self.0 = 0x9E37_79B9; }
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 17;
        self.0 ^= self.0 << 5;
        self.0
    }
    /// A number from 0 to n-1.
    fn below(&mut self, n: u32) -> u32 { self.next() % n }
}

// Mix in the tick count so each game is different:
// Event::Tick { tick, .. } => self.rng.0 ^= tick as u32,

7. Making a game mode

A game mode is a mod that turns itself on only in worlds made for it. Each world can have one game mode, chosen when it's created and saved with the world. The mod sees it in Init:

const MODE: &str = "treasure_hunt";

Event::Init { game_mode, .. } => {
    register_content(); // always, so block ids are the same in every world
    self.active = game_mode.as_deref() == Some(MODE);
    EventReply::Ok
}
_ if !self.active => EventReply::Ok,
Event::Start => {
    // The world is ready: build the arena, hide the treasure...
    EventReply::Ok
}

Start a world with your mode from the command line:

cargo run --release -- --world "Treasure Island" --mode treasure_hunt

# or a dedicated server
cargo run --release -p ourcraft_server --bin ourcraft-server -- \
    --world hunt --mode treasure_hunt

Ideas for a game mode, and how you'd make them:

For a full example, with teams, classes, flags, health, respawning and a scoreboard, read Splat Squad's source.

8. API reference

Everything here comes from ourcraft_mod_api (lib.rs and guest.rs), which has more comments. Run cargo doc -p ourcraft_mod_api --open in the game folder to browse it.

Events

EventWhenWhat it's for
Init { api_version, game_mode }Once, when the world opensRegister blocks, textures, models and tools. The only time you can.
StartOnce, when the world is readyBuild arenas, set things up. world_spawn() works here.
Tick { tick, dt }20 times a secondTimers, game rules, moving things.
PlayerJoined { player, name }Someone joinsGreet them, give tools, pick a team.
PlayerLeft { player }Someone leavesForget about them.
BlockChange { player, pos, old, new }A player tries to break or placeReply Deny to stop it (the block goes back).
Chat { player, text }A chat messageReply Handled if it was your command.
UseTool { player, tool, origin, direction, secondary }A player clicks with your toolFire, swing, build... secondary is the right button / tap.
ProjectileHit { entity, owner, hit }A projectile hit a block or playerSplash, score, place blocks. Check it's yours!

Every mod receives every event. Mods take turns in alphabetical order. For Chat and BlockChange, the first mod to reply Handled or Deny decides, and the mods after it don't see the event.

Functions

All in ourcraft_mod_api::guest. to means Some(player) for one player or None for everyone.

guest:: functionWhat it does
log(text)Write a line in the game's log, as [your_mod] text.
register_block(BlockSpec)A new block. Init only.
register_texture(name, rgba)A 16ร—16 texture (1024 bytes). Init only.
register_model(ModelSpec)A shape made of coloured boxes, for entities. Init only.
register_tool(ToolSpec)An item for the hotbar. Init only.
register_recipe(RecipeSpec)A crafting recipe for survival. Init only.
register_item(ItemSpec)An item that isn't a block (register its texture first). Init only.
register_mob(MobSpec)A new kind of animal (register its model first). Init only.
spawn_mob(name, position) โ†’ Option<EntityId>Put an animal in the world, e.g. "core:pig". It's kept: saved with the world, so don't spawn the same ones every time the world opens.
block_id(name) โ†’ Option<BlockId>Look up a block by name, e.g. "core:stone".
get_block(pos) โ†’ BlockIdWhat's at a position (0 = air, or not loaded).
set_block(pos, id)Change a block. Everyone nearby sees it.
try_set_block(pos, id) โ†’ boolSame, but tells you if it worked.
ground_height(x, z) โ†’ Option<i32>The y of the highest solid block.
raycast(origin, dir, max, hit_players, ignore) โ†’ Option<Hit>What a straight line hits first.
world_spawn() โ†’ Option<BlockPos>Where new players appear. Always the same for a world.
time_of_day() โ†’ f32, set_time_of_day(t)0 = midnight, 0.25 = sunrise, 0.5 = noon.
players() โ†’ Vec<PlayerInfo>Everyone online: id, name, position, velocity, look, on_ground.
chat(to, text)A chat message to one player (Some(id)) or everyone (None).
teleport(player, position)Move a player (feet position).
set_rules(player, PlayerRules)Speed, flying, building, shirt colour, name tag.
set_health(player, health, max)Show a health bar (max 0 hides it). The number is yours to manage.
set_hud(to, Vec<HudLine>)Replace the text panel in the top right.
show_title(to, text, seconds)A big message in the middle of the screen.
set_tools(player, Vec<String>)Fill the hotbar with your tools; an empty list gives blocks back.
spawn_entity(EntitySpec) โ†’ Option<EntityId>Something that floats or flies, drawn with a model.
move_entity(id, position), despawn_entity(id)Move or remove an entity.
effect(kind, position, color)Particles: Splat, Confetti, Poof or Hearts.
sound(name, at)A built-in sound at a place, or for everyone with None.
hud_line(text) โ†’ HudLineA white HUD line (helper).

Types

TypeFields
BlockSpecname, display_name, render (Opaque / Cutout / Translucent), solid, breakable, light (0โ€“15), texture_top/_bottom/_side. BlockSpec::simple(name, display, texture) fills in the rest.
ToolSpecname, display_name, icon (16ร—16 RGBA), cooldown (seconds).
ModelSpecname, boxes (each ModelBox { min, max, color }, in blocks around the origin), spin.
EntitySpecmodel, position, velocity, motion (Fixed or Projectile { gravity, radius, hits_players }), lifetime (0 = forever), owner.
ItemSpecname, display_name, texture (a registered texture), max_stack (1โ€“64), food and heals (out of 20), fuel (seconds), smelts_into. ItemSpec::simple(name, display, texture) fills in the rest.
RecipeSpecShaped { pattern, key, result, count }: pattern is up to 3 rows of up to 3 characters (' ' = empty), key says which item each character is. Shapeless { ingredients, result, count }. Items are named like blocks.
MobSpecname, display_name, model, size ([width, height]), speed, spawns_on (a block name, or None), food (an item name, or None), sound.
PlayerRulesspeed (1.0 normal), can_fly, can_build, color, tag. PlayerRules::default() is a normal player.
PlayerInfoid, name, position (feet), velocity, look (direction), on_ground.
HitBlock { pos, normal } (normal points out of the face that was hit) or Player { id, at }.
HudLinetext, color.
Small typesBlockPos = [i32; 3], Vec3 = [f32; 3], Rgba = [u8; 4], BlockId = u16, PlayerId and EntityId = u64.
Helperspixel_art(&rows, &palette) draws a texture from 16 strings; speckled(color, seed) makes a one-colour texture with a light speckle.

Built-in blocks, items, animals and sounds

Blocks: core:air, core:stone, core:dirt, core:grass, core:sand, core:log, core:leaves, core:planks, core:snow, core:cactus, core:bedrock, core:gravel, core:cobblestone, core:glass, core:bricks, core:lamp, core:water, core:lava, core:clay, core:wool, core:coal_ore, core:iron_ore, core:gold_ore, core:diamond_ore, core:iron_block, core:gold_block, core:diamond_block, core:crafting_table, core:furnace, core:chest, core:torch, core:door, core:trapdoor, core:fence, core:ladder, core:sign, core:bed, core:fence_gate, core:farmland, core:pumpkin, core:melon, core:pumpkin_lantern, core:sugar_cane, core:tall_grass, core:red_mushroom, core:brown_mushroom, core:cake, core:composter, core:planks_stairs, core:planks_slab, core:cobblestone_stairs, core:cobblestone_slab, core:stone_stairs, core:stone_slab, core:bricks_stairs, core:bricks_slab (core:snow is snowy grass). Paint Blocks adds paint:red, paint:orange, paint:yellow, paint:lime, paint:blue, paint:purple, paint:pink, paint:white and paint:rainbow_lamp.

Items (for recipes and food): every block above, plus core:stick, core:coal, core:iron_ingot, core:gold_ingot, core:diamond, core:apple, core:golden_apple, core:egg, core:fried_egg, core:brick, core:clay_ball, core:shears, core:bucket, core:water_bucket, core:lava_bucket, core:milk_bucket, core:wheat, core:wheat_seeds, core:carrot, core:potato, core:baked_potato, core:beetroot, core:beetroot_seeds, core:pumpkin_seeds, core:melon_seeds, core:melon_slice, core:sugar, core:paper, core:bread, core:cookie, core:pumpkin_pie, core:bowl, core:beetroot_soup, core:mushroom_stew, core:bone_meal, core:wood_hoe, core:wood_pickaxe, core:stone_axe, core:iron_shovel, core:diamond_pickaxe (tools are core: + wood, stone, iron or diamond + _pickaxe, _axe or _shovel).

Colours: white, light_gray, gray, black, brown, red, orange, yellow, lime, green, cyan, light_blue, blue, purple, magenta, pink. Each has core:<colour>_dye, _wool (white is core:wool), _bed (red is core:bed), _carpet, _stained_glass and _clay_tiles. Flowers: core:dandelion, core:poppy, core:cornflower, core:daisy, core:pink_tulip, core:orange_tulip, core:allium, core:forget_me_not.

Woods: oak (its blocks keep the old names core:log, core:planks, core:leaves, core:fence, core:door...), birch, pine, jungle, cherry, dark: core:<wood>_log, _planks, _leaves, _sapling, _stairs, _slab, _fence, _fence_gate, _door, _trapdoor, and a core:<wood>_boat item. Stone: core:stone_bricks, core:sandstone, core:granite, core:diorite, core:andesite, core:deep_slate, core:obsidian, core:ice and more, with walls like core:cobblestone_wall. In recipes, #planks, #logs, #wool and #leaves mean any of that kind.

Animals: core:sheep, core:pig, core:cow, core:chicken.

Sounds: place, break, pop, splat, grab, cheer, click, baa, moo, oink, cluck, munch, bonk, boing.

9. Rules and limits

10. Testing and debugging

11. Sharing your mod

A mod is a single file: mods/dist/your_mod.wasm. Send it to a friend, and they put it in the mods folder next to their worlds folder (create it if it isn't there):

ComputerMods folder
macOS~/Library/Application Support/Ourcraft/mods
Windows%APPDATA%\Ourcraft\mods
Linux~/.local/share/ourcraft/mods
Dedicated servermods next to where it runs, or --mods FOLDER

Mods are loaded when a world opens. A file in the mods folder replaces a bundled mod with the same name, which is handy for trying changes to Paint Blocks or Splat Squad.

iPad and Android can't easily add files, so they use the mods bundled into the game: put your .wasm in mods/dist/ and rebuild the app. When an iPad joins a computer's world, though, it gets that world's mods automatically.

Only the person hosting the world (or the server) needs the mod. Friends who join get everything they need to see your blocks and tools.

12. Troubleshooting

"Mod 'x' wasn't loaded: it was made for mod API version..."

The game was updated. Run ./build.sh again.

"...it needs `env.something`, which mods aren't allowed to use"

Something in your mod (or a library it uses) wants a file, the clock or random numbers. Remove that code or library. Libraries that use time, files or system random numbers (like rand) won't build or won't load; use the random number recipe.

"Mod 'x' failed" / "switched off after 3 failures"

The mod crashed or ran out of time. Look for unwrap(), indexing past the end of a list, dividing by zero, or a loop that never ends. Add guest::log lines to find where.

My block isn't in the inventory

It has to be registered while handling Event::Init. Check the terminal for "content can only be registered during Init" and that the mod loaded at all.

set_block does nothing

That part of the world isn't loaded (it's too far from every player). Use try_set_block to find out, and build near players.

My command shows up in chat

Return EventReply::Handled for it. Also check the spelling: it's compared exactly, so trim spaces with text.trim().

My mod reacts to other mods' projectiles

Remember the ids that spawn_entity gives you and check ProjectileHit's entity against them, as in step 4.

13. Other languages

Rust is the easiest way, because the API is a Rust crate. But a mod is just a WebAssembly module, so any language that compiles to WebAssembly can make one, with a bit more work. The contract:

exports:
  memory
  ourcraft_api_version() -> u32            // must return API_VERSION (1)
  ourcraft_alloc(len: u32) -> u32          // give the host a buffer of len bytes
  ourcraft_event(ptr: u32, len: u32) -> u64  // handle an Event, return an EventReply

import (the only one allowed):
  ourcraft.call(ptr: u32, len: u32) -> u64 // send a HostCall, get a HostReply

Messages are postcard-encoded. A u64 result packs a buffer as (ptr << 32) | len.

The message types are the enums Event, EventReply, HostCall and HostReply in crates/mod_api/src/lib.rs, encoded with postcard's wire format. New features are only ever added at the end of the enums, so old mods keep working.