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

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

253 lines
9.5 KiB
Go

package services
import (
"os"
"strconv"
"strings"
"testing"
"unicode/utf16"
"pvfine/internal/pvf"
)
const upgradeTestRow = "2 0.018 0.1 0.033 0.3 0.6 12500 2 3 3037 10 1 0.018 0.1 0.033 0.17 0.6"
// Representative 24-column rows from the supplied normal table: +1, +11, +18.
const upgradeTypedRow = "2.0 0 0 0 0 0.6 0 0.828 0 0 1 0 1 0 3037 10 0.3 0.018 0.1 0.033 0.17 0.228 0 0.2622"
const upgradeTypedDowngradeRow = "26.9 0 0 0 0 6.75 0 9.703125 75000 75000 2 3 3 0 3037 120 3 0.22 1.9 0.645 3.17 2.565 0 2.94975"
const upgradeTypedDifferentRatesRow = "74.3 0 0 0 0 63 0 90.5625 90000 88000 3 0 3 0 3037 260 8.2 0.62 10.3 3.501 17.17 23.94 0 27.531"
func TestUpgradeTypedRowsAndIndependentFields(t *testing.T) {
text := "# 😀\r\n[table]\r\n[table type]\r\n`event` `unrelated`\r\n[/table]\r\n" +
"[table]\r\n[table type]\r\n`normal` " + upgradeTypedRow + "\r\n" + upgradeTypedDowngradeRow + "\r\n" + upgradeTypedDifferentRatesRow + "\r\n[/table]\r\n" +
"[table]\r\n[table type]\r\n`amplify` " + strings.Replace(upgradeTypedRow, "3037", "3038", 1) + "\r\n[/table]"
doc, err := parseUpgrade(text, "normal")
if err != nil || len(doc.Rows) != 3 {
t.Fatalf("normal: %v %v", doc, err)
}
first, downgrade, different := doc.Rows[0], doc.Rows[1], doc.Rows[2]
if first.Fields[3].Value != 3037 || first.Fields[4].Value != 10 {
t.Fatal("incorrect material fields")
}
if downgrade.Fields[1].Value != 2 || downgrade.Fields[2].Value != 3 || downgrade.FieldsT2[1].Value != 3 || downgrade.FieldsT2[2].Value != 0 {
t.Fatal("T1/T2 operations mixed")
}
if different.Fields[0].Value != 90000 || different.FieldsT2[0].Value != 88000 {
t.Fatal("T1/T2 probabilities mixed")
}
source := utf16.Encode([]rune(strings.ReplaceAll(text, "\r\n", "\n")))
for _, row := range doc.Rows {
for _, field := range append(append([]SkillNumber(nil), row.Fields...), row.FieldsT2...) {
if got := string(utf16.Decode(source[field.Start:field.End])); got != strconv.FormatInt(int64(field.Value), 10) {
t.Fatalf("wrong field position: %s", got)
}
}
}
amplify, err := parseUpgrade(text, "amplify")
if err != nil || len(amplify.Rows) != 1 || amplify.Rows[0].Fields[3].Value != 3038 {
t.Fatalf("amplify: %v %v", amplify, err)
}
large, err := parseUpgrade("[table]\n[table type]\n`normal` "+strings.Repeat(upgradeTypedRow+"\n", 250)+"[/table]", "normal")
if err != nil || len(large.Rows) != 250 || large.Rows[249].Level != 250 {
t.Fatal("typed levels truncated")
}
if _, err := parseUpgrade("[table]\n[table type]\n`normal` "+upgradeTestRow+"\n[/table]", "normal"); err == nil {
t.Fatal("accepted wrong typed row width")
}
}
func TestUpgradeParseSourceAndRange(t *testing.T) {
text := "#PVF_File\r\n[name]\r\n`😀`\r\n[table]\r\n" + strings.Repeat(upgradeTestRow+"\r\n", 32) + "[/table]\r\n[other]\r\n99"
doc, err := parseUpgrade(text, "normal")
if err != nil || len(doc.Rows) != 31 {
t.Fatalf("rows: %v %v", doc, err)
}
row := doc.Rows[0]
if row.Level != 1 || row.Fields[0].Value != 12500 || row.Fields[3].Value != 3037 || row.Fields[4].Value != 10 {
t.Fatalf("fields: %+v", row)
}
source := utf16.Encode([]rune(strings.ReplaceAll(text, "\r\n", "\n")))
field := row.Fields[0]
if string(utf16.Decode(source[field.Start:field.End])) != "12500" {
t.Fatal("invalid UTF-16 source positions")
}
}
func TestUpgradeRejectMalformed(t *testing.T) {
for _, text := range []string{
"[other]\n1", "[table]\n", "[table]\n1 2",
"[table]\n" + upgradeTestRow + "\n[table]\n" + upgradeTestRow,
"[table]\n" + strings.Replace(upgradeTestRow, "12500", "100001", 1),
"[table]\n" + strings.Replace(upgradeTestRow, "12500", "1.0", 1),
"[table]\n" + strings.Replace(upgradeTestRow, "3037", "-1", 1),
"[table]\n" + strings.Replace(upgradeTestRow, "12500 2 3", "12500 4 3", 1),
} {
if _, err := parseUpgrade(text, "normal"); err == nil {
t.Fatalf("accepted malformed data: %q", text)
}
}
}
func TestUpgradeReadDraftAndPaths(t *testing.T) {
a := pvf.New()
text := "[table]\n" + upgradeTestRow + "\n[/table]"
indexes := []int32{}
for _, path := range []string{"etc/upgrade.etc", "etc/amplifyupgrade.etc", "etc/other.etc"} {
i, err := a.AddFileText(path, text, pvf.TypeScript)
if err != nil {
t.Fatal(err)
}
indexes = append(indexes, i)
}
c := NewCore()
if err := c.setArchive(a); err != nil {
t.Fatal(err)
}
t.Cleanup(c.closeArchive)
s := NewFileGUIService(c)
draft := strings.Replace(text, "12500", "0", 1)
for _, i := range indexes[:2] {
doc, err := s.ReadUpgrade(i, draft)
if err != nil || doc.Rows[0].Fields[0].Value != 0 {
t.Fatalf("draft: %v %v", doc, err)
}
original, _ := a.Text(i)
if !strings.Contains(original, "12500") {
t.Fatal("read mutated archive")
}
}
if _, err := s.ReadUpgrade(indexes[2], draft); err == nil {
t.Fatal("accepted unrelated path")
}
if _, err := s.ReadUpgrade(-1, draft); err == nil {
t.Fatal("accepted invalid index")
}
// Both documents may contain both types; choose by the file being edited.
typedDraft := "[table]\n[table type]\n`normal` " + upgradeTypedRow + "\n[/table]\n" +
"[table]\n[table type]\n`amplify` " + strings.Replace(upgradeTypedRow, "3037", "3038", 1) + "\n[/table]"
for n, i := range indexes[:2] {
doc, err := s.ReadUpgrade(i, typedDraft)
if err != nil || doc.Rows[0].Fields[3].Value != float64(3037+n) {
t.Fatalf("file type selection: %v %v", doc, err)
}
}
}
func TestUpgradeSelectTypedTables(t *testing.T) {
text := "# 😀\r\n[table]\r\n[table type]\r\n`event` `other data`\r\n[/table]\r\n[table]\r\n[table type]\r\n`normal` 2.0 20000 3037\r\n[/table]\r\n[table]\r\n[table type]\r\n`amplify` 4.0 40000 3038\r\n[/table]"
for _, kind := range []string{"normal", "amplify"} {
values, typed, err := upgradeTableTokens(text, kind)
if err != nil || !typed || len(values) != 3 {
t.Fatalf("%s: %v %v", kind, values, err)
}
want := "20000"
if kind == "amplify" {
want = "40000"
}
if values[1].Value != want {
t.Fatalf("wrong table selected: %v", values)
}
source := utf16.Encode([]rune(strings.ReplaceAll(text, "\r\n", "\n")))
if string(utf16.Decode(source[values[1].Start:values[1].End])) != want {
t.Fatal("source offsets lost")
}
}
for _, candidate := range []string{
"[table]\n[table type]\n`event` 1\n[/table]",
"[table]\n[table type]\n`normal` 1\n[/table]\n[table]\n[table type]\n`normal` 2\n[/table]",
} {
if _, _, err := upgradeTableTokens(candidate, "normal"); err == nil {
t.Fatal("accepted missing or ambiguous target")
}
}
}
const upgrade19Row1 = "2.2 0.018 0.1 0.033 0.2 0.6 0 0 1 0 3037 10 1 0.018 0.1 0.033 0.17 0.228 0"
const upgrade19Row5 = "7.7 0.128 0.7 0.233 1.2 1.8 0 10000 2 1 3037 50 5 0.09 0.5 0.169 0.83 0.684 0"
const upgrade19Row11 = "12.1 0.22 1.2 0.4 2 2.7 0 70000 3 0 3037 120 11 0.144 0.8 0.27 1.33 1.026 0"
func TestUpgrade19ColumnsRowsAndFields(t *testing.T) {
text := "# 😀\r\n[table]\r\n[table type]\r\n`normal`\r\n" + upgrade19Row1 + "\r\n" + upgrade19Row5 + "\r\n" + upgrade19Row11 + "\r\n[/table]"
doc, err := parseUpgrade(text, "normal")
if err != nil || len(doc.Rows) != 3 {
t.Fatalf("parseUpgrade 19 columns: %v %v", doc, err)
}
r1, r5, r11 := doc.Rows[0], doc.Rows[1], doc.Rows[2]
if r1.Level != 1 || r1.Fields[0].Value != 0 || r1.Fields[1].Value != 1 || r1.Fields[2].Value != 0 || r1.Fields[3].Value != 3037 || r1.Fields[4].Value != 10 {
t.Fatalf("row 1 fields wrong: %+v", r1)
}
if len(r1.FieldsT2) != 0 {
t.Fatalf("row 1 should not have FieldsT2: %+v", r1.FieldsT2)
}
if r5.Level != 2 || r5.Fields[0].Value != 10000 || r5.Fields[1].Value != 2 || r5.Fields[2].Value != 1 || r5.Fields[3].Value != 3037 || r5.Fields[4].Value != 50 {
t.Fatalf("row 5 fields wrong: %+v", r5)
}
if r11.Level != 3 || r11.Fields[0].Value != 70000 || r11.Fields[1].Value != 3 || r11.Fields[2].Value != 0 || r11.Fields[4].Value != 120 {
t.Fatalf("row 11 fields wrong: %+v", r11)
}
source := utf16.Encode([]rune(strings.ReplaceAll(text, "\r\n", "\n")))
for _, row := range doc.Rows {
for _, field := range row.Fields {
if got := string(utf16.Decode(source[field.Start:field.End])); got != strconv.FormatInt(int64(field.Value), 10) {
t.Fatalf("wrong field position: %s, want %d", got, int64(field.Value))
}
}
}
large19, err := parseUpgrade("[table]\n[table type]\n`normal` "+strings.Repeat(upgrade19Row1+"\n", 255)+"[/table]", "normal")
if err != nil || len(large19.Rows) != 255 || large19.Rows[254].Level != 255 {
t.Fatalf("19 column 255 levels truncated: %v", err)
}
}
func TestUpgradeRealArchive86JP(t *testing.T) {
testFile := os.Getenv("PVF_TESTFILE")
if testFile == "" {
t.Skip("PVF_TESTFILE not set")
}
a, err := pvf.Open(testFile)
if err != nil {
t.Fatal(err)
}
defer a.Release()
core := NewCore()
if err := core.setArchive(a); err != nil {
t.Fatal(err)
}
defer core.closeArchive()
gui := NewFileGUIService(core)
for _, path := range []string{"etc/upgrade.etc", "etc/amplifyupgrade.etc"} {
idx, ok := a.Find(path)
if !ok {
t.Logf("archive does not have %s", path)
continue
}
text, err := a.Text(idx)
if err != nil {
t.Fatal(err)
}
doc, err := gui.ReadUpgrade(idx, text)
if err != nil {
t.Fatalf("ReadUpgrade %s failed: %v", path, err)
}
if len(doc.Rows) != 255 {
t.Fatalf("%s rows = %d, want 255", path, len(doc.Rows))
}
if doc.Rows[0].Fields[3].Value <= 0 {
t.Fatalf("%s material ID invalid: %v", path, doc.Rows[0].Fields[3].Value)
}
if doc.Rows[0].CostName == "" {
t.Logf("%s note: costName empty (no annotation database or unknown item)", path)
} else {
t.Logf("%s item 1 costName: %s", path, doc.Rows[0].CostName)
}
}
}