在线联机原型全集:第 28 章 副本脚本系统

副本脚本系统(Instance Scripting System) 类别:副本脚本系统(Instance Scripting System) 目标:提供“可编程副本(Programmable Dungeons/Instances)”能力,支持 Lua/JS 沙箱、事件触发器(Triggers)、可热更(Hot …

副本脚本系统(Instance Scripting System)

  • 类别:副本脚本系统(Instance Scripting System)
  • 目标:提供“可编程副本(Programmable Dungeons/Instances)”能力,支持 Lua/JS 沙箱、事件触发器(Triggers)、可热更(Hot Reload)、版本化(Versioning)、可回放(Replay),并与房间/战斗/任务/AI 等子系统解耦。
  • 原型代号proto-028-instance-script
  • 依赖模块proto-003-board(房间内核)
  • 推荐语言栈:Go(gopher-lua/yaegi/quickjs-go)、Rust(mlua/rquickjs)、Java(GraalVM Polyglot)
  • 协议栈:TCP/UDP + WebSocket(实时)/HTTP(管理与发布) + Cron/DelayQueue(定时)

1. 设计原则(Design Principles)

  1. 隔离(Isolation):脚本运行在受限沙箱(Sandbox)中,CPU/内存/IO/时钟/随机数受控;只暴露白名单 API。
  2. 确定性(Determinism):战斗/判定相关逻辑支持“固定步长 + 纯函数输入输出”,用于回放与反作弊;允许“弱非确定(e.g. 掉落表)”通过可播种 RNG 复现。
  3. 热更 & 版本化:副本脚本以 script_pack(包)为单位发布,支持蓝绿/灰度、向下兼容数据迁移。
  4. 事件驱动(Event-Driven):统一触发器模型:事件源(Event Source)→ 条件(Condition)→ 行为(Action)→ 效果(Effect)。
  5. 可观测(Observability):脚本级指标(执行耗时、内存峰值、触发频率、错误栈)、结构化日志、分布式 Trace。
  6. 安全(Security):禁用危险库、限制 FFI、限制文件/网络访问;审计与签名校验。

2. 核心概念(Core Concepts)

  • Instance(副本实例):一次房间/关卡运行期容器,持有状态(地图、怪物、机关、任务进度、时间轴)。
  • ScriptPack(脚本包)manifest.json + /lua or /js + /assets + schema,含版本号、依赖、迁移脚本。
  • Trigger(触发器)on(Event) if Condition then Action 的声明式配置或内嵌脚本。
  • Sandbox VM(沙箱虚拟机):Lua 或 JS 引擎(QuickJS)包装,注入 Host API(Timer、ECS、Quest、Drop、UI、Path、Rand、State)。
  • State Snapshot(状态快照):用于断线重连、回放与回滚(prediction rollback)。

3. 体系结构(Architecture Overview)

+---------------- 管理面 / CI-CD ------------------+
|  Script Registry  | Pack Signer | Gray Release  |
+---------+---------+-------------+---------------+
          | Pull
          v
+----------------- 运行面(Game) ------------------+
| InstanceManager  | InstanceShard(N)              |
|  - routing       |  - VM Pool (Lua/JS)          |
|  - lifecycle     |  - Trigger Engine            |
|                  |  - Event Bus Adapter         |
|                  |  - Snapshot/Replay           |
+------------------+------------------------------+
          ^                              |
          | Metrics/Logs/Trace           | Publish Events
          |                              v
+------------------ Infra -------------------------+
| DelayQueue | KV/DB | ObjectStore | PubSub | KMS |
+--------------------------------------------------+

4. 数据模型(Data Model, 简化)

ScriptPack:
  id: string           # "dungeon-forest"
  version: string      # "1.3.2"
  engine: "lua"|"js"
  entry: "main.lua"    # 入口
  apis: ["ecs","quest","rand","timer","state","drop","ui","path"]
  manifest:
    min_protocol: 2
    compat: [ "1.x" ]
  hash: "sha256:..."
  sign: "ed25519:..."

Instance:
  id: int64
  pack_id: string
  pack_version: string
  seed: int64
  state_ref: "kv://instances/{id}"
  tick_ms: 50
  players: [player_id...]

Trigger:
  id: string
  event: string        # e.g. "OnEnterArea", "OnMobDead", "OnTimer"
  condition: string    # 脚本或表达式
  action: string       # 脚本或动作组合
  throttle_ms: 200     # 节流

5. 沙箱设计(Lua / JS)

5.1 资源限制

  • CPU:执行指令计数器/时间片;超过阈值中断(debug.sethook/QuickJS JS_SetInterruptHandler)。
  • 内存:VM 堆上限(Lua lua_gc 配额,QuickJS JS_NewRuntime memory limit)。
  • 时钟:暴露受控 now(),可注入回放时钟。
  • 随机数:播种 RNG(rand.seed(seed), rand.next()),确保可复现。
  • IO:禁止文件/网络,提供 Host 代理函数(队列化、限频)。

5.2 Host API(示例)

-- Lua
local ecs = require("host.ecs")
local timer = require("host.timer")
local ev = require("host.event")
local drop = require("host.drop")
local rand = require("host.rand")
local state = require("host.state")

JS 侧同名命名空间:Host.ecs, Host.timer, …

6. 触发器(Triggers)

6.1 声明式配置

triggers:
  - id: "gate-open"
    event: "OnLeverSwitch"
    condition: "state.get('key_count')>=3"
    action: |
      ecs.openGate("north")
      ui.broadcast("Gate to the North is now open!")
  - id: "boss-spawn"
    event: "OnAreaEnter:boss_room"
    condition: "players.count >= 2"
    action: |
      ecs.spawn("boss_ogre",{pos={x=10,y=0,z=5}})
      music.play("boss_theme")

6.2 编程式(Lua 示例)

ev.on("OnMobDead", function(ctx)
  if ctx.mob_id == "ogre_minion" then
    local k = state.get("minion_dead") or 0
    k = k + 1
    state.set("minion_dead", k)
    if k >= 5 then
      ecs.spawn("boss_ogre", {pos=ctx.pos})
      ui.broadcast("Boss appears!")
    end
  end
end)

7. 生命周期(Lifecycle)

  1. Create:分配 Instance→绑定 ScriptPack→播种 RNG→载入地图与初始状态。
  2. Warmup:执行 on_init()(生成物件、注册触发器、定时器)。
  3. Run:固定 Tick 驱动;事件注入→条件评估→行为执行→状态变更→快照。
  4. Checkpoint/Snapshot:关键帧保存(每 N 秒或关键事件)。
  5. Close:掉落与奖励结算→持久化→释放 VM/资源。

8. 回放 / 回滚(Replay / Rollback)

  • 输入日志:[tick, event_type, payload_hash]
  • 状态快照:S0 + Δ1..Δn
  • 回放:重播事件流 + 同种 seed + 同版脚本包(或兼容回放适配层)。
  • 回滚:用于客户端预测冲突,回退至最近快照并重放未确认事件。

9. 热更与版本(Hot Reload & Versioning)

  • 包签名:CI 产出 *.spk(脚本包),带哈希与签名。
  • 蓝绿pack_version=ab 并存;新开副本用 b,旧实例继续用 a
  • 灰度:按玩家段/区服/时间窗比例路由。
  • 迁移脚本migrations/1.2.0_to_1.3.0.lua,用于长期副本状态兼容。

10. 管理与发布(Ops)

  • API:

    • POST /packs 上传脚本包
    • POST /packs/{id}/deploy 灰度参数
    • POST /instances 创建/关闭/查询
  • 指标:vm_cpu_ms, vm_mem, trigger_exec_count, error_rate, p99_latency

  • 日志:结构化(inst_id, pack_ver, trigger_id, elapsed_ms)。

11. 与子系统的接口(Integration)

  • Event Bus:统一事件名空间(combat.*, room.*, quest.*),触发器订阅模式匹配。
  • ECS/AOI:脚本通过 Host API 操作实体(查询、创建、移动、加 Buff)。
  • 任务/成就:脚本调用 quest.progress(player, key, delta)
  • 掉落表drop.roll(table_id, rand),可播种。
  • AI:脚本发 Command 到 AI 子系统,避免重逻辑。

12. 安全清单(Security Checklist)

  • os.*, io.*, 原生 FFI;
  • 运行上限:单触发器执行 ≤ 5ms(可配置),超时中断并告警;
  • 沙箱上下文只读/写受控 state
  • 包签名 & 来源校验,审计发布人/变更单;
  • VM 级隔离(按实例/按触发器池化可选);

13. Go 参考实现(节选)

13.1 VM 抽象

type VM interface {
	Load(pack ScriptPack) error
	Call(funcName string, args ...any) (any, error)
	Emit(event string, payload map[string]any) error
	SetHostAPI(name string, fn any) error
	Close() error
}

type VMLimiter struct {
	MaxCPU time.Duration
	MaxMemBytes int64
}

13.2 QuickJS(JS)示意

// quickjs-go 伪代码
rt := quickjs.NewRuntime(quickjs.WithMemoryLimit(16<<20))
rt.SetInterruptHandler(func() bool { return time.Since(start) > 5*time.Millisecond })
ctx := rt.NewContext()
injectHost(ctx) // 注入 Host.ecs / Host.timer / ...
ctx.Eval(string(pack.Main), quickjs.EvalModule)

13.3 gopher-lua(Lua)示意

L := lua.NewState(lua.Options{SkipOpenLibs: true})
openSafeLibs(L) // 仅打开 string/table/math 等
injectHost(L)   // 注册 host.* 函数
if err := L.DoString(mainLua); err != nil { ... }

func onEvent(event string, payload map[string]any) error {
	L.GetGlobal("ev_on") // 约定注册函数
	// push args...
	return L.PCall(nArgs, 0, nil)
}

13.4 触发器执行器

type Trigger struct {
	ID        string
	Event     string
	Condition string // 脚本或表达式
	Action    string
	Throttle  time.Duration
	lastAt    time.Time
}
func (t *Trigger) TryFire(ctx *ExecCtx, ev Event) {
	if time.Since(t.lastAt) < t.Throttle { return }
	if ok := ctx.EvalBool(t.Condition, ev); !ok { return }
	_ = ctx.Exec(t.Action, ev) // 超时/限流在 ctx 内部
	t.lastAt = time.Now()
}

14. 调试与工具(Tooling)

  • 脚本单元测试:离线 VM,Mock Host API,golden tests
  • 场景回放:用录制的事件流批量回放对比 Hash。
  • 可视化编辑器:Trigger Graph(节点编辑)、时间轴(Timeline)、波形图(计时/冷却)。
  • 火焰图:脚本热点函数与长尾触发器定位。

15. 作者工作流(Authoring Workflow)

  1. 脚本包模板:spk init dungeon-forest
  2. 本地运行:spk run --seed 123 --record trace.json
  3. CI:lint(静态检查)→ 单测 → 回放对齐 → 签名打包。
  4. 灰度发布:spk deploy --percent 10 --regions eu,de
  5. 回滚:spk rollback --to 1.3.1

16. 示例脚本(Lua & JS)

16.1 Lua:机关 + 波次

function on_init()
  state.set("wave", 0)
  timer.every(5000, function() spawn_wave() end)
end

function spawn_wave()
  local w = (state.get("wave") or 0) + 1
  state.set("wave", w)
  ui.broadcast("Wave "..w.." incoming")
  for i=1,5 do
    ecs.spawn("goblin",{pos={x=5*i,y=0,z=10}})
  end
  if w==3 then ecs.spawn("ogre",{pos={x=20,z=20}}) end
end

ev.on("OnLeverSwitch", function(ctx)
  if state.get("wave")>=2 then
    ecs.openGate("east")
  else
    ui.tip(ctx.player, "Defeat more waves to open the gate.")
  end
end)

16.2 JS(QuickJS):钥匙收集 + Boss

export function on_init() {
  Host.state.set("keys", 0);
  Host.ev.on("OnPickup:key", (ctx) => {
    const k = (Host.state.get("keys") || 0) + 1;
    Host.state.set("keys", k);
    Host.ui.broadcast(`Keys: ${k}/3`);
    if (k >= 3) {
      Host.ecs.spawn("boss_ogre",{pos:{x:12,z:8}});
      Host.music.play("boss_theme");
    }
  });
}

17. 性能与容量规划

  • 每实例 VM:≤ 16 MB 堆(可配置);
  • 触发器 QPS:常态 < 1k/s;
  • 池化策略:按实例/按触发器组 VM 复用;
  • 限流:per-trigger, per-player, global 三层。

18. 测试用例清单(节选)

  • 触发器节流/去抖;
  • RNG 重播一致性;
  • 热更期间老实例不变、心跳延续;
  • 快照/回放哈希对齐;
  • 恶意脚本(死循环、内存爆)拦截;
  • 掉落表边界概率;

19. 风险与对策

  • 脚本抖动:加节流/批量合并(coalesce),行动压缩。
  • 非确定性:统一 RNG、禁止系统时钟;外部调用入队。
  • 安全合规:最小权限、包签名、审计链。
  • 作者误用 API:强类型声明与 Schema 校验、IDE 提示、示例仓库。

20. 落地里程碑(Milestones)

  • M1:Lua 沙箱 + 触发器引擎 + 基本 Host API(2 周)。
  • M2:QuickJS 支持 + 回放/快照 + 指标(2 周)。
  • M3:灰度/蓝绿发布 + 包签名 + 管理面(2 周)。
  • M4:可视化编辑器(触发器图)+ 离线单测套件(3 周)。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页