Guides

CraftScript

Crafting's language. English keywords, short syntax, no compiling: you save the file and the mod reloads. It works for a ten-line mod and for a forty-file one with classes and types.

What you can do today

Area How
Items item "ruby" { … }: rarity, glint, food, tool, durability, texture, creative tab, use action.
Blocks block "ruby_ore" { … }: hardness, resistance, light, sound, tool and minimum tier, drops, friction, transparency, use action. Shapes and models: see Block shapes. Stairs, doors, fences, chests, signs, crops, ores and the game's other kinds, seats and blocks two high: see Block kinds. Blocks can also react to steps and ticks, be portals, have states, use redstone and hold items: see Blocks, items and recipes.
World generation ore, tree, patch, structure, biome: what appears in new chunks; terrain, biome_map, carver and structures with plan/build for land, biomes, caves and buildings made by code. See World generation and Generation by code.
Creatures creature "ghost" { … }: the body of a game creature with your texture or your own 3D model, stats, behaviour, targets and loot, plus natural spawning, taming, breeding, riding, ranged attacks and keyframed animations. See Creatures.
Sounds and particles sound "chime" { file: … }, particle "spark" { textures: … }; used by id. See Sounds and particles.
Recipes recipe shaped, shapeless, smelting. Models, loot, tags and translations are generated for you.
Events on join, on break(e)… every event, with priority and cancellation.
Armor, weapons, cooldowns armor: { slot, defense, texture } to wear it, on_hit when it hits, cooldown between uses. See Blocks, items and recipes.
Effects, attributes, equipment e.add_effect(…), e.attribute(…), e.set_attribute(…), e.set_equipment(…). See The world and players.
Keys key "dash" { label: "Dash", default: "R" } and on key(e); entity events on interact_entity and on attack. See Interface and Events.
Commands command pay(to: player, amount: int) requires "eco.pay", with aliases, description and permissions.
Time every 5 minutes { }, after 10 seconds { }, on tick.
World and players Blocks with states, entities, explosions, particles, sounds, time of day, chests, signs; inventory, health, titles, teleporting. See The world and players.
Interface form(…) and player.show_form, server.boss_bar, player.set_sidebar. See Interface.
Saved data p.data.set("rubies.visits", n), p.data.get_int("rubies.visits"): on players, entities, blocks and worlds; survives a restart. See The world and players.
Settings and networking config("settings", { … }), listen, send, send_to_server. See Config and networking.
Large mods Optional types, classes with inheritance, enums, modules, constants.

Kotlin only: background work, Java libraries, and, in world generation, features and tags made by code.

Files

A mod is a single file (mods/gems.craft) or, better once it grows, a folder with main.craft and the files it imports. main.craft starts with the mod header, which gives the id, name and version:

mods/rubies/main.craftCraftScript
mod "rubies" { name: "Rubies", version: "1.0.0", author: "you" }

// What exists
import "content/items"
import "content/ores"
import "content/world"
import "content/dimensions"
import "content/creatures"
import "content/sounds"
import "content/particles"
import "content/recipes"

// What happens
import "events/mining"
import "events/hunting"
import "events/welcome"
import "commands/realm"
import "commands/rubies"
import "commands/shop"

How to split the rest into folders, and why: Organizing a mod.

Values and variables

lib/values.craftCraftScript
let coins = 100
const MAX_HOMES = 3                          // const: cannot be reassigned
let greeting = "You have {coins * 2} coins"
let names = ["Ana", "Ben"]
let prices = { gold: 5, ruby: 2 }

names.add("Cleo")
prices["diamond"] = 9
log(len(names), prices.gold, prices.keys())

Numbers (always decimal), strings, true/false, null, lists and maps. Double quotes interpolate {expression}; \{ writes a literal brace. Single quotes don't interpolate. Chat colors are written with &: "&aGreen".

Control flow

lib/flow.craftCraftScript
fun describe(coins: number, banned: bool): string {
    if coins > 100 and not banned {
        return "rich"
    } else if coins == 0 {
        return "broke"
    }
    return "fine"
}

fun countdown() {
    for i in 1..5 { broadcast("{6 - i}…") }    // inclusive range
    for p in players() { p.send("Go!") }
}

fun who(target: Player?, fallback: Player): string {
    let p = target ?? fallback                  // ?? : value if null
    return p?.name ?? "nobody"                  // ?. : safe on null
}

Functions

lib/rewards.craftCraftScript
fun give_rubies(p: Player, amount: number = 1) {
    p.give(item("rubies:ruby", amount))
}

fun healthy(): list<Player> {
    return players().filter(fun(p) { return p.health > 10 })
}

Parameters with default values, return, and anonymous functions with fun(x) { … }, which is how you pass them to map, filter or on_use.

Events

Inside on, the event's properties are variables (player, message, block…), or you can name the event: on chat(e). e.cancel() cancels it if it's cancellable.

events/chat.craftCraftScript
const BLOCKED = ["spam", "scam"]

on chat(e) priority high {
    if BLOCKED.any(fun(w) { return e.message.contains(w) }) {
        e.cancel()
        e.player.send("&cPlease keep it friendly")
    }
}

Commands

commands/heal.craftCraftScript
command heal(target: player?, amount: number?) requires "rubies.heal" alias "h" describe "Heal a player" {
    let p = target ?? player
    if p == null { return "Name a player" }
    p.health = min(p.max_health, p.health + (amount ?? 20))
    return "Healed {p.name}"
}

Inside a command you have sender, player (or null if the console runs it) and args. Whatever you return is the reply. If an argument is missing or has the wrong type, the player sees the correct usage.

Time

events/reminders.craftCraftScript
every 5 minutes { broadcast("&6Remember to vote") }

after 10 seconds { log("the server has been up for 10 seconds") }

Large mods

Optional types

Where you write a type, it's enforced; where you don't, the code stays free. Even if you write none, the checker knows the types of the whole API:

events/welcome.craftCraftScript
on join {
    player.helth = 20          // typo
    player.name = "Admin"      // read-only property
    player.give()              // missing argument
}
crafting checkText
welcome.craft:2: error: Player has no property 'helth'. Did you mean 'health'?
welcome.craft:3: error: Player.name is read-only
welcome.craft:4: error: give() expects 1 argument(s), got 0
lib/heal.craftCraftScript
fun heal(p: Player, amount: number = 20): bool {
    p.health = min(p.max_health, p.health + amount)
    return true
}

// heal(player, "a lot")  →  error: Argument 'amount' of heal() must be number, got string

Types: number, string, bool, id, list<T>, map<T>, T? (may be null), any, void, fun, the API types (Player, World, Block, Entity, ItemStack, Location, Inventory…), events, and your own classes and enums. After a null check, the checker knows the value exists:

commands/home.craftCraftScript
command home(target: player?) {
    if target == null { return "Name a player" }
    target.teleport(world().block_at(0, 80, 0).location)   // target is no longer null here
}

Classes

lib/wallet.craftCraftScript
class Wallet {
    coins: number = 100

    fun pay(n: number): bool {
        if n > this.coins { return false }
        this.coins -= n
        return true
    }
}

class VipWallet extends Wallet {
    fun pay(n: number): bool {
        return super.pay(n / 2)      // VIPs pay half
    }
}

Assigning a field the class doesn't declare is an error (so typos don't slip through). If the class defines init(…), that's the constructor; if not, the arguments fill the fields in order. is(value, Class) checks the type.

Enums

lib/rank.craftCraftScript
enum Rank { PEASANT, KNIGHT, LORD, KING }

fun rank_for(coins: number): Rank {
    if coins >= 1000 { return Rank.LORD }
    return Rank.PEASANT
}

// Rank.LORD.name → "LORD", Rank.values() → [PEASANT, …], Rank.from("king") → KING

Modules

events/economy.craftCraftScript
import { VipWallet } from "/lib/wallet"
import { rank_for } from "/lib/rank"

let wallets = {}

on join {
    wallets[player.name] = VipWallet()
    player.send("Rank: {rank_for(wallets[player.name].coins).name}")
}

Everything a file declares outside any block is importable: functions, classes, enums, constants. Each file runs only once even if several files import it.

Checking, reloading and safety

  • Everything is checked when the mod loads: an error stops it from loading and says where; a warning goes to the log. crafting check main.craft does the same from the terminal, following imports.
  • In development mode (-Dcrafting.dev=true, always on in crafting sandbox) saving reloads the mod; so does /crafting reload <id>. A new item or block needs a game restart; everything else changes immediately.
  • A script only sees the Crafting API: it can't read files, open connections or use Java classes.
  • Each event, command or timer has a limit of 2,000,000 steps: an infinite loop is cut off with an error instead of freezing the server.
  • A runtime error is logged with the file and line, and the server keeps running.

Every function, operator and member: CraftScript reference.