跳转至

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 最有用的配置——同一份脚本可以按「包模式」跑不同逻辑(比如单人/服务器模式):

# kubejs/config/common.properties
packmode=default
// 脚本里读取当前包模式,按模式切换行为
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 换/删存档数据就跟着变(多周目玩法可以考虑)

⚠️ 两个必须注意的点

  1. 父目录必须已存在:KubeJS 只写文件、不会帮你建目录。写 kubejs/config/a.json 没问题(config/ 本来就在);想写到 kubejs/config/rank/a.json 这种新子目录,得先自己把 rank/ 文件夹建好,否则写入会失败(报 NoSuchFileException 之类的错)。
  2. 文件名用英文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 想嵌套多少层都行,写进文件就是你想的那种样子:

{
    "11": {
        "11": {
            "值": 1
        }
    }
}

① 结构固定:直接用 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')
追加数组元素 readdata.list.push(x) → 再 write(直接 write 会覆盖!)
改某个字段 readdata.键 = 新值 → 再 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.propertiespackmode 然后 /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)。