Files
90CNServer/server/internal/modules/dnf/dungeonaltar/owner_test.go
T
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

175 lines
7.1 KiB
Go

package dungeonaltar
import (
"context"
"errors"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
)
func TestOwnerReservesEndlessAdmissionWithEscalatingFeeAndReplay(t *testing.T) {
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "7", AccountID: "account-1", Level: 90, Stats: map[string]int64{"gold": 100_000},
}); err != nil {
t.Fatal(err)
}
owner, err := NewOwner(repositories)
if err != nil {
t.Fatal(err)
}
rule := testAdmissionRule()
now := time.Date(2026, 9, 1, 12, 0, 0, 0, time.FixedZone("CST", 8*60*60))
first, err := owner.ReserveEndless(ctx, "7", rule, "attempt-1", now)
if err != nil || first.Fee != 22_000 || first.GoldAfter != 78_000 || first.DailyEnterCount != 1 || first.WeeklyEnterCount != 1 ||
first.LottoPointEarned != 1 || first.LottoGoldSpent != 10_000 || first.TotalLottoPoints != 1 || first.TotalLottoGold != 10_000 {
t.Fatalf("first=%+v err=%v", first, err)
}
replay, err := owner.ReserveEndless(ctx, "7", rule, "attempt-1", now)
if err != nil || !replay.Replayed || replay != (AdmissionSnapshot{
CharacterID: "7", Fee: 22_000, GoldAfter: 78_000, DailyEnterCount: 1, WeeklyEnterCount: 1,
LottoPointEarned: 1, LottoGoldSpent: 10_000, TotalLottoPoints: 1, TotalLottoGold: 10_000, Replayed: true,
}) {
t.Fatalf("replay=%+v err=%v", replay, err)
}
second, err := owner.ReserveEndless(ctx, "7", rule, "attempt-2", now)
if err != nil || second.Fee != 29_200 || second.GoldAfter != 48_800 || second.DailyEnterCount != 2 || second.WeeklyEnterCount != 2 ||
second.LottoPointEarned != 3 || second.LottoGoldSpent != 17_200 || second.TotalLottoPoints != 4 || second.TotalLottoGold != 27_200 {
t.Fatalf("second=%+v err=%v", second, err)
}
}
func TestOwnerRestoresEndlessAdmissionAndRejectsReceiptReuse(t *testing.T) {
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "8", AccountID: "account-1", Level: 60, Stats: map[string]int64{"gold": 50_000},
}); err != nil {
t.Fatal(err)
}
owner, _ := NewOwner(repositories)
now := time.Date(2026, 9, 1, 12, 0, 0, 0, time.UTC)
reserved, err := owner.ReserveEndless(ctx, "8", testAdmissionRule(), "attempt", now)
if err != nil || reserved.Fee != 22_000 {
t.Fatalf("reserved=%+v err=%v", reserved, err)
}
restored, err := owner.RestoreEndless(ctx, "8", "attempt", now)
if err != nil || restored.GoldAfter != 50_000 || restored.DailyEnterCount != 0 || restored.WeeklyEnterCount != 0 ||
restored.TotalLottoPoints != 0 || restored.TotalLottoGold != 0 {
t.Fatalf("restored=%+v err=%v", restored, err)
}
if _, err := owner.ReserveEndless(ctx, "8", testAdmissionRule(), "attempt", now); !errors.Is(err, ErrReceiptRestored) {
t.Fatalf("receipt reuse error=%v", err)
}
character, _, err := repositories.Character.Load(ctx, "8")
if err != nil || character.Stats["gold"] != 50_000 {
t.Fatalf("character=%+v err=%v", character, err)
}
}
func TestOwnerResetsDailyCountAtBeijingSixAndKeepsWeeklyCount(t *testing.T) {
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "9", AccountID: "account-1", Level: 90, Stats: map[string]int64{"gold": 200_000},
}); err != nil {
t.Fatal(err)
}
owner, _ := NewOwner(repositories)
zone := time.FixedZone("CST", 8*60*60)
before := time.Date(2026, 9, 1, 5, 59, 59, 0, zone)
a, err := owner.ReserveEndless(ctx, "9", testAdmissionRule(), "before", before)
if err != nil || a.DailyEnterCount != 1 || a.WeeklyEnterCount != 1 {
t.Fatalf("before=%+v err=%v", a, err)
}
after := time.Date(2026, 9, 1, 6, 0, 0, 0, zone)
b, err := owner.ReserveEndless(ctx, "9", testAdmissionRule(), "after", after)
if err != nil || b.DailyEnterCount != 1 || b.WeeklyEnterCount != 2 || b.Fee != 22_000 {
t.Fatalf("after=%+v err=%v", b, err)
}
}
func TestOwnerPersistsBestRoundThenLowestTime(t *testing.T) {
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "10", AccountID: "account-1"}); err != nil {
t.Fatal(err)
}
owner, _ := NewOwner(repositories)
now := time.Now()
first, err := owner.UpdateBestRecord(ctx, "10", 11006, 4, 12_000, "clear-1", now)
if err != nil || first.BestRound != 4 || first.BestTimeMillis != 12_000 || !first.Changed {
t.Fatalf("first=%+v err=%v", first, err)
}
slower, err := owner.UpdateBestRecord(ctx, "10", 11006, 4, 13_000, "clear-2", now)
if err != nil || slower.BestTimeMillis != 12_000 || slower.Changed {
t.Fatalf("slower=%+v err=%v", slower, err)
}
faster, err := owner.UpdateBestRecord(ctx, "10", 11006, 4, 11_000, "clear-3", now)
if err != nil || faster.BestTimeMillis != 11_000 || !faster.Changed {
t.Fatalf("faster=%+v err=%v", faster, err)
}
higher, err := owner.UpdateBestRecord(ctx, "10", 11006, 5, 20_000, "clear-4", now)
if err != nil || higher.BestRound != 5 || higher.BestTimeMillis != 20_000 || !higher.Changed {
t.Fatalf("higher=%+v err=%v", higher, err)
}
replay, err := owner.UpdateBestRecord(ctx, "10", 11006, 5, 20_000, "clear-4", now)
if err != nil || !replay.Replayed || replay.BestRound != 5 || replay.BestTimeMillis != 20_000 {
t.Fatalf("replay=%+v err=%v", replay, err)
}
}
func TestOwnerRejectsMalformedDurableAdmissionCounter(t *testing.T) {
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "11", AccountID: "account-1", Level: 90, Stats: map[string]int64{"gold": 100_000},
}); err != nil {
t.Fatal(err)
}
now := time.Date(2026, 9, 1, 12, 0, 0, 0, time.FixedZone("CST", 8*60*60))
values := map[string]string{}
resetAdmissionWindows(values, now)
values["daily_count"] = "not-a-number"
values["weekly_count"] = "1"
if err := repositories.Settings.Save(ctx, dnfrepo.SettingsRecord{
Scope: SettingsScope("11"),
Values: values,
}); err != nil {
t.Fatal(err)
}
owner, _ := NewOwner(repositories)
if _, err := owner.ReserveEndless(ctx, "11", testAdmissionRule(), "corrupt", now); !errors.Is(err, ErrReceiptMismatch) {
t.Fatalf("malformed counter error=%v", err)
}
character, found, err := repositories.Character.Load(ctx, "11")
if err != nil || !found || character.Stats["gold"] != 100_000 {
t.Fatalf("character=%+v found=%t err=%v", character, found, err)
}
}
func testAdmissionRule() AdmissionRule {
return AdmissionRule{
PriceBands: []PriceBand{
{MinimumLevel: 1, MaximumLevel: 50, Price: 8_000},
{MinimumLevel: 51, MaximumLevel: 55, Price: 10_000},
{MinimumLevel: 56, MaximumLevel: 99, Price: 12_000},
},
AdmissionIncrease: []AdmissionIncrease{
{UpperExclusiveEnterCount: 1, Percent: 0},
{UpperExclusiveEnterCount: 2, Percent: 60},
{UpperExclusiveEnterCount: 3, Percent: 200},
},
LottoPoints: []LottoPoint{
{UpperExclusiveEnterCount: 1, Points: 1},
{UpperExclusiveEnterCount: 2, Points: 3},
{UpperExclusiveEnterCount: 3, Points: 5},
},
LottoCost: 10_000,
}
}