package mods import ( "context" "fmt" "time" lua "github.com/yuin/gopher-lua" ) // 沙箱限制(设计.md §3.5、Mod开发指南.md §6)。 const ( defaultScriptTimeout = 5 * time.Millisecond // 单事件处理时限 defaultMaxEntries = 256 // 注册数量上限 ) // LuaMod 一个 Lua 脚本 Mod。 type LuaMod struct { id string reg *Registry timeout time.Duration entries int handlers map[string]*lua.LFunction // 事件 → 处理器 } // NewLuaMod 创建 Lua Mod。 func NewLuaMod(id string, reg *Registry) *LuaMod { return &LuaMod{ id: id, reg: reg, timeout: defaultScriptTimeout, handlers: make(map[string]*lua.LFunction), } } // Load 在沙箱中执行入口脚本(设计.md §3.3 第 5 步)。 // 脚本内可调用 api.* 注册内容与事件;重复 Load 用于热重载。 func (m *LuaMod) Load(script string) error { return m.reg.Reload(m.id, func(gen uint64) error { return m.run(script, gen) }) } // run 执行脚本(generation 版本化 + 超时 + 限额)。 func (m *LuaMod) run(script string, gen uint64) error { ctx, cancel := context.WithTimeout(context.Background(), m.timeout*10) // 加载阶段放宽 defer cancel() L := lua.NewState(lua.Options{SkipOpenLibs: true}) defer L.Close() // 仅开放安全基础库(沙箱:禁 os/io/debug/package,Mod开发指南.md §6) for _, pair := range []struct { n string f lua.LGFunction }{ {lua.BaseLibName, lua.OpenBase}, {lua.TabLibName, lua.OpenTable}, {lua.StringLibName, lua.OpenString}, {lua.MathLibName, lua.OpenMath}, } { L.Push(L.NewFunction(pair.f)) L.Push(lua.LString(pair.n)) L.Call(1, 0) } L.SetContext(ctx) api := L.NewTable() L.SetFuncs(api, map[string]lua.LGFunction{ "register_block": m.apiRegisterBlock(gen), "register_item": m.apiRegisterItem(gen), "register_recipe": m.apiRegisterRecipe(gen), "on": m.apiOn, "get_block": m.apiGetBlock, }) L.SetGlobal("api", api) if err := L.DoString(script); err != nil { return fmt.Errorf("mods.run %s: %w", m.id, err) } return nil } // apiRegisterBlock 注册方块(Mod开发指南.md §4.2)。 func (m *LuaMod) apiRegisterBlock(gen uint64) lua.LGFunction { return func(L *lua.LState) int { t := L.CheckTable(1) id := L.GetField(t, "id").String() m.entries++ return m.registerOrError(L, "block", id, tableToMap(L, t), gen) } } // apiRegisterItem 注册物品。 func (m *LuaMod) apiRegisterItem(gen uint64) lua.LGFunction { return func(L *lua.LState) int { t := L.CheckTable(1) id := L.GetField(t, "id").String() m.entries++ return m.registerOrError(L, "item", id, tableToMap(L, t), gen) } } // apiRegisterRecipe 注册配方。 func (m *LuaMod) apiRegisterRecipe(gen uint64) lua.LGFunction { return func(L *lua.LState) int { t := L.CheckTable(1) key := fmt.Sprintf("%d", m.entries) m.entries++ return m.registerOrError(L, "recipe", key, tableToMap(L, t), gen) } } // registerOrError 写入注册表;失败向 Lua 抛错(热更新.md §4.2 回滚路径)。 func (m *LuaMod) registerOrError(L *lua.LState, kind, key string, data map[string]any, gen uint64) int { if m.entries > defaultMaxEntries { L.RaiseError("注册数量超上限 %d", defaultMaxEntries) return 0 } if err := m.reg.Register(m.id, gen, kind, key, data); err != nil { L.RaiseError("%v", err) return 0 } return 0 } // apiOn 注册事件处理器(Mod开发指南.md §4.3)。 func (m *LuaMod) apiOn(L *lua.LState) int { event := L.CheckString(1) fn := L.CheckFunction(2) m.handlers[event] = fn return 0 } // apiGetBlock 世界只读(占位:完整接入见 Mod 加载器与 world 联动)。 func (m *LuaMod) apiGetBlock(L *lua.LState) int { L.Push(lua.LNil) return 1 } // tableToMap Lua 表 → map[string]any(仅一层)。 func tableToMap(L *lua.LState, t *lua.LTable) map[string]any { out := make(map[string]any) t.ForEach(func(k, v lua.LValue) { out[k.String()] = luaValueToGo(L, v) }) return out } // luaValueToGo Lua 值 → Go 值(基础类型)。 func luaValueToGo(L *lua.LState, v lua.LValue) any { switch vv := v.(type) { case *lua.LTable: return tableToMap(L, vv) case lua.LString: return string(vv) case lua.LNumber: return float64(vv) case lua.LBool: return bool(vv) default: return nil } } // DispatchEvent 触发事件(热更新.md §4.1:处理器按来源绑定,重载时解绑)。 func (m *LuaMod) DispatchEvent(event string, args ...any) error { fn, ok := m.handlers[event] if !ok { return nil } ctx, cancel := context.WithTimeout(context.Background(), m.timeout) defer cancel() L := m.newDispatchState(ctx, args...) defer L.Close() if err := L.CallByParam(lua.P{Fn: fn, NRet: 0, Protect: true}); err != nil { return fmt.Errorf("mods.DispatchEvent %s: %w", event, err) } return nil } // newDispatchState 创建事件分发 VM(基础库最小集)。 func (m *LuaMod) newDispatchState(ctx context.Context, args ...any) *lua.LState { L := lua.NewState(lua.Options{SkipOpenLibs: true}) for _, pair := range []struct { n string f lua.LGFunction }{ {lua.BaseLibName, lua.OpenBase}, {lua.TabLibName, lua.OpenTable}, {lua.StringLibName, lua.OpenString}, {lua.MathLibName, lua.OpenMath}, } { L.Push(L.NewFunction(pair.f)) L.Push(lua.LString(pair.n)) L.Call(1, 0) } L.SetContext(ctx) return L }