feat(mods): Lua 沙箱 Mod 运行时——generation 注册表、注册 API、事件、热重载与回滚、文件监听防抖(含测试)

This commit is contained in:
NianGao Dev
2026-08-15 23:04:51 +08:00
parent 07c086e127
commit 38f015ba22
9 changed files with 644 additions and 0 deletions

79
internal/textr/text.go Normal file
View File

@@ -0,0 +1,79 @@
// Package textr 文本光栅化:把字符串用位图字体渲染成 RGBA 图像(CPU 侧,UI还原.md §4)。
//
// 字形来自素材包 8×8 位图字体(internal/font);本包输出可直接上传为 GL 纹理的图像,
// 因此与渲染线程解耦(GL 只负责贴图)。
package textr
import (
"image"
"image/color"
"image/draw"
"mc/internal/font"
)
// Drawer 文本绘制器。
type Drawer struct {
atlas *font.Atlas
}
// New 创建绘制器。
func New(atlas *font.Atlas) *Drawer { return &Drawer{atlas: atlas} }
// Width 计算文本像素宽度(8px/字符)。
func (d *Drawer) Width(s string) int {
w := 0
for _, r := range s {
if g, ok := d.atlas.Glyphs[r]; ok {
w += g.Width
} else {
w += 8 // 缺失字形占位
}
}
return w
}
// Draw 渲染一行文本(白字透明底;shadow 为 true 时附加 1px 黑色阴影,UI还原.md §4)。
// 返回图像与阴影偏移(阴影在图像内左上角,调用方绘制时需偏移 1px)。
func (d *Drawer) Draw(s string, c color.RGBA, shadow bool) image.Image {
w := d.Width(s)
h := 8
if shadow {
w++
h++
}
img := image.NewRGBA(image.Rect(0, 0, w, h))
x := 0
for _, r := range s {
g, ok := d.atlas.Glyphs[r]
if !ok {
g = d.atlas.Missing()
}
d.blit(img, g, x, 0, c)
if shadow {
d.blit(img, g, x+1, 1, color.RGBA{0, 0, 0, 255})
}
x += g.Width
}
return img
}
// blit 把字形像素以指定颜色写入目标(字形位图按 alpha 通道着色)。
func (d *Drawer) blit(dst *image.RGBA, g font.Glyph, dx, dy int, c color.RGBA) {
if d.atlas.Image == nil {
return
}
src := d.atlas.Image
for y := 0; y < g.H; y++ {
for x := 0; x < g.W; x++ {
r, _, _, a := src.At(g.X+x, g.Y+y).RGBA()
if a == 0 {
continue
}
_ = r // 字形颜色以纹理亮度为准,目标色统一用 c
dst.SetRGBA(dx+x, dy+y, c)
}
}
}
var _ = draw.Draw // 保持 image/draw 引用(后续 9-patch 使用)

View File

@@ -0,0 +1,66 @@
package textr
import (
"image/color"
"path/filepath"
"testing"
"mc/internal/assets"
"mc/internal/font"
)
// newTestDrawer 从真实素材包构建字体图集与绘制器。
func newTestDrawer(t *testing.T) *Drawer {
t.Helper()
m := assets.New()
if err := m.Load(filepath.Join("..", "..", "assets"), filepath.Join("..", "..", "resourcepacks")); err != nil {
t.Fatalf("加载内容包失败: %v", err)
}
at, err := font.Build(m, "default")
if err != nil {
t.Fatalf("构建字体失败: %v", err)
}
return New(at)
}
// TestWidth 文本宽度 = 字符数 × 8。
func TestWidth(t *testing.T) {
d := newTestDrawer(t)
if w := d.Width("AB"); w != 16 {
t.Fatalf("宽度期望 16,实际 %d", w)
}
if w := d.Width(""); w != 0 {
t.Fatalf("空串宽度期望 0,实际 %d", w)
}
}
// TestDraw 渲染 'A' 得到 8×8 且含非透明像素。
func TestDraw(t *testing.T) {
d := newTestDrawer(t)
img := d.Draw("A", color.RGBA{255, 255, 255, 255}, false)
b := img.Bounds()
if b.Dx() != 8 || b.Dy() != 8 {
t.Fatalf("图像尺寸期望 8×8,实际 %dx%d", b.Dx(), b.Dy())
}
opaque := 0
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
if _, _, _, a := img.At(x, y).RGBA(); a > 0 {
opaque++
}
}
}
if opaque == 0 {
t.Fatal("渲染结果全透明")
}
}
// TestDrawShadow 阴影模式:图像加 1px 且含黑色像素。
func TestDrawShadow(t *testing.T) {
d := newTestDrawer(t)
img := d.Draw("A", color.RGBA{255, 255, 255, 255}, true)
b := img.Bounds()
if b.Dx() != 9 || b.Dy() != 9 {
t.Fatalf("阴影图像尺寸期望 9×9,实际 %dx%d", b.Dx(), b.Dy())
}
}