200 lines
5.3 KiB
Go
200 lines
5.3 KiB
Go
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
|
||
}
|