KubeJS 配置文件与数据储存¶
原文:原创教程(ぴよまる),API 依据 KubeJS 6.1(1.20.1)源码与官方 Wiki
写整合包脚本,最常遇到的问题就是:设置项放哪?数据存哪? 这篇把 KubeJS 的「配置文件」和「数据储存」两大块讲清楚——配置文件管「玩家/服主能改的设置」,数据储存管「脚本运行时的存档」,各有各的存放位置和读写方式。
1. 配置文件总览(kubejs/config/)¶
KubeJS 的配置都放在 kubejs/config/ 文件夹里,游戏启动时自动生成/读取:
| 文件 | 作用 |
|---|---|
common.properties |
KubeJS 主配置(packmode 包模式、debug 等) |
defaultoptions.txt |
默认游戏选项(详见本站「默认选项」篇) |
dev.properties(可选) |
开发者调试配置(日志开关等,默认在 local/kubejsdev.properties) |
你自己建的 .json / .properties |
自定义配置,用 JsonIO 读写(见下文) |
1.1 common.properties:packmode(包模式)¶
packmode 是 KubeJS 最有用的配置——同一份脚本可以按「包模式」跑不同逻辑(比如单人/服务器模式):
// 脚本里读取当前包模式,按模式切换行为
ServerEvents.recipes(event => { // 监听"配方"事件
if (global.packmode === 'expert') { // 如果包模式是 expert(专家模式)
event.remove({ output: 'minecraft:diamond' }) // 删掉钻石配方(专家模式不给钻石)
}
})
// 全局读取:packmode 会注入到全局变量 global.packmode
console.log('当前包模式:' + global.packmode) // 打印到 logs/server.log
💡 切换模式不用改脚本:改
common.properties里的packmode=后执行/kubejs reload config即可(KubeJS 6.1 新增命令,不用重启游戏)。
1.2 常见配置项(common.properties)¶
| 配置项 | 作用 |
|---|---|
packmode |
包模式(default / expert / 任意自定义值) |
debugInfo |
调试信息开关(6.1 起挪到 local/kubejsdev.properties) |
saveDevPropertiesInConfig |
设为 true 时把开发者配置存到 kubejs/config/dev.properties 而非 local/ |
serverOnly |
是否仅服务端模式(减少客户端加载) |
2. 数据储存四种方式(怎么选)¶
| 方式 | 存哪 | 重启丢不丢 | 适合存什么 |
|---|---|---|---|
player.persistentData |
玩家 NBT | ❌ 不丢 | 每个玩家自己的数据(等级、金钱、家坐标) |
entity/block.persistentData |
实体/方块 NBT | ❌ 不丢 | 实体状态、方块实体数据 |
server.getData() / level.getData() |
内存(AttachedData) | ✅ 重启丢 | 临时数据、跨脚本共享变量 |
JsonIO / NBTIO 读写文件 |
kubejs/ 下 JSON/NBT 文件 |
❌ 不丢 | 自定义配置、全局存档、跨重启数据 |
💡 记忆口诀:跟「人/物」走用
persistentData;跟「服务器/世界」走用getData()(临时)或 JsonIO 写文件(持久)。
3. persistentData(玩家/实体/方块持久数据)¶
挂在实际对象上,服务器重启不丢,是 KubeJS 最常用的存档方式:
// 玩家数据:每个玩家一份,互不干扰
PlayerEvents.loggedIn(event => { // 监听"玩家登录"事件
let data = event.player.persistentData // 玩家的持久数据(对象)
data.joinCount = (data.joinCount || 0) + 1 // 登录次数 +1(没记录过就当 0)
event.player.tell('这是你第 ' + data.joinCount + ' 次登录!')
})
// 可以存数字/字符串/布尔/数组/对象,随便嵌套
event.player.persistentData.home = { x: 100, y: 64, z: 200, dim: 'minecraft:overworld' }
event.player.persistentData.balance = 500 // 存钱
event.player.persistentData.tags = ['vip', 'builder'] // 存数组
📖 详细用法(击杀计数/商店系统/传送点等完整案例)见本站「KubeJS 进阶实战」第 2 节和「NBT 数据操作」篇。
3.1 用命令查看/修改 persistentData(/kubejs persistent_data)¶
KubeJS 6.1 内置了一组命令,OP 权限,不写代码就能看/改持久数据(调试、手工修数据很有用):
/kubejs persistent_data server get * # 看服务器持久数据(全部)
/kubejs persistent_data server get <键> # 看某个键
/kubejs persistent_data server merge {键:值} # 合并写入(NBT 语法)
/kubejs persistent_data server remove <键> # 删某个键
/kubejs persistent_data server remove * # 清空
/kubejs persistent_data dimension <维度> get * # 某个维度的持久数据
/kubejs persistent_data dimension * get * # 所有维度
/kubejs persistent_data entity <目标> get * # 实体(含玩家)的持久数据
还支持和记分板互转(老服务器迁数据很好用):
/kubejs persistent_data entity @s scoreboard import <键> <记分板玩家> <目标>
/kubejs persistent_data entity @s scoreboard export <键> <记分板玩家> <目标>
💡 用途:改脚本测试时直接
merge造数据;玩家数据写坏了不用去改存档文件;把老记分板分数一次性搬进persistentData。
4. getData()(服务器/世界的附加数据)¶
server.getData() 和 level.getData() 返回一个 AttachedData(本质是 Map),适合脚本之间共享数据:
// server_scripts/global_data.js
ServerEvents.loaded(event => { // 监听"服务器加载完成"事件
let data = event.server.getData() // 服务器的附加数据(Map 对象)
data.serverName = '落英服' // 存一个全局值
data.startedAt = Date.now() // 记录启动时间
})
// 其他脚本里读取(同一个 server 对象共享)
PlayerEvents.loggedIn(event => {
let data = event.server.getData() // 同一个数据容器
event.player.tell('服务器名:' + data.serverName) // 读到 '落英服'
})
⚠️
getData()的数据在内存里,服务器重启会清空。要跨重启保存全局数据,用下面的 JsonIO 写文件。
5. JsonIO:读写 JSON 文件(自定义配置 + 全局存档)¶
最灵活的方式:把数据写成 JSON 文件放在 kubejs/ 目录下,想存啥存啥,重启不丢。
5.1 路径规则(先搞清往哪存)¶
JsonIO 的路径永远相对游戏根目录(不是相对某个脚本文件),也不能写绝对路径:
| 想存哪 | 路径写法 | 特点 |
|---|---|---|
| 配置 / 统计(推荐) | kubejs/config/xxx.json |
目录本来就存在,玩家也能看到、能手动改 |
| 想低调一点 | kubejs/data/xxx.json |
只是个普通文件夹(不是数据包的 data/),一般玩家不会去翻 |
| 跟存档走 | world/kubejs/xxx.json |
换/删存档数据就跟着变(多周目玩法可以考虑) |
⚠️ 两个必须注意的点
- 父目录必须已存在:KubeJS 只写文件、不会帮你建目录。写
kubejs/config/a.json没问题(config/本来就在);想写到kubejs/config/rank/a.json这种新子目录,得先自己把rank/文件夹建好,否则写入会失败(报NoSuchFileException之类的错)。- 文件名用英文:
rank.json/player-stats.json这样写,别用中文或空格,跨系统更安全、出问题也好搜。
5.2 写入:JsonIO.write(整文件覆盖)¶
// server_scripts/save_data.js
ServerEvents.loaded(event => { // 服务器加载时执行
// JsonIO.write(相对游戏目录的路径, 数据对象)
// 注意:路径从游戏根目录算起,kubejs/ 开头
JsonIO.write('kubejs/config/mydata.json', {
welcome: '欢迎来到落英服!', // 字符串
price: 100, // 数字
enabled: true, // 布尔
rewards: ['diamond', 'emerald'] // 数组
})
console.log('配置已写入 kubejs/config/mydata.json')
})
写入的源码级行为(KubeJS 6.1,照着实测照着看就不容易翻车):
| 行为 | 说明 |
|---|---|
| 整文件覆盖 | 不是「追加」,旧内容全没了(下一节讲怎么保留旧数据) |
传 null |
JsonIO.write('xxx.json', null) = 删除这个文件(不是写一个 null 进去) |
| 缩进 | 用 Tab 缩进,文件可读性好 |
| null 字段 | 会原样写出来({ a: null } → "a": null) |
| 不支持的值 | 函数、undefined(字段直接消失)、循环引用(报错) |
| 中文 | 按 UTF-8 直写,文件里能直接看到中文(不会变成 \u4e2d\u6587) |
5.3 读取:JsonIO.read(文件不存在返回 null)¶
// server_scripts/read_data.js
PlayerEvents.loggedIn(event => { // 玩家登录时读取配置
let data = JsonIO.read('kubejs/config/mydata.json') // 读 JSON 文件,返回对象
if (!data) { // 文件不存在(第一次运行)返回 null
event.player.tell('还没有配置文件')
return
}
event.player.tell(data.welcome) // 读字符串
event.player.tell('价格是:' + data.price) // 读数字
console.log(data.rewards) // 读数组
})
读到的就是普通 JS 对象,直接用 .字段 访问;文件不存在时返回 null(不是空对象,所以判断用 if (!data)):
let data = JsonIO.read('kubejs/config/mydata.json') // 对象 或 null
let price = data && data.price ? data.price : 100 // 常见写法:读不到就用默认值
5.4 完整案例:全局计数器(服务器总击杀数,重启不丢)¶
// server_scripts/kill_counter_global.js
// 记录服务器历史上所有玩家的总击杀数,重启不丢
function loadKills() { // 定义函数:读取存档
let data = JsonIO.read('kubejs/config/total_kills.json') // 读 JSON
return data && data.total ? data.total : 0 // 没有就返回 0
}
function saveKills(total) { // 定义函数:写入存档
JsonIO.write('kubejs/config/total_kills.json', { total: total }) // 写 JSON
}
EntityEvents.death(event => { // 监听"实体死亡"事件
let player = event.source.player // 凶手玩家
if (!player) return // 不是玩家杀的就算了
let total = loadKills() + 1 // 总击杀数 +1
saveKills(total) // 存回去
console.log('服务器总击杀数:' + total) // 打日志
})
💡 这个文件玩家也能看到(在 kubejs/config/ 里),适合存公开的全局数据;不想被看到的存档建议存
kubejs/data/或配合服务器端使用。
5.5 ⚠️ 追加/更新内容(重点:先读 → 改 → 写)¶
JsonIO.write 是整个文件覆盖——直接 JsonIO.write('xxx.json', 新对象) 会把旧数据全冲掉!想要「往里加内容」必须三步走:先 read 出来 → 改对象 → 再 write 回去。
例 1:往数组里追加一条记录(登录日志)
// server_scripts/login_log.js
PlayerEvents.loggedIn(event => { // 监听玩家登录
let data = JsonIO.read('kubejs/config/login_log.json') // ① 先读现有内容
if (!data || !data.logs) data = { logs: [] } // 文件不存在 / 没有 logs 字段 → 初始化为空数组
data.logs.push({ // ② 往数组末尾追加一条(push = 追加,不影响旧记录)
player: event.player.username, // 玩家名
time: Date.now() // 当前时间戳
})
JsonIO.write('kubejs/config/login_log.json', data) // ③ 整个对象写回(旧记录 + 新记录都在)
console.log('登录日志已追加,共 ' + data.logs.length + ' 条')
})
例 2:往对象里加/改某个字段(玩家签到天数,保留其他人)
// server_scripts/signin_json.js
PlayerEvents.chat(event => { // 监听聊天
if (event.message !== '!签到') return // 只响应签到指令
let data = JsonIO.read('kubejs/config/signin.json') || {} // ① 读,文件不存在就当空对象
let name = event.player.username // 当前玩家名
data[name] = (data[name] || 0) + 1 // ② 给这个玩家键 +1(其他玩家的键原样保留,不丢!)
JsonIO.write('kubejs/config/signin.json', data) // ③ 写回
event.player.tell('§a签到成功!你累计签了 ' + data[name] + ' 天')
})
💡 为什么能保留旧数据? 因为 write 写的是「读出来的整个对象」——你只改了其中一个字段/加了一条,其他字段原封不动跟着写回去了。千万别
JsonIO.write('xxx.json', { 只有新内容 }),那会把旧数据全删了。
例 3:排行榜(按分数排序存储)
// server_scripts/rank_json.js
EntityEvents.death(event => { // 击杀时加分
let player = event.source.player // 凶手玩家
if (!player) return
let data = JsonIO.read('kubejs/config/rank.json') || { scores: {} } // ① 读,初始化
let name = player.username
data.scores[name] = (data.scores[name] || 0) + 1 // ② 该玩家击杀数 +1
JsonIO.write('kubejs/config/rank.json', data) // ③ 写回
})
// 查看排行榜:!排行
PlayerEvents.chat(event => {
if (event.message !== '!排行') return
let data = JsonIO.read('kubejs/config/rank.json')
if (!data || !data.scores) { event.player.tell('§7还没有排行榜数据'); return }
// 按分数从高到低排序,取前 5 名
let top = Object.entries(data.scores).sort((a, b) => b[1] - a[1]).slice(0, 5)
event.player.tell('§6===== 击杀榜 TOP5 =====')
top.forEach((row, i) => event.player.tell('§e第' + (i + 1) + '名:' + row[0] + ' —— ' + row[1] + ' 杀'))
})
⚠️ 并发注意:多个脚本同时写同一个 JSON 文件可能互相覆盖(KubeJS 的 JsonIO 没有文件锁)。高频写入的全局数据建议统一由一个脚本入口写(见 5.8 的缓存方案),或改用
persistentData/ 数据库。
5.6 JsonIO 全部 API 速查¶
除了 read / write,JsonIO 还暴露了一堆好用的转换方法(脚本里直接 JsonIO.xxx 就能调):
| API | 作用 | 返回 |
|---|---|---|
JsonIO.read(路径) |
读 JSON 文件 → JS 对象 | 对象 / null(文件不存在) |
JsonIO.write(路径, 对象) |
写 JSON 文件(覆盖) | 无;传 null = 删除文件 |
JsonIO.readString(路径) |
整个文件当字符串读 | 字符串 |
JsonIO.readJson(路径) |
读成 JsonElement(不做 JS 包装) | JsonElement / null |
JsonIO.parse(字符串) |
JSON 字符串 → JS 对象 | 对象 |
JsonIO.parseRaw(字符串) |
JSON 字符串 → JsonElement | JsonElement |
JsonIO.toString(对象) |
对象 → JSON 字符串(压成一行) | 字符串 |
JsonIO.toPrettyString(对象) |
同上但带缩进美化(调试用) | 字符串 |
JsonIO.of(对象) |
JS 对象/Map/NBT/数组 → JsonElement | JsonElement |
JsonIO.toObject(JsonElement) |
JsonElement → JS 对象 | 对象 |
JsonIO.copy(json) |
深拷贝一份(改副本不影响原对象) | JsonElement |
JsonIO.getJsonHashString(json) |
取内容哈希(做缓存键 / 比对两份数据是否相同) | 字符串 |
JsonIO.toArray(element) |
单个值包成数组(已是数组则原样返回) | JsonArray |
实用例子:
// server_scripts/json_tips.js
PlayerEvents.loggedIn(event => {
// ① 把玩家持久数据导成一行 JSON(打日志 / 发给管理员 / 存进 NBT)
let json = JsonIO.toString(event.player.persistentData)
console.log('[导出] ' + event.player.username + ' → ' + json)
// ② 调试时美化输出(带缩进、人看得懂)
console.log(JsonIO.toPrettyString(event.player.persistentData))
// ③ 把字符串配置解析成对象(跟 JSON.parse 差不多,但走 KubeJS)
let cfg = JsonIO.parse('{"price":100,"enabled":true}')
event.player.tell('价格:' + cfg.price)
// ④ 比对两份数据是否完全相同(哈希一致 = 内容一致,做缓存校验很好用)
let a = JsonIO.read('kubejs/config/a.json')
let b = JsonIO.read('kubejs/config/b.json')
if (JsonIO.getJsonHashString(a) === JsonIO.getJsonHashString(b)) {
console.log('两份数据一模一样')
}
})
5.7 能存什么类型?数字精度怎么算?¶
| 类型 | 支持 | 说明 |
|---|---|---|
| 字符串 / 布尔 / null | ✅ | 直接存 |
| 数字 | ✅ | 底层按双精度浮点存:整数到 2⁵³ 都准;再大的数会丢精度 → 大 ID / 大额数字建议存字符串 |
| 数组 | ✅ | [1, 'a', true] 混合类型也行 |
| 嵌套对象 | ✅ | 随便套 |
undefined 字段 |
❌ | 写出去字段直接消失(想要空值就写 null) |
| 函数 / Java 对象 | ❌ | 会报错或写成奇怪的东西 → 先手动挑出需要的字段 |
| 循环引用 | ❌ | 对象里套自己会报错 |
💡 三个经验: 1. 时间存时间戳(
Date.now()数字),比存日期字符串好比较、好排序; 2. 有序列表用数组,按名字查用对象; 3. JSON 对象的键本来就是字符串,用数字当键(data[123])读回来会变'123'。
5.8 高频写入怎么办?内存缓存 + 定时落盘¶
JsonIO.read / write 每次都真的读写磁盘。放在 ServerEvents.tick(每 1/20 秒)里叫,会把 TPS 和硬盘/存储卡拖垮。
正确姿势:启动读一次 → 内存里改 → 定时/退出时写一次:
// server_scripts/stats_cached.js
// 高频统计的正确写法:内存缓存 + 定时落盘
const FILE = 'kubejs/config/stats.json'
let cache = null // 内存缓存
function get() { // 拿缓存;没有就读盘
if (!cache) cache = JsonIO.read(FILE) || {}
return cache
}
ServerEvents.loaded(event => { // ① 启动时预热(顺便建文件)
get()
console.log('[stats] 已载入,玩家数:' + Object.keys(cache).length)
})
PlayerEvents.loggedIn(event => { // ② 平时只改内存(快)
let d = get()
d[event.player.username] = (d[event.player.username] || 0) + 1
})
ServerEvents.tick(event => { // ③ 每 6000 tick(= 5 分钟)落盘一次
if (event.server.tickCount % 6000 === 0) {
JsonIO.write(FILE, get())
console.log('[stats] 已保存到 ' + FILE)
}
})
ServerEvents.unloaded(event => { // ④ 关服前再存一次(保险)
JsonIO.write(FILE, get())
})
| 写法 | 磁盘 I/O | 丢数据风险 |
|---|---|---|
每次改都 write |
巨大 | 几乎不丢 |
| 缓存 + 定时落盘(推荐) | 很小 | 最多丢一个周期(示例是 5 分钟) |
只靠 getData() 内存 |
无 | 重启全丢 |
⚠️ 服务器崩服时定时写还没来得及落盘 → 会丢最近一个周期的数据。很在意的数据(付费/重要进度)就改成「关键操作后立即写」。
5.9 案例:玩家 JSON 存档 + 结构版本迁移¶
JSON 存档最容易犯的错是「结构改了不兼容旧文件」。养成加 version 字段的习惯:
// server_scripts/players_json.js
const FILE = 'kubejs/config/players.json'
function load() {
let data = JsonIO.read(FILE) || { version: 1, players: {} }
if ((data.version || 1) < 2) { // 旧格式 → 新格式(老版本存的是纯数字,新版是对象)
data.players = data.players || {}
Object.keys(data.players).forEach(name => {
if (typeof data.players[name] === 'number') {
data.players[name] = { coins: data.players[name], level: 1 }
}
})
data.version = 2
console.log('[players] 数据已从 v1 升级到 v2')
}
return data
}
function save(data) {
JsonIO.write(FILE, data)
}
PlayerEvents.loggedIn(event => {
let data = load()
let name = event.player.username
data.players[name] = data.players[name] || { coins: 0, level: 1 }
save(data)
event.player.tell('§a欢迎回来!金币:' + data.players[name].coins)
})
💡 用用户名还是 UUID 当键? - 用户名:文件人类可读、好查好改,但玩家改名后找不到旧数据; - UUID(
event.player.uuid.toString()):改名不丢,但离线服/正版服 UUID 可能不同,也不好看; - 折中:键用 UUID,值里额外存一份name方便查看。
5.10 JSON ⇄ persistentData / NBT 互转¶
两边不是对立的,能互相导:
// ① persistentData(NBT)→ JSON 字符串 / 文件(备份玩家数据很好用)
let dump = JsonIO.toString(event.player.persistentData)
JsonIO.write('kubejs/config/backup_' + event.player.username + '.json',
JsonIO.of(event.player.persistentData))
// ② persistentData 直接存成 .nbt 压缩文件
NBTIO.write('kubejs/config/backup_' + event.player.username + '.nbt',
event.player.persistentData)
| 场景 | 推荐用什么 | 原因 |
|---|---|---|
| 玩家个人数据(要跟存档走) | persistentData |
自动跟随玩家/存档,不用管文件 |
| 全服统计 / 配置表 | JsonIO + JSON 文件 |
人类可读、可手改、能备份 |
| 复杂物品 / 实体结构 | NBTIO + .nbt |
完整保留 NBT 类型 |
| 脚本之间临时共享 | getData() |
内存 Map,快但重启丢 |
📖 persistentData 的详细类型规则(取数字/字符串/列表)见本站「NBT 数据操作」篇;本节只讲 JSON 这条路。
5.11 并发、备份与安全¶
| 风险 | 说明 | 对策 |
|---|---|---|
| 并发覆盖 | JsonIO 没有文件锁,两个脚本同时写同一文件 → 后写的赢 | 一个文件只让一个脚本负责写(统一入口) |
| 保存频率 | 每 tick 写盘拖 TPS、耗存储寿命 | 用 5.8 的缓存 + 定时落盘 |
| 文件坏了 | 写到一半断电/杀进程 → JSON 残缺,read 会抛错 |
定时备份 + 代码里 try/catch 兜底(读失败当空对象) |
| 隐私 | kubejs/config/ 里的东西玩家能翻(客户端本地也有同名目录) |
别放密钥/后台密码;敏感数据放 world/ 或服务器目录外 |
| 文件过大 | 几 MB 的 JSON 每次读/写都很慢 | 拆成多个小文件(按玩家/按功能分) |
// 读文件出错别让整个脚本崩:包一层 try/catch
let data = {}
try {
data = JsonIO.read('kubejs/config/maybe-broken.json') || {}
} catch (e) {
console.error('JSON 读取失败,按空数据处理:' + e)
}
💡 备份建议:这些 JSON 都是纯文本,用宝塔的任务或者一个外部脚本,每天把
kubejs/config/*.json复制一份到backup/就够(体积小、恢复简单)。
5.12 嵌套对象(多层 JSON)怎么写¶
JSON 想嵌套多少层都行,写进文件就是你想的那种样子:
① 结构固定:直接用 JS 对象写死
// server_scripts/nested_static.js
JsonIO.write('kubejs/config/nested.json', {
"11": {
"11": {
"值": 1
}
}
})
② 结构不固定(键是变量、层数也不定):用「路径数组」动态生成
自己写两个小工具函数,一个按路径写、一个按路径读(以后所有嵌套数据都能用):
// server_scripts/deep.js
// deepSet:按路径写入,中间缺的层自动建好
function deepSet(root, keys, value) {
let cur = root
for (let i = 0; i < keys.length - 1; i++) {
let k = keys[i]
if (typeof cur[k] !== 'object' || cur[k] === null || Array.isArray(cur[k])) cur[k] = {}
cur = cur[k]
}
cur[keys[keys.length - 1]] = value
return root
}
// deepGet:按路径读取,缺任何一层就返回默认值(不会报错)
function deepGet(root, keys, def) {
let cur = root
for (let k of keys) {
if (typeof cur !== 'object' || cur === null || !(k in cur)) return def
cur = cur[k]
}
return cur
}
// 用法:存 / 读任意层
PlayerEvents.loggedIn(event => {
let data = JsonIO.read('kubejs/config/nested.json') || {} // 先读
deepSet(data, ['11', '11'], 1) // data["11"]["11"] = 1
deepSet(data, ['11', '22', '33'], { a: 1 }) // 自动建 11 → 22 → 33
deepSet(data, ['players', event.player.username, 'coins'], 500)
JsonIO.write('kubejs/config/nested.json', data) // 再写回
event.player.tell('金币:' + deepGet(data, ['players', event.player.username, 'coins'], 0))
})
③ 递归遍历(把整棵树走一遍)
// 把嵌套对象展平成 "a.b.c = 值" 一行行输出,调试嵌套数据神器
function walk(node, path, out) {
for (let k of Object.keys(node)) {
let v = node[k], p = path.concat(k)
if (v !== null && typeof v === 'object' && !Array.isArray(v)) walk(v, p, out)
else out.push(p.join('.') + ' = ' + JSON.stringify(v))
}
return out
}
let data = JsonIO.read('kubejs/config/nested.json') || {}
console.log(walk(data, [], []).join('\n'))
④ 键是数字时必须用中括号(很多人在这里翻车)
let data = { "11": { "11": 1 } }
data['11']['11'] // ✅ 正确:1
data.11.11 // ❌ 语法错误,数字开头的键不能点访问
data['11'].x // ✅ 后面是普通键可以继续点
⑤ 嵌套的两个坑
| 坑 | 例子 | 正确做法 |
|---|---|---|
| 改深层字段时把整层覆盖了 | data.a = { b: 1 } 会把 data.a 原来的内容全冲掉 |
先 data.a = data.a \|\| {},再 data.a.b = 1(或直接用 deepSet) |
| 直接点到底,中间是 null/不存在就报错 | data.a.b.c 可能报 Cannot read properties of undefined |
逐层判空,或用 deepGet(data, ['a','b','c'], 默认值) |
💡 什么时候别嵌套太深:超过三、四层就难维护了。需要「玩家 → 职业 → 装备槽 → 武器」这种结构时,想想能不能改成平表 + 键拼接(比如
'pyz/战士/主手'当一个键),读起来更简单。
如果存的是复杂 NBT 结构(比如完整物品 NBT),可以用 NBTIO:
// server_scripts/nbt_save.js
// 存 NBT 文件(.nbt 是压缩格式,适合物品/实体数据)
ServerEvents.loaded(event => {
let nbt = { test: 'hello', count: 42 } // 一个 NBT 对象
NBTIO.write('kubejs/config/mydata.nbt', nbt) // 写入压缩 NBT 文件
})
// 读取
ServerEvents.loaded(event => {
let nbt = NBTIO.read('kubejs/config/mydata.nbt') // 读 NBT 文件
if (nbt) {
console.log(nbt.test) // 'hello'
console.log(nbt.count) // 42
}
})
💡 日常存配置/统计用 JsonIO 就够了(可读性好);NBTIO 适合存完整物品/实体结构。
7. 速查表¶
| 想做什么 | 写法 |
|---|---|
| 玩家自己的存档 | player.persistentData.键 = 值 |
| 实体存档 | entity.persistentData.键 = 值 |
| 方块实体存档 | block.persistentData.键 = 值 |
| 服务器临时数据 | server.getData().键 = 值 |
| 世界临时数据 | level.getData().键 = 值 |
| 写 JSON 文件 | JsonIO.write('kubejs/config/xxx.json', 对象) |
| 删 JSON 文件 | JsonIO.write('kubejs/config/xxx.json', null)(传 null = 删文件) |
| 对象 → JSON 字符串 | JsonIO.toString(对象)(一行)/ JsonIO.toPrettyString(对象)(美化) |
| JSON 字符串 → 对象 | JsonIO.parse('{"a":1}') |
| 导出玩家数据 | JsonIO.toString(player.persistentData) |
| 两份数据比对 | JsonIO.getJsonHashString(a) === JsonIO.getJsonHashString(b) |
| 看/改持久数据(命令) | /kubejs persistent_data server get * / merge {"键":值} |
| 高频数据只落盘不卡服 | 内存缓存 + tickCount % 6000 === 0 时 write |
| 读 JSON 文件 | JsonIO.read('kubejs/config/xxx.json') |
| 追加数组元素 | 先 read → data.list.push(x) → 再 write(直接 write 会覆盖!) |
| 改某个字段 | 先 read → data.键 = 新值 → 再 write(其他字段保留) |
| 排行榜排序 | Object.entries(obj).sort((a,b) => b[1]-a[1]) |
| 写 NBT 文件 | NBTIO.write('kubejs/config/xxx.nbt', nbt对象) |
| 读 NBT 文件 | NBTIO.read('kubejs/config/xxx.nbt') |
| 读包模式 | global.packmode |
| 改配置后生效 | /kubejs reload config(不用重启) |
| 配置目录 | kubejs/config/ |
| 日志输出 | console.log('...') → logs/server.log |
💡 记忆口诀:玩家/实体/方块 →
persistentData(重启不丢);服务器/世界临时 →getData();要跨重启的全局数据 →JsonIO.write/read存 JSON 文件;要改包模式 → 改common.properties的packmode然后/kubejs reload config。
8. 🧩 联动:虚拟实体生成(展示实体)¶
虚拟实体(item_display / text_display / block_display / interaction)不会跟随存档保存,服务器一重启就消失。想让虚拟实体「永久存在」,就用本节的 JsonIO 把生成配置存成文件,启动时读档重建:
// server_scripts/virtual_entities_save.js
ServerEvents.loaded(event => { // 服务器加载完成
let data = JsonIO.read('kubejs/config/virtual_entities.json') // ① 读存档
if (!data || !data.displays) { console.log('无虚拟实体存档'); return }
data.displays.forEach(d => { // ② 遍历并重新生成
event.server.runCommandSilent(`summon ${d.type} ${d.x} ${d.y} ${d.z} ${d.nbt}`)
})
console.log('已恢复 ' + data.displays.length + ' 个虚拟实体')
})
📖 完整案例(含保存函数、interaction 虚拟按钮、批量清理)见本站「虚拟实体生成(展示实体)」篇第 4 节。
本文为 KubeJS 1.20.1 中文文档原创篇目。API 依据 KubeJS 6.1 源码(JsonIO / NBTIO / AttachedData / persistentData)。