橡果与遗迹 Wiki
研发

研发AcornCore 架构方案 v0.1

最后编辑 2026-10-21 11:09 · 第 1 版 · 新增页面 · 修订记录(1)

  • 任务:T003
  • 编写:研发窗口(Claude Opus 5.5)
  • 日期:2026-10-21
  • 状态:待审查。本文是研发方案,尚未经总策划审查,不代表已确认
  • 依据:D001(Purpur 1.21.10 #2535、自研层)、D002(多语言)、D003(第三节模块、第六节 W1~W6)、想法池(I05、I14、I15、I21、I24、I25、I28、C5、C8、C10)、W001、M2 草案、T004~T006 任务卡
  • 资料来源:下文"KB"表示 知识库/橡果与遗迹_CE与云系列_1.21.10_报告与本地知识库/知识库/pages/。凡是不能在 KB 里找到原文的结论,都标了 待验证

〇、总体结构一览

┌──────────────── Purpur 1.21.10 #2535 / Java 21 ────────────────┐
│                                                                │
│  AcornCore.jar(主插件,一个 jar)                             │
│  ├─ 平台层 platform:模块管理、配置、数据库、语言、调度、命令 │
│  ├─ 公共服务 services:玩家档案、奖励账本、编年史、世界时钟  │
│  ├─ 业务模块 modules:基础功能、新手、世界事件、冒险重置、   │
│  │                     防挂机……(每个都能单独关闭)           │
│  └─ 外部适配 integrations:Vault、PlayerPoints、云副本、     │
│                            云世界、云物品、MythicMobs……       │
│                                                                │
│  AcornNetEase.jar(网易专属,独立插件,可整体拆掉)          │
│  └─ 只依赖 AcornCore 的公开接口(acorn-api),反过来不依赖  │
└────────────────────────────────────────────────────────────────┘
        │ 只读视图/同步程序(W-5,后期)
        ▼
   网站(W001)

三条基本约束,贯穿全文:

  1. AcornCore 是长期数据的唯一存放处。发现记录、奖励账本、编年史、局外成长都不存在闭源插件里(D001 第四节)。
  2. 界面层只负责显示。CloudUI 按钮只能"请求" AcornCore 做事,校验、扣费、发放都由 AcornCore 完成(D001 第二节)。
  3. 玩家看得到的文字一律走语言键,数据里只存 ID(D002 第四节第 1~3、10 条)。

一、模块划分与故障隔离

1.1 工程结构

源码用 Gradle 多工程,打包成两个 jar:

工程 产物 内容
acorn-api 编进两个 jar 对外接口:服务接口、事件类、数据记录类型(只有接口和不可变数据,没有实现)
acorn-core AcornCore.jar 平台层和公共服务
acorn-module-* 编进 AcornCore.jar 各业务模块,每个模块一个子工程,编译期禁止互相引用实现类(只能引用 acorn-api)
acorn-netease AcornNetEase.jar 网易专属,见第五节 5.8

把业务模块分成子工程,并不是为了分开发布,而是为了用编译器强制模块边界:A 模块想调用 B 模块,只能通过 acorn-api 里的接口,拿不到 B 的内部类。这样关掉 B 时,A 只会拿到"服务不可用",不会因为找不到类而崩溃。

插件描述文件用传统的 plugin.yml(softdepend 列出所有外部插件),不用 paper-plugin.yml。原因:云系列都是闭源 jar,官方示例都写 softdepend(KB cloudplugins/f118205d3fcf6614827a.md,https://cloud.plugins.wiki/docs/dungeon/api )。paper-plugin.yml 要访问别的插件的类,还得逐个配 join-classpath(KB craftengine-source/d9fb1874b512ee1bc3f3.source.md),收益不大。

1.2 模块清单(对应 D003 第三节)

模块 ID 名称 职责 边界(明确不做什么) 依赖的服务 / 外部插件 首批
platform 平台层 模块生命周期、配置、数据库、语言、调度、命令注册、指标 不含任何玩法 — ✅ 不可关闭
profile 玩家档案 玩家 ID、最后用名、语言、隐私设置(不上榜)、首次进服 不存显示文字 platform ✅ 不可关闭
ledger 奖励账本 所有高价值发放的唯一入口,结算号、防重复、恢复 不决定"发什么",只负责"发且只发一次" platform、Vault、PlayerPoints、CloudItems ✅
chronicle 编年史 事件记录、参与者名单、查询接口、导出 不负责游戏内展示馆的建筑 platform、profile ✅ 先记录
worldclock 世界时间与事件 全服统一时间、游戏日计数、月相、每日随机特性、满月大事件调度 事件内容本身由各内容模块注册 platform、chronicle ✅
adventure 冒险世界重置 重置预告、倒计时、送回玩家、纪元(月换种子)记录 不执行世界复制(由 CloudWorlds 执行,W5) worldclock、chronicle、CloudWorlds ✅
basics 基础功能 回主城、tpa、home、back、传送点、公告 不做经济、聊天、全息(D003 第四节) profile、CloudWorlds(传送目的地) ✅
onboarding 新手流程 新手阶段进度、旅人包、免费新手领地发放、可跳过 不做新手世界的实例管理(交给云副本或 CloudWorlds) ledger、chronicle、basics、领地插件、云副本 ✅
afk 防挂机 挂机嫌疑评分、按需弹验证、处置 不做反作弊(交给 Grim) platform、CloudUI 或原生对话框 ✅
collection 收集与图鉴 收藏、系列收集、"源"、主题货币(奇幻松针) — ledger、chronicle 第二批
worldboss 世界 Boss 战 刷新、通告、前 N 名传送、伤害统计、最后一击与参与结算 Boss AI 由 MythicMobs 负责 ledger、chronicle、MythicMobs、afk 第二批
roguelike 肉鸽层 随机房间、局内祝福、局内武器池 组队、门票、断线恢复交给云副本 ledger、云副本 后期
(独立 jar)netease 网易专属 充值发货、防沉迷提示、网易身份映射 不写进任何通用玩法逻辑 ledger、PlayerPoints ✅ 先留接口

"多语言"不是一个模块,而是平台层的服务,所有模块都必须使用(D003 原表"贯穿全部模块")。

1.3 "每个模块都能单独关闭"怎么实现

每个模块实现统一的生命周期接口,由平台层的模块管理器统一调度:

加载描述 → 检查依赖 → 执行数据库迁移 → 启用 → 运行 → 停用
   ↓失败       ↓缺依赖        ↓失败           ↓失败
       ───────────── 模块状态 = FAILED,其他模块照常 ─────────────
  • 开关:plugins/AcornCore/modules.yml 里每个模块一行 enabled: true/false。关闭的模块不加载、不注册监听、不注册命令,它的数据表保留不动。
  • 依赖声明:每个模块声明"硬依赖"(缺了就不能启用)和"软依赖"(缺了就降级)。例:adventure 硬依赖 worldclock;onboarding 软依赖领地插件,缺了就只是不发新手领地,并在后台警告。
  • 启动顺序:按依赖做拓扑排序;有循环依赖时拒绝启动这几个模块,并在日志里报出依赖环。
  • 运行中关闭:管理命令 /acorn module disable <id>,会连带停用依赖它的模块,并列出受影响的模块请管理员确认。账本、档案这两个核心服务不允许运行中关闭。

1.4 "一个模块出错不影响其他模块"怎么实现

Java 插件运行在同一个进程、同一个主线程里,做不到操作系统级别的隔离。能做到的是把下面几类常见故障拦在模块内部:

故障类型 隔离办法
启用时抛异常(配置写错、迁移失败) 模块管理器捕获异常,模块标为 FAILED,依赖它的模块一起不启用,其他模块照常
事件处理抛异常 模块的监听器不直接注册给 Bukkit,而是通过平台层包一层:捕获异常、记录、计数。同一模块 60 秒内异常超过阈值(默认 20 次)就自动熔断:停用该模块,通知在线管理员
定时任务出错或泄漏 模块只能通过平台层提供的调度器建任务,任务归属于模块;模块停用时平台层统一取消,避免"模块关了任务还在跑"
命令出错 命令由平台层统一注册和分发;模块停用后命令仍然存在,但只回复语言键 acorn.common.module.disabled("该功能暂时关闭"),不会出现"未知命令"
数据库慢或出错 所有数据库读写走专用的后台线程池,主线程不等数据库。单个模块的查询超时只影响该模块的功能
跨模块调用 只通过服务接口调用,返回"可选值"。对方模块未启用或已熔断时,调用方拿到"不可用",按自己的降级逻辑处理
卡主线程 平台层记录每个模块的监听器和任务耗时,超过阈值(默认单次 50ms)写警告日志,交给 spark 进一步分析

做不到的(如实说明):死循环卡住主线程、内存溢出、修改了 Bukkit 全局状态(例如乱改游戏规则)。这些只能靠代码审查和测试服压测防住。

1.5 配置约定

  • config.yml:平台设置(数据库、默认语言、地区)。
  • modules.yml:模块开关。
  • modules/<模块ID>.yml:模块自己的参数。配置里不写任何玩家可见文字,只写语言键和数值。
  • 配置写错时,模块启用失败并报出文件名、行号和字段,不使用"猜一个默认值继续跑"的做法(会掩盖错误)。
  • 权限节点统一 acorn.<模块>.<动作>,由 LuckPerms 管理。

二、数据层

2.1 存储选型

阶段 选型 说明
首发(单服) SQLite,开 WAL 模式 免运维;单写线程,所有写入排队执行,避免 SQLite 锁冲突
迁移触发条件 网站上线前(W-5),或拆分子服时 SQLite 是单个文件,网站所在的机器读不到;到时迁 MySQL
之后 MySQL 8(或兼容的 MariaDB) 连接池用 HikariCP

怎么保证以后迁得动:

  1. 只用 JDBC 和两种数据库都支持的 SQL 子集。必须不同写法的地方(主要是"有则更新、无则插入"、自增主键、时间函数)集中放进一个"方言"接口,SQLite 一份、MySQL 一份。
  2. 不用数据库自增主键当业务 ID。所有主键由程序生成(见 2.4),迁移时不会因为自增值冲突而对不上。
  3. 时间一律存 UTC 毫秒整数,不存数据库的日期类型,避免两种数据库时区处理不同。
  4. 表结构变更走迁移脚本:每个模块自己的迁移脚本放在 db/<模块ID>/V<n>__说明.sql,按方言各一份。每个模块单独记录自己的结构版本,一个模块迁移失败只让这个模块 FAILED。
  5. 提供停服迁移工具:/acorn db export 把 SQLite 导出成中立格式,在 MySQL 上导入后逐表比对行数和校验值。迁移流程交给 sol 执行,研发写工具和说明。
  6. 测试:每个数据访问类同时跑 SQLite 和 MySQL 两套测试(MySQL 用容器)。

数据库驱动和连接池打包进 jar 并重定位包名,避免和其他插件自带的版本冲突。Purpur 自带的 SQLite 驱动版本是多少、能不能直接用,待验证。

2.2 "只存 ID"的落实方式

数据 存什么 不存什么
玩家 UUID 不把玩家名当主键(会改名)
玩家名 只在 player_profile.last_name 存一份"最后已知名",用于离线查询和网站展示 名字是玩家输入的文字,不属于翻译范围,但公开前要过滤(W001 第三节第 3 条)
内容(物品、生物、Boss、群系、事件类型、收集品) 带命名空间的字符串 ID,如 acorn:butterfly_divine、mythicmobs:frost_griffin、minecraft:snowy_taiga 不存显示名、不存语言文字
世界 世界键,如 acorn:adventure 不存世界 UUID:冒险世界每天由模板覆盖,UUID 可能变(待验证),存键才稳
坐标 世界键 + x/y/z/朝向 —
物品实物 优先存"奖励规格"(来源插件 + 物品 ID + 数量);必须保存实物时(如带随机词缀的装备),用 Paper 的物品序列化存字节 不存物品的名称和描述文字

ID 规则:

  • 全小写英文、数字、下划线,格式 命名空间:路径。
  • ID 一旦上线就不改、不复用。内容下架时 ID 保留,在内容登记表里标"已下架";确实要改名的,建别名表映射,旧数据不改。
  • 外部插件的 ID 原样加命名空间前缀存,例如云物品 clouditems:long_sword/thunder_sword(云物品是"类型 + ID"两级,KB cloudplugins/f93e1f41430e45de22f2.md)。

⚠️ 关键前提,待验证:网易认证接入后,同一个网易账号每次进服的 UUID 是否固定。如果网易认证走的是离线模式,UUID 由用户名计算,那么玩家改名就会变成"另一个人",整个"只存 ID"体系都会失效。这一条必须在 T6(网易认证接入验证)时确认。如果不固定,就要在 netease 模块里加一张"网易账号 ID → 内部玩家 ID"的映射表,所有模块使用内部 ID。

2.3 数据表(大致结构,首批)

以下只列主要字段,最终字段在写代码时细化。

平台与档案

表 主要字段 说明
acorn_schema_version module, version, applied_at 每个模块的结构版本
player_profile uuid(主键), last_name, locale, first_join_at, last_seen_at, chronicle_visibility chronicle_visibility:公开 / 匿名(显示为"一位旅人")
content_registry content_id(主键), kind, status, alias_of 内容 ID 登记,用于下架和改名

奖励账本(详见第四节)

表 主要字段
ledger_settlement settlement_id(主键), idem_key(唯一), source_type, source_ref, player_uuid, status, created_at, updated_at, attempts, last_error
ledger_line settlement_id, line_no, kind(item / vault / points / theme_currency / command), target_id, amount, payload, status
ledger_audit audit_id, settlement_id, action, actor, at, detail

编年史(详见第六节)

表 主要字段
chronicle_event event_id(主键), type_id, epoch, game_day, moon_phase, occurred_at, world_key, subject_id, data(JSON,只放 ID 和数字), visibility, schema_version
chronicle_participant event_id, player_uuid, role, rank, metric

世界与模块

表 主要字段 说明
world_state key(主键), value 全服游戏日计数、当前纪元号、当前种子、上次重置日期等
basics_home uuid, home_id, world_key, x, y, z, yaw, pitch —
basics_back uuid, world_key, x, y, z, world_generation world_generation 用于判断冒险世界是否已经重置(见 5.4),重置后旧的 back 点失效
onboarding_progress uuid, step_id, state, updated_at 每一步可跳过,可中途回来
afk_record uuid, at, score, result 只记验证结果,不记行为细节

主题货币(第二批)用 wallet(uuid, currency_id, balance, version),每次变动都在账本里留记录,用版本号做乐观锁,防止并发扣减出错。

2.4 ID 生成

  • 结算号、事件号统一用 ULID(26 位,按时间有序,可在多台机器上独立生成,不会冲突)。
  • 给人看的结算号(客服、补偿时使用)显示为 S- 加 ULID 后 10 位,数据库里仍然存完整 ULID。

三、多语言

3.1 文件格式与目录

plugins/AcornCore/lang/
├── zh_cn/              主语言,唯一必须完整的语言
│   ├── common.yml
│   ├── basics.yml
│   ├── ledger.yml
│   └── …(一个模块一个文件)
├── zh_tw/              先建空壳(D002 第四节第 5 条)
└── en_us/              先建空壳
  • 格式:YAML,扁平键,一行一个完整键名,例如 acorn.basics.home.set_success: "已把这里设为家「<home>」。" 之所以用扁平键,不按点号嵌套,是为了让键名能直接全文搜索,翻译人员也不会弄乱层级。YAML 允许写注释,可以给翻译人员留上下文说明。
  • 文字格式:MiniMessage(Paper 自带的 Adventure 文字库,CloudWorlds 的提示文字也支持 MiniMessage,KB cloudplugins/449a2c3abb154caa11f9.md)。
  • 占位符:D002 写的是 {item},MiniMessage 里对应写成 <item>。整句一个键,变量只能用占位符插入,禁止在代码里拼接两个语言键。
  • 默认语言文件打包在 jar 里;服务器上的文件可以覆盖。启动时把 jar 里有、服务器文件里没有的键补进去,并写日志提示。

3.2 按玩家语言选择与回退

玩家客户端语言(Paper 接口取得,如 zh_tw)
  → 精确匹配 zh_tw
  → 同语族映射(zh_hk → zh_tw;zh_sg → zh_cn;en_gb/en_au → en_us)
  → 部署默认语言(config.yml 的 default-locale;大陆服 zh_cn,台湾服 zh_tw)
  → zh_cn
  → 仍然找不到:显示键名本身,并写一次警告日志(同一个键只警告一次)
  • 玩家切换客户端语言后,下一条消息就用新语言。Paper 的取得语言接口和语言变更事件,KB 里没有收录 Paper 开发者文档,待验证(参考 https://docs.papermc.io/paper/dev/ ,未能离线核对)。
  • 地区部署(D002 方案 A):config.yml 提供 forced-locale。台湾服设为 zh_tw 后,所有人都看繁体,不跟随客户端。
  • 服务端渲染为主:AcornCore 在服务端按玩家语言把键翻译成文字再发出去。这样不依赖资源包是否加载成功,后台日志也能看到实际文字。
  • 可选的 CE 通道:CraftEngine 支持"在其他插件的消息和物品里使用 <l10n> 标签,CE 在发包时按玩家语言替换"(KB craftengine-zh/790bef0b7d25ef28f226.md,https://xiao-momi.github.io/craft-engine-wiki/zh-Hans/configuration/translations )。物品名、物品描述这类会被保存、会被别人看到的文字,适合用这个通道,否则一件物品掉到别人手里还是原主人的语言。注意 CE 的回退顺序是"zh_cn → zh → en",和我们的"回退到 zh_cn"不同,用这个通道的键必须保证 zh_cn 完整。
  • 网站:语言文件可以导出成 JSON 交给网站,网站和游戏用同一份文字(W001 第三节第 5 条)。

3.3 语言键命名规则

  • 格式:acorn.<模块>.<对象>.<用途>(D002 第四节第 3 条),全小写英文,单词之间用下划线,不用拼音。
  • <模块> 与模块 ID 一致:common、basics、ledger、chronicle、worldclock、adventure、onboarding、afk 等。
  • 常用用途后缀统一:title、desc、button、success、fail、confirm、broadcast、hint。
  • 编年史事件的文字键由事件类型推出:类型 acorn:world_boss.defeated → acorn.chronicle.world_boss_defeated.summary、.detail。
  • 专有名词(地名、生物名、货币名)的键登记在 策划/术语表.md,代码里引用术语表的键,不另起一个。

3.4 自动检查

  • 构建时检查:代码里出现的每个语言键在 zh_cn 里都存在;zh_cn 里的每个占位符,其他语言里都有对应。不通过就构建失败。
  • 启动时检查:输出各语言的缺失键数量报告。
  • 代码审查规则:发给玩家的字符串必须来自语言服务;直接写中文字符串的代码不合并(日志和后台调试信息除外)。

四、奖励账本

4.1 原则

  • 所有高价值发放都必须经过账本:副本通关的核心奖励、世界 Boss 奖励、充值发货、补偿、活动大奖、新手旅人包。低价值的随手掉落(普通怪掉金币)不进账本,否则写入量太大。哪些算"高价值"由总策划给清单,账本只提供能力。
  • 账本只负责"发且只发一次",不决定发什么。
  • 记录只追加。发错了不删记录,补一条"撤回"记录冲正。

4.2 结算号与幂等键

一笔发放有两个号:

号 怎么来 作用
幂等键(idem_key) 由业务事实决定,同一件事永远算出同一个键 数据库唯一约束,防重复发放的关键
结算号(settlement_id) 新生成的 ULID 给人查账、客服、审计用

幂等键的规则:<来源类型>:<来源事件号>:<玩家UUID>:<奖励槽位>。

来源 幂等键示例 依据
云副本通关 dungeon:<副本实例ID>:<玩家>:clear 云副本的 Clear 事件提供 getInstanceId()、getPlayerIds()(含掉线中的玩家)(KB cloudplugins/f118205d3fcf6614827a.md)。实例 ID 重启后会不会重复,待验证;如果会重复,就在键里加上 AcornCore 记录的开始时间
世界 Boss boss:<编年史事件号>:<玩家>:last_hit / :participation 事件号由 AcornCore 生成,见第六节
网易充值 netease:<网易订单号> 订单号来自网易接口,待验证(网易资料入驻后才提供)
新手旅人包 onboarding:<玩家>:starter_kit 每人一辈子只能领一次
补偿 compensation:<补偿批次ID>:<玩家> 由管理命令创建批次

为什么不用随机号防重复:如果每次都生成一个新的随机号,同一个"通关"事件被处理两次(插件重载、事件重复触发、重启后重放)就会得到两个不同的号,数据库认为是两笔,于是发两次。幂等键由事实决定,第二次插入会被唯一约束拒绝,这才能真正防重复。

4.3 发放流程(状态机)

         写入(唯一约束)
请求 ──────────────→ PENDING ──玩家在线──→ GRANTING ──→ GRANTED
          ↓已存在                │                  │
     直接返回原结果           玩家离线             失败
     (不会第二次发)            ↓                  ↓
                          等玩家上线再发        FAILED(重试,超过次数进人工)
  1. 登记:后台线程插入 PENDING 记录。插入被唯一约束拒绝,说明这件事已经处理过,直接返回原记录,什么都不发。
  2. 执行:玩家在线时,回到主线程:先把状态改为 GRANTING 并提交,再逐行执行发放。
  3. 完成:全部成功,改为 GRANTED。

每类奖励怎么保证"只发一次":

奖励类型 执行方式 崩溃时怎么判断发没发
物品 放进背包;背包满了就进 AcornCore 的"待领取"列表,不丢在地上 发放的同时,把结算号写进玩家自己的持久化数据(PDC)。PDC 和背包存在同一份玩家存档里,要么一起保存,要么一起丢失。随后立即保存这名玩家的存档。恢复时看 PDC 里有没有这个结算号:有就补记 GRANTED,没有就重发
金币(Vault) depositPlayer,检查返回值 transactionSuccess()(KB vault/4ba5575f08fc8d48954c.md,https://github.com/MilkBowl/VaultAPI ) Vault 没有交易号,无法事后查证。崩溃时卡在 GRANTING 的记录不自动重发,进入人工核对队列
神奇橡果(PlayerPoints) 用 PlayerPoints 的 Java API(KB 未收录,参考 https://github.com/Rosewood-Development/PlayerPoints ,待验证);备选是控制台命令 points give(云系列文档里的默认写法,KB cloudplugins arena/config 页) 同金币,进人工核对。充值发货的单笔金额大,人工核对必须做
云物品装备 云物品的 create(物品ID, 数量) 生成后按"物品"处理(KB cloudplugins/a11030281c6bd2ae58a5.md,更新日志,签名待验证);备选 /ci give <类型> <ID> <玩家> <数量>(KB cloudplugins/e3ae8f9ab29aa72e9ed3.md)。命令方式在背包满时会掉在脚边,所以优先用 API 同物品
主题货币 AcornCore 自己的钱包表,和账本在同一个数据库事务里更新 天然一致,不存在这个问题

GRANTING 卡住的时间窗口只有几毫秒(从改状态到改完状态之间),只有正好在这时崩溃才会进入人工队列。账本要做到宁可让管理员多核对一笔,也不自动重复发。

4.4 断线、离线与重启恢复

  • 玩家离线:记录停在 PENDING,玩家下次登录时自动发放。第二批接入星月邮箱后,可以改成发邮件,邮件接口本身也支持幂等键(.key(String),"同一个 key 只会存一次邮件",KB cloudplugins/4f893cd86ba5cd670de2.md,https://cloud.plugins.wiki/docs/starmail/api ),用结算号当邮件键,两层都防重复。
  • 玩家在发放中途断线:主线程上的发放是一次性完成的,不会在执行到一半时断线;断线发生在这之前,记录就仍是 PENDING。
  • 服务器重启:启动时扫描所有非终态记录:PENDING 照常等待;GRANTING 记录在玩家上线时按上表核对 PDC,金币和点券类的进入人工队列;FAILED 按退避时间重试,超过次数进入人工队列。
  • 人工队列:/acorn ledger review 列出待核对记录,管理员核对后选择"确认已发"或"补发"。所有操作写入审计表。

4.5 和云副本自带奖励的关系(⚠️ 需要总策划定规则)

云副本自己有完整的结算奖励和掉线补发机制(end.yml 的 rewards,"通关时掉线中的人:指令与物品会存起来,下次登录时补发",KB cloudplugins/a857f47a9f1a176b8b39.md,https://cloud.plugins.wiki/docs/dungeon/end )。如果副本奖励一部分走云副本、一部分走账本,就会回到 D001 担心的"各自发奖"的局面。

研发建议:

  • 高价值奖励一律由 AcornCore 监听云副本的 Clear 事件发放,不写进 end.yml 的 rewards。这样数据在我们手里,幂等键里有实例 ID。
  • end.yml 的 rewards 只放低价值、可以重复也没关系的东西,或者干脆不用。
  • 入场券退还(refund-on-crash)继续交给云副本,那是它自己的数据。
  • 不建议用 end.yml 里的 command 调 AcornCore 发奖:命令变量里没有实例 ID(可用变量只有 {player}、{uuid}、{dungeon}、{rank}),算不出可靠的幂等键。

4.6 防抢奖励与对账

  • 同一事件只结算一次:世界 Boss 等多人事件,先生成编年史事件号,结算以事件号为单位加锁,后续每个人的奖励都以这个事件号为幂等键的一部分。
  • 每日对账报告:每天统计各货币、各物品通过账本发出的总量,以及单个玩家的发放量;超过阈值(如单人单日神奇橡果发放量)在后台告警。这同时服务于 C8(活动掉橡果的上限)和 C10(橡果市场兑换上限)。

五、外部接口

5.1 总原则

  • 每个外部插件对应一个适配器,放在 integrations 包里,对内只暴露 AcornCore 自己定义的接口。外部插件的 API 一变,只改适配器。
  • 全部 softdepend。启动时检测是否存在、是否已通过授权;不存在时,依赖它的功能降级,并在日志里写明"哪个模块的哪项功能因为缺少什么而关闭"。
  • 云系列闭源 jar 作为编译期依赖,放在工程的 libs/ 目录(compileOnly files(...),KB cloudplugins/f118205d3fcf6614827a.md)。这些 jar 不提交到任何公开仓库,授权文件也不进版本管理。
  • 云系列授权失效时:云副本"不会启用(副本世界、指令都不会出现)",云物品"所有功能与指令停用",云世界"世界照常加载,只有 /cw 管理指令被锁"(KB cloudplugins 的 dungeon/items/worlds 安装页)。适配器每次调用前都检查可用性,不可用时由账本把发放留在 PENDING,不发也不丢。

5.2 Vault(金币)

项 内容
方式 通过服务管理器取得 Economy,depositPlayer / withdrawPlayer / getBalance,检查 EconomyResponse.transactionSuccess()
依据 KB vault/4ba5575f08fc8d48954c.md(https://github.com/MilkBowl/VaultAPI );依赖 com.github.MilkBowl:VaultAPI:1.7。README 里没有 withdrawPlayer 示例,签名按 Vault 公开接口,待验证
注意 README 原文"有些经济插件要求金额取整":金额统一用整数(金币不设小数),由策划确认
⚠️ 风险 XConomy 是否每次加减立即写库,还是先缓存再定时写入:如果是缓存,服务器崩溃会丢掉最近的加减,账本记了"已发"但钱没到账。待验证,建议 sol 在 T006 里做一次"加钱后立即强杀进程"的测试

5.3 PlayerPoints(神奇橡果)

项 内容
方式 首选 Java API(加点、扣点、查询);备选控制台命令 points give {player} {amount}
依据 KB 没有 PlayerPoints API 资料;命令写法见 KB cloudplugins arena/config 页(https://cloud.plugins.wiki/docs/arena/config )。API 参考官方仓库 https://github.com/Rosewood-Development/PlayerPoints ,未能离线核对,待验证
约束 D003:货币一律通过接口操作,不直接改 PlayerPoints 的数据库
和云系列的关系 云商城、市集、邮箱都原生支持 PlayerPoints 点券("playerpoints / PlayerPoints / 整数",KB cloudplugins/7854cad0947b011c7905.md,https://cloud.plugins.wiki/docs/cloudshop/prices ),所以神奇橡果不自己实现
1.21.10 构建 由 T002 核验

5.4 CloudWorlds(多世界、冒险世界重置)

项 内容
API 只有一个很小的静态类:CloudWorldsAPI.isReady() / alias / meta / inventoryGroup / teleport(player, "目的地")(KB cloudplugins/449a2c3abb154caa11f9.md,https://cloud.plugins.wiki/docs/worlds/config )。basics 模块的世界传送点调用它
重置事件 知识库未找到重置前后的事件。所以 AcornCore 不能"收到重置通知",只能自己判断
重置执行 按 W1 用 on_startup:"在世界加载前直接换回模板,最快"(KB cloudplugins/08e0346a6d6df528b72a.md,https://cloud.plugins.wiki/docs/worlds/reset )
时间 CloudWorlds 的 time 保持 normal,不设 day-length(W3;字段说明见 KB cloudplugins/b5f6d6ab5c7a8695d533.md)

AcornCore 怎么判断冒险世界已经重置:在冒险世界的世界级持久化数据里写一个"世界代号"(每次开服检查)。模板里存的是模板制作时的代号,模板覆盖后,代号就变回模板的值,和数据库里记录的不一致,说明发生了重置。这时:世界代号加一,作废所有冒险世界里的 back 点和临时记录,写编年史。如果用了新模板(月换种子),就记一条"第 N 纪元开启"(W2)。Paper 的世界级持久化数据在模板复制后是否随世界文件一起被覆盖,待验证(T005 实测时一并确认)。

重置流程中 AcornCore 负责的部分(W5):

  1. 每日重启前 N 分钟开始全服预告和倒计时(语言键 + 时间参数),冒险世界里的玩家额外提示"箱子和临时据点会消失"。
  2. 关服前把冒险世界里的玩家送回主城(不依赖 CloudWorlds 的"送到出生点",因为那时我们自己已经关服)。
  3. 关服由 sol 的定时重启流程执行;AcornCore 只发出"可以重启了"的信号或执行关服命令,具体由 T005 结果决定。

5.5 云副本 CloudDungeon

项 内容
事件 dev.cloudplugins.dungeon.api.DungeonEvent 下的内部类:PreStart(可取消)、Start、MonsterGroup、Clear、Fail、Leave。全部在主线程触发(KB cloudplugins/f118205d3fcf6614827a.md,https://cloud.plugins.wiki/docs/dungeon/api )
可用数据 getDungeonId()、getInstanceId()、getModeId()、getPlayerIds()(含掉线中的)、Clear.getClearMillis()、Clear.getRank()、Fail.getReason()
禁止 文档明确说 getDungeon() / getInstance() 返回的是混淆过的内部对象,不属于公开 API,不准调用
缺口 没有"阶段"事件(只能用 MonsterGroup 判断进度);没有掉线、重连的 Java 事件,只有副本脚本里的 quit / rejoin 触发组(KB cloudplugins/9563109efb0e0adc220b.md)。另外"离开那一刻不在线的玩家不会触发 Leave"
AcornCore 的用法 Clear → 账本发高价值奖励(4.5)、写编年史(如"首次通关");PreStart → 需要时由 AcornCore 做额外的进入检查(例如新手地牢只允许新手进);Start / Fail → 肉鸽层(后期)的局开始和结束
依赖方式 compileOnly files('libs/CloudDungeon-<版本>.jar'),softdepend: [CloudDungeon]。没有 Maven 仓库
不碰的部分 副本世界只归云副本管(W6);入场券、体力、返回位置的持久化由云副本自己处理

5.6 MythicMobs

项 内容
用途 worldboss 模块监听 Boss 受伤和死亡,统计伤害、判定最后一击(含 C5 的"最后一击参与门槛");collection 模块监听珍奇生物的捕捉和击杀;必要时注册自定义技能或条件
方式 MythicMobs 的 Bukkit 事件(生物生成、死亡)和 API
依据 KB 只收录了 MythicMobs 首页,API 页没有抓取(KB mythicmobs/fef44b2ad07f68282543.md)。API 文档地址 https://wiki.mythiccraft.io/mythicmobs/API ,未能离线核对,事件类名和包名全部待验证
伤害统计 不依赖 MythicMobs 自己的威胁值,用 Bukkit 原生的实体受伤事件,按"这个实体是不是我们登记的 Boss"过滤后自己统计。这样换 MythicMobs 版本也不受影响
首发不需要 首批模块(基础、新手、时间、重置、账本、编年史、防挂机)都不依赖 MythicMobs,世界 Boss 在第二批,到时补抓 API 文档再定

5.7 其他接口(简述)

插件 用法 依据
CloudItems 账本发装备用 CloudItemsApi.create(物品ID, 数量) KB cloudplugins/a11030281c6bd2ae58a5.md(仅更新日志提到,包名和签名待验证);备选 /ci give(KB cloudplugins/e3ae8f9ab29aa72e9ed3.md)
CloudUI 界面按钮用 command: 动作调用 AcornCore 的命令(例:command: acorn ui onboarding skip),AcornCore 校验权限和状态后执行。界面上的数据用 PlaceholderAPI 变量显示 动作写法来自云商城和云竞技的接入页(KB cloudplugins/2f8a8c966bd8ad25af69.md、4c17a9ce08bf2edd417f.md)。CloudUI 主文档页没有抓到,第三方插件能否像云商城那样注册自己的动作、command: 动作是用玩家身份还是控制台身份执行,待验证。如果是控制台身份执行,AcornCore 的界面命令必须从参数里取玩家并重新校验,不信任界面传来的任何数据
CloudModel 世界 Boss、珍奇生物的模型挂载由 MythicMobs 配置完成(model{mid=}),AcornCore 一般不直接调用;需要时用 com.cloudplugins.cloudmodel.api.CloudModelAPI KB cloudplugins/06d5d95a95c2f538b05a.md、bd9d3349293ab69b5a2c.md
CraftEngine 遗迹机关(后期)监听 Bukkit 原生的方块事件,再用 CE 的 api 包把方块转成自定义方块 ID;只用 api 包下的类 KB craftengine-source/d9fb1874b512ee1bc3f3.source.md(原文"只有 api 包下的内容是稳定的","不要直接用 CustomBlockBreakEvent,监听对应的 Bukkit 事件")
星月邮箱(第二批) 离线补发、补偿;用 .key() 做幂等 KB cloudplugins/4f893cd86ba5cd670de2.md
领地插件 新手领地的发放。Residence 的 API KB 未收录,待验证;T006 结果出来前,先按"执行领地插件的管理命令"设计,适配器接口留好 —
PlaceholderAPI AcornCore 注册变量(游戏日、月相、纪元、待领取奖励数等),供 CloudUI 和其他插件显示 KB 只有用户文档;开发文档 https://wiki.placeholderapi.com/developers/creating-a-placeholderexpansion/ 待验证
LuckPerms 只用权限节点判断,不调用 LuckPerms API —

5.8 网易模块:怎么做到可以整体拆掉

现状:网易的认证接口和商业化接口资料不公开。网易入驻指南写明,服务器入驻时需要修改 BungeeCord 的认证接入;拿到正式机后由官方对接人员发送相关文件,这些文件属于保密资料(https://mc.163.com/m/dev/mcmanual/mc-dev/mcguide/35-%E4%B8%8A%E6%9E%B6%E4%B8%8E%E5%85%A5%E9%A9%BB/%E8%AF%BE%E7%A8%8B13-%E3%80%8A%E6%88%91%E7%9A%84%E4%B8%96%E7%95%8C%E3%80%8B%E4%B8%AD%E5%9B%BD%E7%89%88JAVA%E7%BD%91%E7%BB%9C%E6%B8%B8%E6%88%8F%E5%85%A5%E9%A9%BB%E6%8C%87%E5%8D%97.html ;KB 只有概述页 netease-java/8e7127b4e0149ed38673.md)。所以本节只定结构,具体对接全部待验证。

结构:

  1. 独立 jar(AcornNetEase.jar),只依赖 acorn-api。AcornCore 里没有任何一行网易相关代码。台湾服或其他地区部署时,不放这个 jar 即可。
  2. AcornCore 在 acorn-api 里定义几个"插口",网易模块往里插实现:
插口 AcornCore 的默认实现(没有网易模块时) 网易模块的实现
充值发货 无(不提供充值) 接网易商业化接口,收到订单后调用账本:幂等键 netease:<订单号>,内容是 PlayerPoints 加点
身份映射 直接用 UUID 如果网易的 UUID 不稳定(见 2.2),在这里映射成内部玩家 ID
合规提示 无 防沉迷、实名相关提示
  1. 认证本身在代理端(BungeeCord),不在 Paper 上。网易认证插件由网易提供,按网易资料配置,归 sol 部署。Paper 上的网易模块只负责发货和合规提示。
  2. 充值发货的防重复:网易回调可能重复通知同一个订单(一般支付系统都会重发直到确认)。账本的幂等键天然挡住重复;网易模块只在账本确认已登记(PENDING 写入成功)后才回复网易"已收到"。这条按通用支付惯例设计,网易的实际协议待资料到手后核对。
  3. 不在 Minecraft 进程里开公网 HTTP 端口。如果网易采用"回调我们的接口"方式,回调由一个独立的小服务接收,写入数据库的待发货表,网易模块从表里取单发货。这样即使回调服务被攻击,也碰不到游戏进程。具体形式等网易资料。

六、编年史事件

6.1 事件数据结构

一条事件 = 一行 chronicle_event + 若干行 chronicle_participant。

字段 类型 说明
event_id ULID 主键,也是世界 Boss 结算的幂等键组成部分
type_id 字符串 ID 事件类型,如 acorn:world_boss.defeated、acorn:collection.first_complete、acorn:epoch.begin、acorn:player.first_arrival
epoch 整数 第几纪元(每月换种子加一,I24)
game_day / moon_phase 整数 全服游戏日与月相(来自 worldclock)
occurred_at UTC 毫秒 现实时间
world_key / biome_id 字符串 ID 发生地点(可空)
subject_id 字符串 ID 主体,如 Boss ID、系列 ID
data JSON 只放 ID 和数字,如 {"duration_ms":392000,"drops":[{"id":"acorn:frost_mane_heart","n":1}],"participant_count":47}
visibility 枚举 public(公开)/ internal(只在后台)
schema_version 整数 事件格式版本,以后改格式时旧事件仍能解析

参与者表 chronicle_participant:event_id、player_uuid、role(last_hit 最后一击、top_damage 输出第一、participant 参与者、first 首个达成者……)、rank、metric(伤害等数值)。

世界 Boss 的参与者名单和参与奖励用同一份数据(I25):worldboss 模块战斗结束时先写编年史事件和参与者,再以 event_id 发奖。这样"编年史上有名"和"拿到了参与奖励"永远一致。

展示时才变成句子:type_id 对应语言键(3.3),data 和参与者填进占位符。玩家名在展示时从 player_profile 取当前名字。

隐私(W001 第三节第 2 条):是否匿名在读取时判断,不在写入时判断。玩家在任何时候把自己设为"不上榜",过去所有记录都会立刻显示为"一位旅人"(语言键 acorn.chronicle.anonymous_player);改回公开也立即恢复。数据本身不删,统计和发奖不受影响。

第 0 纪元:编年史/第0纪元_筹备纪.md 在开服前一次性导入为 epoch = 0 的事件,类别对应 W001 约定的立项、决策、美术、技术、里程碑。

6.2 为网站预留的读取方式(W001)

W001 的原则是"游戏内和网站读的是同一份数据",同时网站只负责展示。研发建议分三步:

阶段 做法 说明
首发 只记录,不开放 编年史写进 SQLite;游戏内用命令或史官 NPC 查看
网站上线前 数据库迁到 MySQL,给网站建一个只读账号,只能读编年史相关的视图 视图里已经按隐私设置处理过(匿名玩家的 UUID 和名字不出现在视图里),网站拿不到不该拿的数据
W-5 联通插件 独立的小型 API 服务(不在 Minecraft 进程里),按游标增量读取:GET /chronicle/events?after=<event_id>&limit=100。ULID 按时间有序,可以直接当游标 鉴权方式等 W-5 时再定(W001 第四节)。网站用游戏导出的语言文件拼句子

为什么不在 AcornCore 里直接开 HTTP 接口:游戏进程开公网端口会增加被攻击面,网站流量大时也会拖慢主线程。

玩家输入文字过滤(W001 第三节第 3 条):玩家名、以后的公会名在写入只读视图前标记"是否已通过过滤",网站只显示通过的;没通过的显示为"一位旅人"。过滤规则和网易审核要求怎么接,待定。


七、开发顺序建议

阶段 内容 前置 和原型验证的关系
P0 平台 工程骨架、模块管理与故障隔离、配置、数据库与迁移(SQLite + MySQL 方言)、语言服务与检查、调度、命令框架、玩家档案 无,可以马上开始(不需要测试服) 一切模块的地基
P1 账本 + 编年史 账本状态机、Vault / PlayerPoints / 云物品适配、人工队列、对账报告;编年史记录与查询 P0;T002(插件版本核验);T006(经济插件装好) D001 的 T3、T4 验证需要账本:T4"副本五条路径不吞物品、不重复发奖"直接测账本
P2 世界 worldclock(全服时间、夜晚不跳过、月相)、adventure(预告、送回、世界代号、纪元记录)、basics(回城、tpa、home、back、传送点、公告) P0、P1(编年史);T004(Purpur 能否关掉睡觉跳过)、T005(重置实测数据) 依赖 sol 的实测结果定细节
P3 新手与防挂机 onboarding(进度、旅人包走账本、新手领地适配)、afk P1、P2;T006(领地);M2 草案定稿;Q17 新手第一天流程能完整走通
P4 网易模块 充值发货、身份映射、合规提示 P1;网易入驻后的对接资料 D001 的 T6
第二批 collection、worldboss(含伤害统计、最后一击门槛、临时战场)、星月邮箱接入 P1、P2;MythicMobs API 补抓 —
后期 roguelike 肉鸽层 云副本事件;肉鸽玩法设计 —

P0 不依赖任何外部条件,审查通过后可以立刻开工,和 sol 的 T001、T002 并行。


八、待验证清单

编号 内容 影响 谁来验证
V1 ⚠️ 网易认证后同一账号的 UUID 是否固定 整个"只存 ID"体系 sol / 用户(T6 网易认证接入时)
V2 XConomy 加减金币是否立即写库 账本金币发放的可靠性 sol(T006 加做强杀测试)
V3 云副本实例 ID 重启后是否会重复 副本奖励幂等键 研发(测试服)
V4 冒险世界由模板覆盖后,世界级持久化数据和世界 UUID 是否一起被覆盖 重置检测、back 点失效 研发 + sol(T005)
V5 CloudUI 的 command: 动作用谁的身份执行;第三方插件能否注册自己的动作 界面接入方式 研发(拿到 CloudUI 构建后)
V6 PlayerPoints、MythicMobs、PlaceholderAPI、Paper 取语言接口的官方 API 细节 适配器写法 研发(补抓官方文档)
V7 云物品 CloudItemsApi 的包名和方法签名 账本发装备 研发(拿到 jar 后)
V8 Purpur 自带 SQLite 驱动的版本 数据层打包方式 研发
V9 Residence 的 API 或管理命令 新手领地发放 sol(T006)+ 研发
V10 网易充值接口形式(回调还是轮询)、订单号、重复通知规则 网易模块 用户(入驻后向网易对接人索取资料)