执行面与控制面分层

一期技术架构

插件运行时负责 Paper 服内规则与本地数据;Cloudflare SaaS 负责身份、归属、配对、顺序化快照和受能力约束的扩展控制面。

两套运行边界

启用 JavaScript 后可用页签切换;禁用 JavaScript 时两套架构都保持完整可读。

下面的拆解仅描述当前 Java 源码已经体现的职责与调用关系。它不把未来机器、未实现学科或云端路线反写成本地插件事实。

layers

插件入口与生命周期

TalexSoulTech 是 JavaPlugin 入口并保存静态实例。onEnable 保存默认配置、初始化 BaseTalex、注册 Listeners、BlockListener、UIListener 与命令,然后为在线玩家创建 PlayerData。onDisable 先关闭界面和电网,再保存机器、物品、方块和玩家数据,最后关闭 MySQL 连接。

组件
  • TalexSoulTech
  • BaseTalex
  • MysqlManager
  • ElectricityManager

玩法事件组合

Listeners 负责进入、离开、手持、交互等玩家事件;BlockListener 负责自定义方块放置与破坏;MachineManager 在通过保护检查后用每台机器的 MachineChecker 决定是否打开机器 UI。

组件
  • Listeners
  • BlockListener
  • MachineManager
  • ProtectorManager

库存界面

InventoryUI 管理分页 Inventory2D 与 Holder;UIListener 只接管该 Holder 的点击和关闭事件,并按玩家节流。MenuBasic 将 Setup、按玩家 Setup、打开、重开与销毁包装成通用菜单生命周期。

组件
  • InventoryUI
  • UIListener
  • MenuBasic

电网结算

电网只在服务端主线程结算。它把位置化端点和线缆重建为连通网络,以源、路径和目标的预算进行能量转移,并把周期统计保留给观测与排错。

组件
  • PowerGrid
  • EnergyBuffer
  • PowerCable
  • PowerCycleStats
  • ElectricityManager

领域与持久化

物品、分类、机器、已放置方块和玩家状态各有自己的领域对象;MySQL 与 YAML 缓存承担不同层次的正常停服持久化职责。

组件
  • PlayerData
  • TalexItem
  • SoulTechItem
  • BaseMachine
  • CategoryObject
  • TalexBlock

PowerGrid 的一次周期

流程
  1. ElectricityManager.start 在主线程注册周期任务;runCycleNow 再次校验主线程后调用 PowerGrid.tick。
  2. PowerGrid 复制并排序端点,对每个仍被注册的端点调用 beforePowerCycle;拓扑变更时通过六向邻接重建连通分量。
  3. 节点数超过 maxNetworkNodes 的网络被标记为 oversized,并在该周期统计中跳过结算,而不是半算一部分。
  4. 正常网络使用公平游标排序生产者、储能与消费者:生产者和储能先向消费者供电,随后未放电的储能只从生产者充电。
  5. 每次转移同时检查源 EnergyBuffer 的可提取量、目标可接收量、路线的可用线缆吞吐与 PowerCable 损耗;内部预算不一致会抛出异常而不是静默吞能。
  6. 产生变化的端点收到 onPowerChanged,PowerCycleStats 记录总输入、交付、损耗、未满足需求、网络规模和耗时。
不变量
  • PowerGrid 在同一 BlockKey 上将端点与线缆互斥注册;新注册项会让 topologyDirty 变为 true。
  • EnergyBuffer 不允许负请求,receive 与 extract 都返回实际接受或提取的数量,并可在不改变状态时模拟。
  • PowerCable 拒绝非正吞吐与非法千分比损耗,损耗范围为 0 至 999。
  • ElectricityManager.runCycleSafely 会记录运行时异常,避免一个周期异常直接停止后续调度。

InventoryUI、UIListener 与 MenuBasic

边界
界面点击节流和事件取消是 UI 行为控制,不是权限系统;机器操作前的保护检查仍在玩法事件路径中完成。
流程
  1. InventoryUI 用 InventoryUIHolder 标识自己创建的库存,并可建立多页 Inventory2D;翻页按钮由可点击物品实现。
  2. UIListener.onClick 先确认点击的是 InventoryUIHolder,然后把事件交给 UI,再执行当前槽位 ClickableItem。
  3. 同一玩家的连续点击受 UI interval 节流;点击项声明处理成功,或 UI 不允许放入物品时,事件会被取消。
  4. 关闭时,UIListener 根据 canClose、closed 状态调用 onTryInventoryClose 或仅一次 onInventoryClose。
  5. UIListener 的异步定时器把实际 refresh 切回 Bukkit 主线程,并只刷新当前打开且开启 autoRefresh 的 InventoryUI。
  6. MenuBasic 的 openForPlayer 先运行 SetupForPlayer,再执行只打开动作;destroy 会释放自身保有的 InventoryUI 与菜单引用。

Listeners、BlockListener 与 MachineManager 的组合

组合关系

Listeners.onJoin 异步构造 PlayerData;onLeave 查找该玩家数据并调用 leave,触发状态写回。

source
玩家进入与离开

Listeners.onInteract 取 PlayerData,先调用 ProtectorManager,再交给 MachineManager。随后才处理 MachineItem 放置逻辑、guide 标签和 st_items NBT 标签分发。

source
玩家交互

Listeners.onItemHold 先验证 TalexItem,再用 SoulTechItem 的物品验证将事件交给匹配的扩展物品。

source
手持物品

BlockListener.onBlockPlaced 过滤原版不适合作为自定义方块的材料,验证 TalexItem 与 soul_tech_item_id;未被物品自行处理时创建 TalexBlock。

source
放置方块

BlockListener.onBlockBreak 先走保护检查,再让自定义工具处理;若目标是 BlockManager 中的 TalexBlock,则交给其受控破坏逻辑。

source
破坏方块

MachineManager 保存以机器名称为键的 BaseMachine。onEvent 依次调用 MachineChecker,首个通过者关闭当前界面并打开机器,随后立即返回。

source
机器选择

TalexSoulTech、BaseTalex 与 MysqlManager 的生命周期

启动顺序
  1. TalexSoulTech.onEnable 设置插件实例与前缀,调用 BaseTalex.init,再调用 BaseTalex.enable。
  2. BaseTalex.enable 在 Settings.mysql.enabled 为 true 时连接 MySQL,并建立 soul_tech_player_data 和 soul_tech_system;连接失败会抛出异常阻止继续启动。
  3. 随后创建并启用 CategoryManager,创建 MachineManager、BlockManager、ProtectorManager,实例化五类机器。
  4. initBase 载入方块缓存、可恢复的 MachineBlockItem 缓存,启动 ElectricityManager,并读取机器缓存。
  5. MysqlManager.get 是进程内惰性单例入口;ElectricityManager.INSTANCE 是固定单例;BaseTalex 则由静态 init 创建并作为玩法服务枢纽暴露管理器。
关闭顺序
  1. TalexSoulTech.onDisable 先关闭在线玩家库存和电网周期,清理全息文本,再保存机器与物品缓存。
  2. BlockManager 正常写入方块缓存;所有 PlayerData 执行 leave 后,MysqlManager.shutdown 关闭 JDBC 连接。
  3. 这条顺序服务于正常停服,不保证对进程被强杀或底层存储损坏的自动恢复。

领域模型关系

关系
model
PlayerData
relationship
绑定 BaseTalex、Bukkit Player、名称、UUID、JSON 状态与 PlayerAttractData;构造时进入 playerManager 并从 soul_tech_player_data 读取,leave 时插入或更新该表。
model
TalexItem → SoulTechItem
relationship
TalexItem 以 ItemStack 和 ItemBuilder 为基础,负责类型、NBT 标签与物品一致性验证。SoulTechItem 继承它,写入 st_items 类型和 soul_tech_item_id,并维护静态物品注册表。
model
BaseMachine → MachineManager
relationship
BaseMachine 维护名称、展示物品、MachineChecker 与配方集合,并在构造时向 MachineManager 自动注册。
model
CategoryObject → RecipeObject → TalexItem
relationship
CategoryObject 是向导树节点,拥有父子关系、前置关系、优先级和菜单或对象类型。对象类型会把 RecipeObject 的展示物品反向关联到自身。
model
TalexBlock → BlockManager → SoulTechItem
relationship
TalexBlock 以位置和原始 ItemStack 注册到 BlockManager,可关联 SoulTechItem。受控破坏会先取消原版事件、执行物品钩子、注销自身、清空方块并掉落保存的物品。

一期云端负责服主身份、服务器归属、一次性配对、顺序化快照同步,以及受能力权限约束的 Cordis 风格扩展运行时。Paper 插件仍是游戏规则与本地数据的执行者;云端不接管玩家背包、机器判定或实时电网结算。

状态

当前状态
一期已实现
范围
  • 静态站点与 Worker API 同属 site 目录;公开站点只提供内容与控制台入口。
  • 认证、服务器管理、配对与同步全部使用 JSON;错误统一为 {error:{code,message}}。
  • D1 保存身份、会话、服务器、密钥哈希、配对码、快照与事件;每台服务器由一个 Durable Object 串行处理同步序列。
  • Cordis 风格扩展以 Context、依赖拓扑、LIFO disposer、原子热更新与 last-known-good 为运行时骨架,Lua 与 JavaScript 都只能通过被授予的能力访问云端资源。
不承诺事项
  • 当前实现不把云端写入当作游戏内状态的唯一真相。
  • 当前实现不承诺跨服物品转移、远程玩家操作、自动回档、远程命令执行或未列出的公开 API。

明确边界

浏览器与会话

服主使用用户名和密码注册或登录。密码使用 PBKDF2;会话放在 HttpOnly、Secure、SameSite=Lax Cookie 中,浏览器脚本不读取会话令牌。

D1 租户数据

DB 绑定指向 D1,其中 users、sessions、servers、server_api_keys、pairing_codes、server_snapshots、server_events 是一期的基础持久化表;扩展控制记录与审计同样按租户和服务器范围持久化。外键与索引服务于用户归属、服务器查询和按服务器写入。

服务器 API Key

明文 API Key 只在配对成功响应中返回给插件一次;D1 仅保存 SHA-256 哈希。控制台、日志、状态接口和插件命令不回显它。

Durable Object

SYNC_COORDINATOR Durable Object 绑定为每个 serverId 提供串行化入口。同步的 sequence 判定、快照写入和事件记录在同一服务器顺序内完成,避免同服并发请求抢写最后状态。

扩展 Context 与沙箱

每个扩展实例获得独立 Context、自己的依赖视图、已授权能力与 disposer 栈。Lua 与 JavaScript 都运行在受限沙箱中,不继承 Worker 全局绑定、其他扩展状态或跨服务器租户的数据访问权。

Paper 插件

插件配置 cloud.enabled 默认 false。完成 /tst cloud link 配对码 后才保存 serverId、apiBase 和 API Key,并以 Bearer 认证提交状态快照。

API 契约

服务健康

路由

/api/health

返回 Worker 健康状态;它不泄露会话、服务器密钥或租户快照。

method
GET

认证

路由

/api/auth/register

创建用户名与 PBKDF2 密码凭据,并建立受保护会话。

method
POST

/api/auth/login

校验用户名和密码后建立受保护会话。

method
POST

/api/auth/logout

撤销当前会话并清除会话 Cookie。

method
POST

/api/auth/me

返回当前已登录服主的身份信息。

method
GET

服务器管理

路由

/api/servers

读取当前服主的服务器列表,或在当前服主名下创建服务器。

method
GET / POST

/api/servers/:id

在 current user_id 与 owner 边界通过后读取单台服务器。

method
GET

/api/servers/:id/pairing

为当前服主拥有的服务器生成一次性、十分钟有效的配对码。

method
POST

/api/servers/:id/snapshot

在所有权校验通过后读取该服务器的最近快照。

method
GET

插件配对

路由

/api/pair/claim

未登录插件提交 {code,name,softwareVersion};有效且未使用的码成功后返回 {serverId,apiKey,apiBase}。

method
POST

状态同步

路由

/api/sync

插件用 Authorization: Bearer API Key 提交 {serverId,sequence,sentAt,server,players,systems,catalog};成功响应为 {accepted,sequence,serverTime}。

method
POST

租户隔离

  • 管理端所有 servers、pairing 与 snapshot 读写都从当前会话得出 user_id,并验证 servers.owner_id;仅靠前端隐藏服务器不构成授权。
  • 配对码属于一台已归属服务器,只能成功领取一次,且十分钟后失效。领取接口不要求浏览器会话,但必须把产生的 serverId、名称与版本绑定到有效码。
  • 同步 API 先通过 Bearer API Key 的哈希校验定位服务器;提交的 serverId 必须与该凭据对应的服务器一致,不能借其他服务器 ID 写入。
  • 每一台服务器的 sequence 在自己的 Durable Object 内串行处理,服务器 A 的写入不会阻塞或污染服务器 B。
  • 扩展控制面同样以 current user_id、owner_id 与 serverId 为边界;扩展 Context 与能力句柄只在该服务器租户中有效,审计事件也不能跨租户读取。

一次性配对契约

步骤
  1. 已登录服主在自己拥有的服务器详情页请求 POST /api/servers/:id/pairing。
  2. 控制台展示一枚单次、十分钟有效的配对码;它不是长期 API Key。
  3. 插件以未登录请求 POST /api/pair/claim,并提交配对码、服务器名称与软件版本。
  4. 成功响应把 serverId、apiKey、apiBase 交给插件。插件本地保存这些值,但 status 与 link 命令不打印 apiKey。
  5. 此后插件只用 Bearer API Key 访问 POST /api/sync;服主仍通过浏览器会话管理服务器与查看快照。

快照同步契约

保证
  • 请求体把服务器元信息、在线玩家概览、系统状态和目录摘要打包为一条服务器快照;它不发送玩家私有背包或数据库凭据。
  • Durable Object 以 serverId 分片,顺序化同一服务器的 sequence 决策与持久化,从而避免并发同步覆盖。
  • API 返回 accepted、最终 sequence 与 serverTime,插件据此判断本次状态是否被接受并调整下一次同步。
  • 同步失败应保持本地游戏继续运行,待下一周期重试;不要因为云端短暂不可用阻塞 Paper 主线程。

Cordis 风格云端扩展运行时

扩展不是能随意读取 Worker 绑定的脚本片段,而是由云端控制面创建、在服务器租户 Context 中运行、受依赖图与能力权限约束的生命周期单元。

状态
一期已实现
Extension Context

每个已启动扩展拥有独立 Context:其中保存扩展标识、所属服务器与租户、解析后的依赖、已获授权的能力、运行状态和专属 disposer 栈。扩展之间不共享可变全局状态,Context 是资源与授权的唯一宿主。

规则
  • Context 由控制面创建并绑定到一台已归属服务器;扩展不能通过参数伪造其他 serverId 或 owner_id。
  • 任何通过 Context 取得的资源都在取得时登记 disposer,避免启动失败、停用或更新后遗留定时器、句柄、订阅或沙箱状态。
  • Context 销毁后,能力句柄立即失效;已停止扩展不能继续读取快照、写入状态或追加审计。
依赖拓扑

控制面将扩展清单中的依赖构成有向图,在启动或更新前检查缺失依赖、循环依赖和版本约束。只有依赖全部处于可用状态时,目标扩展才可启动。

生命周期顺序
  1. 启动按拓扑顺序进行:先依赖,后依赖者。
  2. 停止与删除按反向拓扑进行:先依赖者,后其依赖,避免上游资源先被释放。
  3. 任一节点启动失败时,当前候选链路回收已经取得的资源,不把半初始化的扩展标成运行中。
LIFO disposer

Context 使用后进先出释放策略。资源通常有依赖建立顺序:最后登记的订阅、任务或桥接句柄最先释放,先登记的底层资源最后关闭。

保证
  • 停用、停止、删除、热更新失败和启动失败都走同一条 disposer 路径。
  • 单个 disposer 异常会被记录为审计与运行错误,但不会中断后续 disposer 的释放。
  • 释放过程完成前,扩展不会被标记为已彻底停止;这样控制台状态与实际资源状态保持一致。
原子热更新与 last-known-good

更新从候选包与候选配置开始,而不是原地改写正在运行的实例。控制面先校验清单、依赖图、授权能力和沙箱装载,再在独立候选 Context 中启动。

热更新成功才改变活动版本;失败是可审计事件,不是要求服主手工猜测旧包内容的状态。

流程
  1. 验证候选扩展的标识、版本、依赖、配置结构与能力声明。
  2. 在不影响当前运行实例的候选 Context 中装载 Lua 或 JavaScript 沙箱,并等待启动完成。
  3. 候选启动成功后,原子切换活动版本指针;新的 Context 成为唯一可接收控制面操作的实例。
  4. 旧实例按 LIFO disposer 完整退出;成功运行的版本与已解析配置被保存为 last-known-good。
  5. 候选任一步失败时,候选 Context 立即释放,活动实例和 last-known-good 保持不变,不以部分更新覆盖线上扩展。
沙箱

JavaScript

每个 JavaScript 扩展在独立受限执行域中运行,只通过宿主桥接得到 Context 和已授权能力;它不能直接枚举 Worker 环境变量、D1 绑定、Durable Object 绑定或其他扩展的内存。

Lua

每个 Lua 扩展运行在独立受限状态中,宿主只注入与 Context 能力对应的接口;无权访问的主机函数、跨租户状态和未声明的网络或持久化资源不可见。

权限能力,而不是全局权限

扩展清单声明所需能力,控制面在安装、更新与启动前核对授权。运行时把被批准的能力作为窄句柄注入 Context;没有得到句柄的操作默认拒绝。

规则
  • 能力按服务器租户作用域裁剪,不能用一个扩展的授权操作另一台服务器。
  • 能力可覆盖受控状态读取、受控状态写入、允许的外部调用和审计追加,但每类动作都必须由清单与控制面共同允许。
  • 能力变更属于控制面变更:它会进入审计,并在下一次原子启动或热更新中生效,而不是在运行中悄悄放大权限。
云端增删、启停与审计

已登录服主只能在自己拥有的服务器范围内创建、编辑、安装、更新、启用、禁用、启动、停止和删除扩展。控制面负责状态机与资源清理,不把这些操作下放给浏览器本地状态。

操作

创建与编辑

结果
保存扩展标识、清单、配置和依赖声明,但不会绕开权限与依赖校验直接运行。

启动与启用

结果
验证依赖图与能力后创建 Context;成功才标记为运行中。

停止与禁用

结果
阻止新工作进入,按依赖反序和 LIFO disposer 释放资源,再更新状态。

热更新

结果
在候选 Context 中验证并启动,原子切换或保留 last-known-good。

删除

结果
先完成停止与资源释放,再删除该服务器范围内的扩展记录;不会跨租户删除同名扩展。
可追溯审计

每次控制面动作都会记录发起用户、所属租户与服务器、扩展标识、动作、结果、时间、关联版本和失败原因摘要。敏感配置、API Key 和沙箱源代码不写入可见审计字段。

事件入口
  • 创建、编辑、授权变更、安装、启用、禁用、启动、停止、删除。
  • 依赖图拒绝、权限拒绝、沙箱装载失败、disposer 失败、热更新成功与回退到 last-known-good。

后续路线,尚不属于当前接口承诺

items

在现有快照与事件基础上增加趋势图、异常阈值和可下载的运营报表。

area
观测
状态
后续路线

研究密钥轮换、快照保留策略和细粒度服主成员权限,但不改变当前 owner 边界。

area
运维
状态
后续路线

让目录与版本元数据获得审核发布流程,但不允许云端未经审计地下发游戏规则。

area
内容运营
状态
后续路线