服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
323 lines
10 KiB
Go
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
|
|
}
|