DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,357 @@
|
||||
package mod
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
)
|
||||
|
||||
func cn115Fixture(t *testing.T) (cn115Manifest, fstest.MapFS) {
|
||||
t.Helper()
|
||||
m := cn115Manifest{
|
||||
Schema: 1, Client: "115CN", Kind: "resource", ID: "unique-test-mod", Name: "测试 MOD", Version: "1.0.0",
|
||||
Components: []Component{
|
||||
{ID: "base", Title: "基础", DefaultSelected: true},
|
||||
{ID: "optional", Title: "可选", Requires: []string{"base"}},
|
||||
},
|
||||
PVF: []cn115Operation{
|
||||
{Component: "base", Source: "payload/item.stk", Path: "stackable/demo.stk", Mode: "replace", Type: 1},
|
||||
{Component: "base", Source: "payload/list.lst", Path: "list/stackable.lst", Mode: "merge", Type: 1},
|
||||
{Component: "optional", Source: "payload/text.str", Path: "string/demo.str", Mode: "merge", Type: 3},
|
||||
{Component: "optional", Source: "payload/new.stk", Path: "stackable/new.stk", Mode: "add"},
|
||||
},
|
||||
}
|
||||
files := fstest.MapFS{
|
||||
"payload/item.stk": {Data: []byte("[price]\n200")},
|
||||
"payload/new.stk": {Data: []byte("[price]\n300")},
|
||||
"payload/list.lst": {Data: []byte("2\t`stackable/new.stk`\r\n")},
|
||||
"payload/text.str": {Data: []byte("name> 新名称 > \r\nempty>\r\n")},
|
||||
}
|
||||
setCN115Manifest(t, files, m)
|
||||
return m, files
|
||||
}
|
||||
|
||||
func setCN115Manifest(t *testing.T, files fstest.MapFS, m cn115Manifest) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files["mod.json"] = &fstest.MapFile{Data: data}
|
||||
}
|
||||
|
||||
func TestCN115ReadAndSelection(t *testing.T) {
|
||||
_, files := cn115Fixture(t)
|
||||
for _, prefix := range []string{"", "中文包/"} {
|
||||
t.Run(prefix, func(t *testing.T) {
|
||||
root := fstest.MapFS{}
|
||||
for p, f := range files {
|
||||
root[prefix+p] = f
|
||||
}
|
||||
pkg, err := (CN115Reader{}).Read(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaults, ids, err := SelectComponents(pkg, nil)
|
||||
if err != nil || len(ids) != 1 || ids[0] != "base" || len(defaults.Entries) != 2 {
|
||||
t.Fatalf("defaults: %+v %v %v", defaults, ids, err)
|
||||
}
|
||||
all, _, err := SelectComponents(pkg, []string{"base", "optional"})
|
||||
if err != nil || len(all.Entries) != 4 || all.Entries[3].Operation != AddFile || all.Entries[2].Pairs[0].Value != " 新名称 > " {
|
||||
t.Fatalf("all: %+v %v", all, err)
|
||||
}
|
||||
empty, _, err := SelectComponents(pkg, []string{})
|
||||
if err != nil || len(empty.Entries) != 0 {
|
||||
t.Fatal("empty selection became defaults", err)
|
||||
}
|
||||
for _, ids := range [][]string{{"optional"}, {"unknown"}, {"base", "base"}} {
|
||||
if _, _, err := SelectComponents(pkg, ids); err == nil {
|
||||
t.Fatalf("accepted invalid selection %v", ids)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115RejectMalformedPackages(t *testing.T) {
|
||||
for _, kind := range []string{"schema", "client", "kind", "id", "component", "cycle", "missing-dependency", "target", "uppercase", "source", "sensitive", "missing-source", "patch", "type", "merge-type", "merge-extension", "duplicate-key", "encoding", "parent-symlink", "file-symlink", "nested-root"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
m, files := cn115Fixture(t)
|
||||
switch kind {
|
||||
case "schema":
|
||||
m.Schema = 2
|
||||
case "client":
|
||||
m.Client = "110US"
|
||||
case "kind":
|
||||
m.Kind = "script"
|
||||
case "id":
|
||||
m.ID = "Not-Unique"
|
||||
case "component":
|
||||
m.PVF[0].Component = "unknown"
|
||||
case "cycle":
|
||||
m.Components[0].Requires = []string{"optional"}
|
||||
case "missing-dependency":
|
||||
m.Components[0].Requires = []string{"unknown"}
|
||||
case "target":
|
||||
m.PVF[0].Path = "../escape.stk"
|
||||
case "uppercase":
|
||||
m.PVF[0].Path = "Stackable/demo.stk"
|
||||
case "source":
|
||||
m.PVF[0].Source = "../escape"
|
||||
case "sensitive":
|
||||
m.PVF[0].Source = ".env"
|
||||
case "missing-source":
|
||||
delete(files, "payload/item.stk")
|
||||
case "patch":
|
||||
m.PVF[0].Mode = "patch"
|
||||
case "type":
|
||||
m.PVF[0].Type = 9
|
||||
case "merge-type":
|
||||
m.PVF[1].Type = 3
|
||||
case "merge-extension":
|
||||
m.PVF[1].Path = "etc/merge.etc"
|
||||
case "duplicate-key":
|
||||
files["payload/list.lst"].Data = []byte("2 `a`\n2 `b`")
|
||||
case "encoding":
|
||||
files["payload/item.stk"].Data = []byte{0xff}
|
||||
case "parent-symlink":
|
||||
files["payload"] = &fstest.MapFile{Mode: fs.ModeSymlink}
|
||||
case "file-symlink":
|
||||
files["payload/item.stk"].Mode = fs.ModeSymlink
|
||||
case "nested-root":
|
||||
nested := fstest.MapFS{}
|
||||
setCN115Manifest(t, files, m)
|
||||
for p, f := range files {
|
||||
nested["one/two/"+p] = f
|
||||
}
|
||||
files = nested
|
||||
}
|
||||
if kind != "nested-root" {
|
||||
setCN115Manifest(t, files, m)
|
||||
}
|
||||
if _, err := (CN115Reader{}).Read(files); err == nil {
|
||||
t.Fatal("accepted malformed package")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115SelectionConflicts(t *testing.T) {
|
||||
m, files := cn115Fixture(t)
|
||||
m.Components[0].ExclusiveGroup = "variant"
|
||||
m.Components[1].ExclusiveGroup = "variant"
|
||||
setCN115Manifest(t, files, m)
|
||||
pkg, err := (CN115Reader{}).Read(files)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := SelectComponents(pkg, []string{"base", "optional"}); err == nil {
|
||||
t.Fatal("exclusive group ignored")
|
||||
}
|
||||
pkg.Components[0].ExclusiveGroup = ""
|
||||
pkg.Components[1].ExclusiveGroup = ""
|
||||
pkg.Entries[3].Path = pkg.Entries[0].Path
|
||||
if _, _, err := SelectComponents(pkg, []string{"base", "optional"}); err == nil {
|
||||
t.Fatal("duplicate target ignored")
|
||||
}
|
||||
if _, _, err := SelectComponents(pkg, []string{"base"}); err != nil {
|
||||
t.Fatal("unselected alternative blocked selection", err)
|
||||
}
|
||||
pkg.Entries[3].Path = "stackable/demo.stk/child"
|
||||
if _, _, err := SelectComponents(pkg, []string{"base", "optional"}); err == nil {
|
||||
t.Fatal("file/directory conflict ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115WriteRoundTrip(t *testing.T) {
|
||||
pkg := samplePackage()
|
||||
pkg.Metadata.ID = "my-unique-mod"
|
||||
pkg.Metadata.Version = "2.0.0"
|
||||
writer := CN115Writer{}
|
||||
if err := writer.Validate(pkg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := writer.Write(dir, pkg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var data bytes.Buffer
|
||||
if err := writer.WriteArchive(&data, pkg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
z, err := zip.NewReader(bytes.NewReader(data.Bytes()), int64(data.Len()))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, root := range []fs.FS{os.DirFS(dir), z} {
|
||||
got, err := (CN115Reader{}).Read(root)
|
||||
if err != nil || got.Metadata != pkg.Metadata || len(got.Entries) != len(pkg.Entries) {
|
||||
t.Fatalf("roundtrip: %+v %v", got, err)
|
||||
}
|
||||
for i, entry := range got.Entries {
|
||||
if entry.Path != pkg.Entries[i].Path || entry.Operation != pkg.Entries[i].Operation {
|
||||
t.Fatalf("entry changed: %+v", entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
pkg.Metadata.ID = ""
|
||||
if err := writer.Validate(pkg); err == nil {
|
||||
t.Fatal("missing unique ID accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115ZIPSource(t *testing.T) {
|
||||
_, files := cn115Fixture(t)
|
||||
prefix, err := simplifiedchinese.GB18030.NewEncoder().String("中文包")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, kind := range []string{"root", "wrapper", "gbk", "traversal", "case-collision", "symlink", "file-directory"} {
|
||||
t.Run(kind, func(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
z := zip.NewWriter(&buffer)
|
||||
for p, f := range files {
|
||||
name := p
|
||||
switch kind {
|
||||
case "wrapper":
|
||||
name = "中文包/" + p
|
||||
case "gbk":
|
||||
name = prefix + "/" + p
|
||||
}
|
||||
header := &zip.FileHeader{Name: name, NonUTF8: kind == "gbk"}
|
||||
w, err := z.CreateHeader(header)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(f.Data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
switch kind {
|
||||
case "traversal":
|
||||
_, err = z.Create("../escape")
|
||||
case "case-collision":
|
||||
_, err = z.Create("MOD.JSON")
|
||||
case "symlink":
|
||||
header := &zip.FileHeader{Name: "link"}
|
||||
header.SetMode(fs.ModeSymlink | 0777)
|
||||
_, err = z.CreateHeader(header)
|
||||
case "file-directory":
|
||||
_, err = z.Create("payload")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := z.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filename := filepath.Join(t.TempDir(), "package.dnfmod")
|
||||
if err := os.WriteFile(filename, buffer.Bytes(), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root, closer, err := OpenSource(filename, true)
|
||||
valid := kind == "root" || kind == "wrapper" || kind == "gbk"
|
||||
if valid {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer closer.Close()
|
||||
if _, err := (CN115Reader{}).Read(root); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
} else if err == nil {
|
||||
closer.Close()
|
||||
t.Fatal("unsafe ZIP accepted")
|
||||
}
|
||||
if _, closer, err := OpenSource(filename, false); err == nil {
|
||||
closer.Close()
|
||||
t.Fatal("ZIP accepted by directory-only format")
|
||||
}
|
||||
jar := filename + ".jar"
|
||||
if err := os.WriteFile(jar, buffer.Bytes(), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, closer, err := OpenSource(jar, true); err == nil {
|
||||
closer.Close()
|
||||
t.Fatal("JAR accepted as a MOD ZIP")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115ZIPSizeLimit(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
z := zip.NewWriter(&buffer)
|
||||
if _, err := z.CreateRaw(&zip.FileHeader{Name: "huge.stk", UncompressedSize64: maxPayloadSize + 1}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := z.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
filename := filepath.Join(t.TempDir(), "oversize.zip")
|
||||
if err := os.WriteFile(filename, buffer.Bytes(), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, closer, err := OpenSource(filename, true); err == nil {
|
||||
closer.Close()
|
||||
t.Fatal("ZIP declared expansion limit ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115ResourceWarningsAndLimits(t *testing.T) {
|
||||
m, files := cn115Fixture(t)
|
||||
size := int64(20)
|
||||
m.Files = []cn115Resource{{Component: "base", Source: "payload/resource.npk", Target: "ImagePacks2/unique.npk", Size: &size, SHA256: strings.Repeat("a", 64)}}
|
||||
m.Requires = []string{"other-mod"}
|
||||
setCN115Manifest(t, files, m)
|
||||
pkg, err := (CN115Reader{}).Read(files)
|
||||
if err != nil || len(pkg.Warnings) != 2 {
|
||||
t.Fatal("missing resource/dependency warnings", pkg.Warnings, err)
|
||||
}
|
||||
for _, target := range []string{"../a.npk", "bin/file.dll", "SoundPacks/sub/file.npk", "Stream/Video/movie.exe"} {
|
||||
m.Files[0].Target = target
|
||||
setCN115Manifest(t, files, m)
|
||||
if _, err := (CN115Reader{}).Read(files); err == nil {
|
||||
t.Fatalf("accepted resource target %s", target)
|
||||
}
|
||||
}
|
||||
if _, err := readPackageFile(files, "payload/item.stk", 2); err == nil {
|
||||
t.Fatal("payload limit ignored")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCN115RealMOD(t *testing.T) {
|
||||
filename := os.Getenv("PVF_MOD_TESTFILE")
|
||||
if filename == "" {
|
||||
t.Skip("PVF_MOD_TESTFILE required")
|
||||
}
|
||||
root, closer, err := OpenSource(filename, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer closer.Close()
|
||||
pkg, err := (CN115Reader{}).Read(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pkg, ids, err := SelectComponents(pkg, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
counts := map[Operation]int{}
|
||||
for _, entry := range pkg.Entries {
|
||||
counts[entry.Operation]++
|
||||
}
|
||||
t.Logf("MOD: %s %s; selected=%v entries=%d operations=%v", pkg.Metadata.Name, pkg.Metadata.Version, ids, len(pkg.Entries), counts)
|
||||
if len(pkg.Entries) == 0 {
|
||||
t.Fatal("no PVF operations")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user