服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
176 lines
6.2 KiB
Go
176 lines
6.2 KiB
Go
package mod
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
)
|
|
|
|
func samplePackage() Package {
|
|
return Package{Metadata: Metadata{Name: "中文测试"}, Entries: []Entry{
|
|
{Path: "stackable/demo.stk", Operation: ReplaceFile, Encoding: Text, DataType: 1, Data: []byte("[name]\n`中文`")},
|
|
{Path: "list/stackable.lst", Operation: MergeList, Pairs: []Pair{{"123", "stackable/demo.stk"}}},
|
|
{Path: "string/demo.str", Operation: MergeStrings, Pairs: []Pair{{"name", "中文>说明"}, {"empty", ""}}},
|
|
}}
|
|
}
|
|
|
|
func TestExtend110Write(t *testing.T) {
|
|
root := t.TempDir()
|
|
if err := (Extend110Writer{}).Write(root, samplePackage()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for p, want := range map[string]string{
|
|
"pvf/stackable/demo.stk": "[name]\n`中文`",
|
|
"merge/list/stackable.lst": "123\t`stackable/demo.stk`\n",
|
|
"merge/string/demo.str": "name>中文>说明\nempty>\n",
|
|
} {
|
|
data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(p)))
|
|
if err != nil || string(data) != want || strings.HasPrefix(string(data), "\ufeff") {
|
|
t.Fatalf("%s = %q, error %v", p, data, err)
|
|
}
|
|
}
|
|
var manifest struct {
|
|
Name, Version string
|
|
Priority int
|
|
Enabled bool
|
|
Types map[string]int32
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, "pack.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(data, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if manifest.Name != "中文测试" || manifest.Version != "1.0" || manifest.Priority != 100 || !manifest.Enabled || manifest.Types["stackable/demo.stk"] != 1 {
|
|
t.Fatalf("manifest = %#v", manifest)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "sprite")); !os.IsNotExist(err) {
|
|
t.Fatal("unused resource directory created")
|
|
}
|
|
}
|
|
|
|
func TestExtend110CustomVersion(t *testing.T) {
|
|
pkg := samplePackage()
|
|
pkg.Metadata.Version = "2.1-beta"
|
|
root := t.TempDir()
|
|
if err := (Extend110Writer{}).Write(root, pkg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, "pack.json"))
|
|
if err != nil || !strings.Contains(string(data), `"version": "2.1-beta"`) {
|
|
t.Fatalf("custom manifest = %s, %v", data, err)
|
|
}
|
|
for _, invalid := range []string{" 1.0", "1.0 ", "1\n2", strings.Repeat("x", 129)} {
|
|
pkg.Metadata.Version = invalid
|
|
if err := (Extend110Writer{}).Validate(pkg); err == nil {
|
|
t.Fatalf("invalid version accepted: %q", invalid)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtend110RejectsUnsupportedContentAndCollisions(t *testing.T) {
|
|
cases := []Entry{
|
|
{Path: "../outside", Operation: ReplaceFile, Encoding: Text, DataType: 1},
|
|
{Path: "file.equ", Operation: DeleteFile},
|
|
{Path: "data.bin", Operation: ReplaceFile, Encoding: Bytes, DataType: 9},
|
|
{Path: "list/test.lst", Operation: MergeList, Pairs: []Pair{{"text-id", "item/a"}}},
|
|
{Path: "list/test.lst", Operation: MergeList, Pairs: []Pair{{"1", "../outside"}}},
|
|
{Path: "string/test.str", Operation: MergeStrings, Pairs: []Pair{{"bad>key", "value"}}},
|
|
{Path: "string/test.str", Operation: MergeStrings, Pairs: []Pair{{"key", "bad\nline"}}},
|
|
}
|
|
for _, entry := range cases {
|
|
pkg := Package{Metadata: Metadata{Name: "test"}, Entries: []Entry{entry}}
|
|
if err := (Extend110Writer{}).Validate(pkg); err == nil {
|
|
t.Fatalf("accepted %#v", entry)
|
|
}
|
|
}
|
|
pkg := samplePackage()
|
|
pkg.Entries = append(pkg.Entries, Entry{Path: "STACKABLE/DEMO.STK", Operation: ReplaceFile, Encoding: Text, DataType: 1})
|
|
if err := (Extend110Writer{}).Validate(pkg); err == nil {
|
|
t.Fatal("accepted case-insensitive duplicate")
|
|
}
|
|
pkg = samplePackage()
|
|
pkg.Entries = append(pkg.Entries, Entry{Path: "stackable", Operation: ReplaceFile, Encoding: Text, DataType: 1})
|
|
if err := (Extend110Writer{}).Validate(pkg); err == nil {
|
|
t.Fatal("accepted file/directory collision")
|
|
}
|
|
}
|
|
|
|
func TestNameAndPathValidation(t *testing.T) {
|
|
for _, name := range []string{"", ".hidden", "_hidden", "a/b", "a\\b", "CON", "LPT1.txt", "COM¹", strings.Repeat("a", 256), "name.", "name ", " name", "a:b", "a\x00b"} {
|
|
if err := ValidateName(name); err == nil {
|
|
t.Errorf("accepted name %q", name)
|
|
}
|
|
}
|
|
for _, p := range []string{"/root", "dir//file", "../file", "dir/../file", "dir/NUL.txt", "dir/file.", "C:/file", "dir\\file"} {
|
|
if err := ValidatePath(p); err == nil {
|
|
t.Errorf("accepted path %q", p)
|
|
}
|
|
}
|
|
if err := ValidateName("合法名称 MOD"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
type testReader struct{}
|
|
|
|
func (testReader) Detect(source fs.FS) (bool, error) {
|
|
_, err := fs.Stat(source, "test.marker")
|
|
return err == nil, nil
|
|
}
|
|
func (testReader) Read(fs.FS) (Package, error) { return samplePackage(), nil }
|
|
|
|
func TestRegistrySeparatesReadWriteCapabilities(t *testing.T) {
|
|
registry := DefaultRegistry()
|
|
if err := registry.Register(Format{Descriptor: Descriptor{ID: "read-only"}, Reader: testReader{}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader, err := registry.Get("read-only")
|
|
if err != nil || !reader.Descriptor.CanRead || reader.Descriptor.CanWrite || reader.Writer != nil {
|
|
t.Fatalf("reader = %#v, error %v", reader, err)
|
|
}
|
|
detected, err := reader.Reader.Detect(fstest.MapFS{"test.marker": &fstest.MapFile{}})
|
|
if err != nil || !detected {
|
|
t.Fatal("reader detection failed")
|
|
}
|
|
pkg, err := reader.Reader.Read(fstest.MapFS{})
|
|
if err != nil || len(pkg.Entries) != 3 {
|
|
t.Fatal("reader did not produce neutral package")
|
|
}
|
|
writer, _ := registry.Get("110USextend")
|
|
if !writer.Descriptor.CanRead || !writer.Descriptor.CanWrite || writer.Reader == nil {
|
|
t.Fatal("writer capabilities incorrect")
|
|
}
|
|
writer.Descriptor.Operations[0] = DeleteFile
|
|
again, _ := registry.Get("110USextend")
|
|
if again.Descriptor.Operations[0] == DeleteFile {
|
|
t.Fatal("descriptor leaked mutable registry state")
|
|
}
|
|
if err := registry.Register(writer); err == nil {
|
|
t.Fatal("duplicate registry entry accepted")
|
|
}
|
|
}
|
|
|
|
func TestExtend110OutputLayout(t *testing.T) {
|
|
pkg := samplePackage()
|
|
root := t.TempDir()
|
|
writer := Extend110Writer{}
|
|
if err := writer.Write(root, pkg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files := writer.Files(pkg)
|
|
if len(files) != len(pkg.Entries)+1 || files[0].Path != "pack.json" || files[0].EntryPath != "" {
|
|
t.Fatalf("layout = %#v", files)
|
|
}
|
|
for _, file := range files {
|
|
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(file.Path))); err != nil {
|
|
t.Fatalf("declared output does not exist: %s: %v", file.Path, err)
|
|
}
|
|
}
|
|
}
|