feat(mods): Lua 沙箱 Mod 运行时——generation 注册表、注册 API、事件、热重载与回滚、文件监听防抖(含测试)
This commit is contained in:
199
internal/mods/lua.go
Normal file
199
internal/mods/lua.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user