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:
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
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
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
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.
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
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
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:
on join {
player.helth = 20 // typo
player.name = "Admin" // read-only property
player.give() // missing argument
}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 0fun 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 stringTypes: 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:
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
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
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") → KINGModules
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.craftdoes the same from the terminal, following imports. - In development mode (
-Dcrafting.dev=true, always on incrafting 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.