指南
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、名称和版本:
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"如何把其余部分拆分到文件夹中,以及原因:见组织模组。
值与变量
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绿色"。
控制流
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 也安全
}函数
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() 会取消它。
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请友善发言")
}
}命令
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 的内容就是回复。如果缺少参数或参数类型错误,玩家会看到正确的用法。
时间
every 5 minutes { broadcast("&6记得投票") }
after 10 seconds { log("服务器已运行 10 秒") }大型模组
可选类型
写了类型的地方会强制检查;没写的地方代码保持自由。即使一个类型都不写,检查器也了解整个 API 的类型:
on join {
player.helth = 20 // 拼写错误
player.name = "Admin" // 只读属性
player.give() // 缺少参数
}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, "很多") → 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 检查之后,检查器就知道该值存在:
command home(target: player?) {
if target == null { return "请指定一名玩家" }
target.teleport(world().block_at(0, 80, 0).location) // 这里 target 已不再是 null
}类
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) 用于检查类型。
枚举
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模块
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 参考。