DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

+197
View File
@@ -0,0 +1,197 @@
package script
import (
"context"
"reflect"
"strings"
"testing"
)
func runFileSetScript(t *testing.T, host *BatchAPI, source string) RunResult {
t.Helper()
result, err := NewGojaRuntime().Run(context.Background(), source, host)
if err != nil {
t.Fatal(err)
}
if result.Status != RunStatusCompleted || result.Error != nil {
t.Fatalf("run result = %#v error=%#v", result, result.Error)
}
return result
}
func TestFileSetStageChangesDedupeAndPreserveMetadata(t *testing.T) {
stage := NewFileSetStage([]FileSet{
{ID: "set-1", Name: "默认文件集", Entries: []FileSetEntry{
{Path: "equipment/a.equ", Name: "a", IDs: []string{"1008"}, Size: 12, DataType: 1},
}},
})
// Rewriting a set keeps surviving entries and drops the removed one.
if _, err := stage.Replace("默认文件集", []string{"equipment\\a.equ", "/equipment/a.equ/", "equipment/b.equ", "equipment/b.equ"}); err != nil {
t.Fatal(err)
}
fileSet, ok := stage.Lookup("默认文件集")
if !ok {
t.Fatal("file set missing after replace")
}
wantPaths := []string{"equipment/a.equ", "equipment/b.equ"}
gotPaths := []string{fileSet.Entries[0].Path, fileSet.Entries[1].Path}
if !reflect.DeepEqual(gotPaths, wantPaths) {
t.Fatalf("paths = %#v, want %#v", gotPaths, wantPaths)
}
if fileSet.Entries[0].IDs[0] != "1008" || fileSet.Entries[0].Size != 12 {
t.Fatalf("metadata not preserved: %#v", fileSet.Entries[0])
}
if fileSet.Entries[1].Name != "b.equ" {
t.Fatalf("new entry name = %q", fileSet.Entries[1].Name)
}
changes := stage.Changes()
if len(changes) != 1 || changes[0].Created {
t.Fatalf("changes = %#v", changes)
}
if len(changes[0].Entries) != 2 {
t.Fatalf("change entries = %#v", changes[0].Entries)
}
}
func TestFileSetStageOmitsUnchangedAndRejectsDuplicateCreate(t *testing.T) {
stage := NewFileSetStage([]FileSet{{ID: "set-1", Name: "默认文件集", Entries: []FileSetEntry{{Path: "a.equ"}}}})
// Replacing with an equivalent list (only spelling differs) stages nothing.
if _, err := stage.Replace("默认文件集", []string{"/a.equ"}); err != nil {
t.Fatal(err)
}
if changes := stage.Changes(); len(changes) != 0 {
t.Fatalf("unchanged rewrite staged changes = %#v", changes)
}
if _, err := stage.Create("默认文件集", nil); err == nil {
t.Fatal("creating a duplicate name should fail")
}
if _, err := stage.Replace("不存在", nil); err == nil {
t.Fatal("replacing a missing set should fail")
}
if _, err := stage.Create(" ", nil); err == nil {
t.Fatal("blank name should fail")
}
created, err := stage.Create("新建文件集", []string{"b.equ", "b.equ", ""})
if err != nil {
t.Fatal(err)
}
if len(created.Entries) != 1 {
t.Fatalf("created entries = %#v", created.Entries)
}
changes := stage.Changes()
if len(changes) != 1 || !changes[0].Created || changes[0].Name != "新建文件集" {
t.Fatalf("create changes = %#v", changes)
}
}
func TestGojaRuntimeFileSetReadAndWrite(t *testing.T) {
archive := scriptTestArchive(t)
tx := NewTransaction(archive)
host := NewBatchAPI(context.Background(), tx, nil, nil)
host.SetFileSetStage(NewFileSetStage([]FileSet{
{ID: "set-1", Name: "默认文件集", Entries: []FileSetEntry{
{Path: "equipment/a.equ", Name: "a", Size: 10, DataType: 1},
{Path: "equipment/b.equ", Name: "b", Size: 20, DataType: 1},
}},
}))
runFileSetScript(t, host, `
const set = pvf.fileset("默认文件集");
if (set === null) throw new Error("set not found");
if (set.name !== "默认文件集") throw new Error("wrong name");
const paths = set.getAll();
if (paths.length !== 2 || paths[0] !== "equipment/a.equ") throw new Error("wrong paths: " + JSON.stringify(paths));
const kept = set.setAll(["equipment/b.equ", "equipment/c.equ"]);
if (kept !== 2) throw new Error("setAll returned " + kept);
if (set.getAll().join("|") !== "equipment/b.equ|equipment/c.equ") throw new Error("setAll not applied");
const created = pvf.createFileset("新建文件集", ["equipment/a.equ"]);
if (created.name !== "新建文件集") throw new Error("wrong created name");
if (pvf.fileset("新建文件集").getAll().length !== 1) throw new Error("created set not readable");
const empty = pvf.createFileset("空文件集");
if (empty.getAll().length !== 0) throw new Error("empty set should have no paths");
if (pvf.fileset("不存在") !== null) throw new Error("unknown set should be null");
`)
changes := host.FileSetStage().Changes()
if len(changes) != 3 {
t.Fatalf("changes = %#v", changes)
}
byName := make(map[string]FileSetChange, len(changes))
for _, change := range changes {
byName[change.Name] = change
}
edited, ok := byName["默认文件集"]
if !ok || edited.Created || len(edited.Entries) != 2 {
t.Fatalf("edited change = %#v", edited)
}
// The surviving entry keeps its stored metadata across setAll.
if edited.Entries[0].Name != "b" || edited.Entries[0].Size != 20 {
t.Fatalf("surviving metadata lost: %#v", edited.Entries[0])
}
if created, ok := byName["新建文件集"]; !ok || !created.Created {
t.Fatalf("created change = %#v", created)
}
// The archive is untouched: a file set edit is not an archive mutation.
if archive.ModifiedCount() != 0 {
t.Fatalf("file set API changed the archive: %d", archive.ModifiedCount())
}
if staged, err := tx.Changes(); err != nil || len(staged) != 0 {
t.Fatalf("file set API staged archive changes = %#v err=%v", staged, err)
}
}
func TestGojaRuntimeFileSetErrorsAreReported(t *testing.T) {
archive := scriptTestArchive(t)
cases := []struct {
name string
source string
message string
}{
{"blank name", `pvf.fileset(" ");`, "文件集名称不能为空"},
{"duplicate create", `pvf.createFileset("a"); pvf.createFileset("a");`, "文件集已存在"},
{"setAll needs array", `pvf.createFileset("a").setAll("b.equ");`, "路径列表必须是数组"},
{"setAll rejects non-string", `pvf.createFileset("a").setAll([1]);`, "路径列表第 1 项必须是字符串"},
{"blank create name", `pvf.createFileset("");`, "文件集名称不能为空"},
}
for _, testCase := range cases {
// Each case needs its own stage: a create in one case would otherwise
// make the next case fail on a duplicate name instead of its own error.
host := NewBatchAPI(context.Background(), NewTransaction(archive), nil, nil)
host.SetFileSetStage(NewFileSetStage(nil))
result, err := NewGojaRuntime().Run(context.Background(), testCase.source, host)
if err != nil {
t.Fatalf("%s: unexpected error %v", testCase.name, err)
}
if result.Status != RunStatusFailed || result.Error == nil {
t.Fatalf("%s: result = %#v", testCase.name, result)
}
if !strings.Contains(result.Error.Message, testCase.message) {
t.Fatalf("%s: message = %q, want contains %q", testCase.name, result.Error.Message, testCase.message)
}
}
}
func TestGojaRuntimeFileSetUnavailableWithoutStage(t *testing.T) {
archive := scriptTestArchive(t)
host := NewBatchAPI(context.Background(), NewTransaction(archive), nil, nil)
result, err := NewGojaRuntime().Run(context.Background(), `pvf.fileset("默认文件集");`, host)
if err != nil {
t.Fatal(err)
}
if result.Status != RunStatusFailed || result.Error == nil {
t.Fatalf("result = %#v", result)
}
if !strings.Contains(result.Error.Message, "不支持文件集") {
t.Fatalf("message = %q", result.Error.Message)
}
}