指南

CraftScript

Crafting 自己的语言。英文关键字,语法简短,无需编译:保存文件,模组就会重新加载。无论是十行的模组,还是带有类和类型的四十个文件的模组,它都能胜任。

现在能做什么

领域 写法
物品 item "ruby" { … }:稀有度、附魔光效、食物、工具、耐久度、纹理、创造模式物品栏、使用动作。
方块 block "ruby_ore" { … }:硬度、爆炸抗性、亮度、声音、工具及最低等级、掉落物、摩擦力、透明度、使用动作。形状与模型见方块形状。楼梯、门、栅栏、箱子、告示牌、作物、矿石以及游戏的其他种类,还有座位和两格高的方块:见方块种类。方块还可以对踩踏和刻做出反应、成为传送门、拥有状态、使用红石并存放物品:参见方块、物品与配方。
世界生成 ore、tree、patch、structure、biome:出现在新区块中的内容;terrain、biome_map、carver 以及带 plan/build 的结构:用代码生成的地形、生物群系、洞穴和建筑。见世界生成和用代码生成。
生物 creature "ghost" { … }:借用游戏中某个生物的身体,配上你的纹理或你自己的 3D 模型、属性、行为、攻击目标和战利品,还有自然生成、驯服、繁殖、骑乘、远程攻击和关键帧动画。见生物。
声音与粒子 sound "chime" { file: … }、particle "spark" { textures: … };通过 id 使用。见声音与粒子。
配方 recipe shaped、shapeless、smelting。模型、战利品、标签和翻译会自动生成。
事件 on join、on break(e)……所有事件,支持优先级和取消。
护甲、武器、冷却 armor: { slot, defense, texture } 用于穿戴,on_hit 在击打时运行,cooldown 用于两次使用之间。参见方块、物品与配方。
效果、属性、装备 e.add_effect(…)、e.attribute(…)、e.set_attribute(…)、e.set_equipment(…)。参见世界与玩家。
按键 key "dash" { label: "Dash", default: "R" } 和 on key(e);实体事件 on interact_entity 和 on attack。参见界面和事件。
命令 command pay(to: player, amount: int) requires "eco.pay",支持别名、描述和权限。
时间 every 5 minutes { }、after 10 seconds { }、on tick。
世界与玩家 带状态的方块、实体、爆炸、粒子、声音、游戏时间、箱子、告示牌;物品栏、生命值、标题、传送。见世界与玩家。
界面 form(…) 和 player.show_form、server.boss_bar、player.set_sidebar。见界面。
存档数据 p.data.set("rubies.visits", n)、p.data.get_int("rubies.visits"):可用于玩家、实体、方块和世界;重启后依然保留。见世界与玩家。
设置与网络 config("settings", { … })、listen、send、send_to_server。见配置与网络。
大型模组 可选类型、支持继承的类、枚举、模块、常量。

仅限 Kotlin:后台任务、Java 库,以及在世界生成中用代码创建的地物和标签。

文件

模组可以是单个文件(mods/gems.craft),或者在变大后更推荐的形式:一个包含 main.craft 及其导入文件的文件夹。main.craft 以 mod 头开始,给出 id、名称和版本:

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"

如何把其余部分拆分到文件夹中,以及原因:见组织模组。

值与变量

lib/values.craftCraftScript
let coins = 100
const MAX_HOMES = 3                          // const:不能重新赋值
let greeting = "你有 {coins * 2} 枚金币"
let names = ["Ana", "Ben"]
let prices = { gold: 5, ruby: 2 }

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

数字(始终是十进制)、字符串、true/false、null、列表和映射。双引号会插值 {expression};\{ 输出一个字面意义的花括号。单引号不插值。聊天颜色用 & 书写:"&a绿色"。

控制流

lib/flow.craftCraftScript
fun describe(coins: number, banned: bool): string {
    if coins > 100 and not banned {
        return "富有"
    } else if coins == 0 {
        return "破产"
    }
    return "还行"
}

fun countdown() {
    for i in 1..5 { broadcast("{6 - i}…") }    // 包含两端的范围
    for p in players() { p.send("开始!") }
}

fun who(target: Player?, fallback: Player): string {
    let p = target ?? fallback                  // ??:为 null 时取右侧的值
    return p?.name ?? "没有人"                  // ?.:遇到 null 也安全
}

函数

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 })
}

支持带默认值的参数、return,以及用 fun(x) { … } 写的匿名函数,把函数传给 map、filter 或 on_use 就靠它。

事件

在 on 内部,事件的属性就是变量(player、message、block……),你也可以给事件命名:on chat(e)。如果事件可取消,e.cancel() 会取消它。

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("&c请友善发言")
    }
}

命令

commands/heal.craftCraftScript
command heal(target: player?, amount: number?) requires "rubies.heal" alias "h" describe "治疗一名玩家" {
    let p = target ?? player
    if p == null { return "请指定一名玩家" }
    p.health = min(p.max_health, p.health + (amount ?? 20))
    return "已治疗 {p.name}"
}

在命令内部可以使用 sender、player(由控制台执行时为 null)和 args。你 return 的内容就是回复。如果缺少参数或参数类型错误,玩家会看到正确的用法。

时间

events/reminders.craftCraftScript
every 5 minutes { broadcast("&6记得投票") }

after 10 seconds { log("服务器已运行 10 秒") }

大型模组

可选类型

写了类型的地方会强制检查;没写的地方代码保持自由。即使一个类型都不写,检查器也了解整个 API 的类型:

events/welcome.craftCraftScript
on join {
    player.helth = 20          // 拼写错误
    player.name = "Admin"      // 只读属性
    player.give()              // 缺少参数
}
crafting check文本
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, "很多")  →  error: Argument 'amount' of heal() must be number, got string

类型:number、string、bool、id、list<T>、map<T>、T?(可能为 null)、any、void、fun、API 类型(Player、World、Block、Entity、ItemStack、Location、Inventory……)、事件,以及你自己的类和枚举。经过 null 检查之后,检查器就知道该值存在:

commands/home.craftCraftScript
command home(target: player?) {
    if target == null { return "请指定一名玩家" }
    target.teleport(world().block_at(0, 80, 0).location)   // 这里 target 已不再是 null
}

类

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)      // VIP 只付一半
    }
}

给类未声明的字段赋值会报错(这样拼写错误就不会漏网)。如果类定义了 init(…),它就是构造函数;否则参数按顺序填充字段。is(value, Class) 用于检查类型。

枚举

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

模块

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_for(wallets[player.name].coins).name}")
}

文件中在任何代码块之外声明的东西都可以导入:函数、类、枚举、常量。即使被多个文件导入,每个文件也只运行一次。

检查、重载与安全

  • 模组加载时会检查所有内容:错误会阻止加载并指出位置;警告写入日志。crafting check main.craft 在终端中做同样的事,并会跟随导入。
  • 在开发模式下(-Dcrafting.dev=true,在 crafting sandbox 中始终开启),保存即重新加载模组;/crafting reload <id> 也可以。新物品或新方块需要重启游戏;其他改动立即生效。
  • 脚本只能看到 Crafting API:它不能读取文件、不能建立连接,也不能使用 Java 类。
  • 每个事件、命令或计时器最多执行 2,000,000 步:死循环会被报错中断,而不会卡死服务器。
  • 运行时错误会连同文件和行号记录到日志,服务器照常运行。

所有函数、运算符和成员:见CraftScript 参考。