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

200 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/packageMod开发指南.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
}