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

323 lines
10 KiB
Go

package onlineevent
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
)
type attendanceCatalogTestSource map[string]string
func (s attendanceCatalogTestSource) ReadText(relativePath string) (string, error) {
text, ok := s[relativePath]
if !ok {
return "", fmt.Errorf("missing %s", relativePath)
}
return text, nil
}
type cancelingAttendanceCatalogSource struct {
attendanceCatalogTestSource
cancel context.CancelFunc
cancelPath string
}
func (s cancelingAttendanceCatalogSource) ReadText(relativePath string) (string, error) {
text, err := s.attendanceCatalogTestSource.ReadText(relativePath)
if err == nil && relativePath == s.cancelPath {
s.cancel()
}
return text, err
}
func TestLoadAttendancePVFCatalogPreservesRawSections(t *testing.T) {
catalog, err := LoadAttendancePVFCatalog(context.Background(), validAttendanceCatalogSource())
if err != nil {
t.Fatal(err)
}
snapshot := catalog.Snapshot()
if !reflect.DeepEqual(snapshot.ProcessDurationsSeconds, []int64{1800, 1800, 3600, 3600}) {
t.Fatalf("raw process durations=%v", snapshot.ProcessDurationsSeconds)
}
if !reflect.DeepEqual(snapshot.SumThresholds, []int64{5, 10, 15, 25}) {
t.Fatalf("raw sum thresholds=%v", snapshot.SumThresholds)
}
if !reflect.DeepEqual(snapshot.RewardActivityForSum, []int64{0}) {
t.Fatalf("raw reward activity for sum=%v", snapshot.RewardActivityForSum)
}
wantRewards := []AttendancePVFItemReward{
{ItemID: 490006574, Count: 1, DefinitionPath: "stackable/490006001/chn_490006574.stk"},
{ItemID: 490006575, Count: 1, DefinitionPath: "stackable/490006001/chn_490006575.stk"},
{ItemID: 490006576, Count: 1, DefinitionPath: "stackable/490006001/chn_490006576.stk"},
{ItemID: 490006577, Count: 1, DefinitionPath: "stackable/490006001/chn_490006577.stk"},
}
if !reflect.DeepEqual(snapshot.RewardItems, wantRewards) {
t.Fatalf("reward items=%+v", snapshot.RewardItems)
}
wantSumRewards := []AttendancePVFItemReward{
{ItemID: 490006578, Count: 1, DefinitionPath: "stackable/490006001/chn_490006578.stk"},
{ItemID: 490006579, Count: 1, DefinitionPath: "stackable/490006001/chn_490006579.stk"},
{ItemID: 490006580, Count: 1, DefinitionPath: "stackable/490006001/chn_490006580.stk"},
{ItemID: 490006581, Count: 1, DefinitionPath: "stackable/490006001/chn_490006581.stk"},
}
if !reflect.DeepEqual(snapshot.RewardItemsForSum, wantSumRewards) {
t.Fatalf("sum reward items=%+v", snapshot.RewardItemsForSum)
}
// Every accessor result must be detached from the immutable catalog.
snapshot.ProcessDurationsSeconds[0] = 999
snapshot.RewardItems[0].ItemID = 1
snapshot.SumThresholds[0] = 999
snapshot.RewardItemsForSum[0].DefinitionPath = "changed"
snapshot.RewardActivityForSum[0] = 999
again := catalog.Snapshot()
if again.ProcessDurationsSeconds[0] != 1800 || again.RewardItems[0].ItemID != 490006574 ||
again.SumThresholds[0] != 5 || again.RewardItemsForSum[0].DefinitionPath != "stackable/490006001/chn_490006578.stk" ||
again.RewardActivityForSum[0] != 0 {
t.Fatalf("catalog leaked mutable state: %+v", again)
}
}
func TestLoadAttendancePVFCatalogRejectsMalformedSections(t *testing.T) {
valid := validAttendanceCatalogSource()[AttendanceEventPVFPath]
tests := []struct {
name string
text string
}{
{
name: "missing required section",
text: strings.Replace(valid, "[reward activity for sum]\n0\n[/reward activity for sum]\n", "", 1),
},
{
name: "duplicate section",
text: valid + "\n[process time seconds]\n1 2 3 4\n[/process time seconds]\n",
},
{
name: "missing closing section",
text: strings.Replace(valid, "[/reward activity for sum]\n", "", 1),
},
{
name: "unexpected section",
text: valid + "\n[guessed event id]\n1\n[/guessed event id]\n",
},
{
name: "mismatched closing section order",
text: swapAttendanceClosingMarkers(valid, "process time seconds", "reward item"),
},
{
name: "closing section contains a value",
text: strings.Replace(valid, "[/process time seconds]\n", "[/process time seconds]\n999\n", 1),
},
{
name: "root value before first section",
text: "999\n" + valid,
},
{
name: "non integer duration",
text: strings.Replace(valid, "1800 1800 3600 3600", "1800 invalid 3600 3600", 1),
},
{
name: "wrong duration count",
text: strings.Replace(valid, "1800 1800 3600 3600", "1800 1800 3600", 1),
},
{
name: "non positive duration",
text: strings.Replace(valid, "1800 1800 3600 3600", "1800 0 3600 3600", 1),
},
{
name: "non positive reward item",
text: strings.Replace(valid, "490006574 1", "0 1", 1),
},
{
name: "non positive reward count",
text: strings.Replace(valid, "490006574 1", "490006574 0", 1),
},
{
name: "wrong sum reward count",
text: strings.Replace(valid, "490006578 1 490006579 1 490006580 1 490006581 1", "490006578 1 490006579 1 490006580 1", 1),
},
{
name: "non positive sum threshold",
text: strings.Replace(valid, "5 10 15 25", "5 10 0 25", 1),
},
{
name: "wrong raw activity shape",
text: strings.Replace(valid, "[reward activity for sum]\n0", "[reward activity for sum]\n0 1", 1),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := validAttendanceCatalogSource()
source[AttendanceEventPVFPath] = test.text
_, err := LoadAttendancePVFCatalog(context.Background(), source)
if !errors.Is(err, ErrAttendancePVFMalformed) {
t.Fatalf("error=%v, want %v", err, ErrAttendancePVFMalformed)
}
})
}
}
func TestLoadAttendancePVFCatalogRequiresListedReadableDefinitions(t *testing.T) {
tests := []struct {
name string
mutate func(attendanceCatalogTestSource)
want error
}{
{
name: "reward absent from stackable list",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] = strings.Replace(
source[attendanceStackableListPath],
"490006574 `490006001/chn_490006574.stk`\n",
"",
1,
)
},
want: ErrAttendanceRewardUnresolved,
},
{
name: "duplicate reward list entry",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] += "490006574 `other/duplicate.stk`\n"
},
want: ErrAttendancePVFMalformed,
},
{
name: "definition is unreadable",
mutate: func(source attendanceCatalogTestSource) {
delete(source, "stackable/490006001/chn_490006574.stk")
},
want: ErrAttendanceRewardUnresolved,
},
{
name: "definition escapes stackable root",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] = strings.Replace(
source[attendanceStackableListPath],
"490006574 `490006001/chn_490006574.stk`",
"490006574 `../equipment/invalid.equ`",
1,
)
},
want: ErrAttendanceRewardUnresolved,
},
{
name: "definition normalizes outside stackable root",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] = strings.Replace(
source[attendanceStackableListPath],
"490006574 `490006001/chn_490006574.stk`",
"490006574 `stackable/../equipment/invalid.stk`",
1,
)
},
want: ErrAttendanceRewardUnresolved,
},
{
name: "definition uses an absolute drive path",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] = strings.Replace(
source[attendanceStackableListPath],
"490006574 `490006001/chn_490006574.stk`",
"490006574 `C:/outside/invalid.stk`",
1,
)
},
want: ErrAttendanceRewardUnresolved,
},
{
name: "definition is readable but not stackable extension",
mutate: func(source attendanceCatalogTestSource) {
source[attendanceStackableListPath] = strings.Replace(
source[attendanceStackableListPath],
"490006574 `490006001/chn_490006574.stk`",
"490006574 `490006001/chn_490006574.equ`",
1,
)
source["stackable/490006001/chn_490006574.equ"] = "[name] `wrong kind`\n"
},
want: ErrAttendanceRewardUnresolved,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
source := validAttendanceCatalogSource()
test.mutate(source)
_, err := LoadAttendancePVFCatalog(context.Background(), source)
if !errors.Is(err, test.want) {
t.Fatalf("error=%v, want %v", err, test.want)
}
})
}
}
func TestLoadAttendancePVFCatalogHonorsCanceledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := LoadAttendancePVFCatalog(ctx, validAttendanceCatalogSource())
if !errors.Is(err, context.Canceled) {
t.Fatalf("error=%v, want context canceled", err)
}
}
func TestLoadAttendancePVFCatalogHonorsCancellationDuringLastDefinitionRead(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
source := cancelingAttendanceCatalogSource{
attendanceCatalogTestSource: validAttendanceCatalogSource(),
cancel: cancel,
cancelPath: "stackable/490006001/chn_490006581.stk",
}
_, err := LoadAttendancePVFCatalog(ctx, source)
if !errors.Is(err, context.Canceled) {
t.Fatalf("error=%v, want context canceled", err)
}
}
func swapAttendanceClosingMarkers(text string, left string, right string) string {
placeholder := "[/__attendance_swap_marker__]"
text = strings.Replace(text, "[/"+left+"]", placeholder, 1)
text = strings.Replace(text, "[/"+right+"]", "[/"+left+"]", 1)
return strings.Replace(text, placeholder, "[/"+right+"]", 1)
}
func validAttendanceCatalogSource() attendanceCatalogTestSource {
source := attendanceCatalogTestSource{
AttendanceEventPVFPath: `[process time seconds]
1800 1800 3600 3600
[/process time seconds]
[reward item]
490006574 1 490006575 1 490006576 1 490006577 1
[/reward item]
[process seconds for max count]
5 10 15 25
[/process seconds for max count]
[reward item for sum]
490006578 1 490006579 1 490006580 1 490006581 1
[/reward item for sum]
[reward activity for sum]
0
[/reward activity for sum]
`,
attendanceStackableListPath: `490006574 ` + "`490006001/chn_490006574.stk`" + `
490006575 ` + "`490006001/chn_490006575.stk`" + `
490006576 ` + "`490006001/chn_490006576.stk`" + `
490006577 ` + "`490006001/chn_490006577.stk`" + `
490006578 ` + "`490006001/chn_490006578.stk`" + `
490006579 ` + "`490006001/chn_490006579.stk`" + `
490006580 ` + "`490006001/chn_490006580.stk`" + `
490006581 ` + "`490006001/chn_490006581.stk`" + `
`,
}
for itemID := int64(490006574); itemID <= 490006581; itemID++ {
source[fmt.Sprintf("stackable/490006001/chn_%d.stk", itemID)] = fmt.Sprintf("[name] `reward-%d`\n", itemID)
}
return source
}