Files
ngzz-mc/internal/mods/lua.go

200 lines
5.3 KiB
Go
Raw Normal View History

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
}