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

@@ -0,0 +1,212 @@
package onlineevent
import (
"context"
"errors"
"fmt"
"os"
"sync"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/repository/memory"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestRealPCRoomTimePointPVFCatalog(t *testing.T) {
p := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if p == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE")
}
a, err := platformpvf.LoadArchive(platformpvf.Options{Path: p})
if err != nil {
t.Fatal(err)
}
c, err := LoadPCRoomTimePointCatalog(context.Background(), a)
if err != nil {
t.Fatal(err)
}
if c.Seconds != [4]uint32{1800, 3600, 7200, 10800} || c.Counts != [4]uint32{5, 10, 15, 25} {
t.Fatalf("catalog=%+v", c)
}
for i, id := range []int64{490008561, 490008562, 490008564, 490008565} {
if c.Daily[i].ItemID != id || c.Daily[i].DefinitionPath == "" {
t.Fatalf("daily[%d]=%+v", i, c.Daily[i])
}
}
for i, id := range []int64{490008566, 490008567, 490008568, 490008570} {
if c.Period[i].ItemID != id || c.Period[i].DefinitionPath == "" {
t.Fatalf("period[%d]=%+v", i, c.Period[i])
}
}
}
func pcroomTestCatalog() (*PCRoomTimePointCatalog, Definition) {
c := &PCRoomTimePointCatalog{Seconds: [4]uint32{1800, 3600, 7200, 10800}, Counts: [4]uint32{5, 10, 15, 25}}
d := Definition{ID: "2347"}
for i := range c.Daily {
c.Daily[i] = AttendancePVFItemReward{ItemID: int64(100 + i), Count: 1}
c.Period[i] = AttendancePVFItemReward{ItemID: int64(200 + i), Count: 1}
d.Stages = append(d.Stages, Stage{ID: fmt.Sprintf("stage-%d", i+1), RequiredSeconds: uint64(c.Seconds[i]), Items: []ItemReward{{ItemID: int64(300 + i), Count: 1}}})
}
return c, d
}
func TestPCRoomTimePointClockClaimsCrossDayAndRollback(t *testing.T) {
ctx := context.Background()
repos := memory.NewMemoryGroup()
c, d := pcroomTestCatalog()
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: "a"}); err != nil {
t.Fatal(err)
}
for _, id := range []string{"1", "2"} {
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: id, AccountID: "a"}); err != nil {
t.Fatal(err)
}
}
owner, err := NewOwner(repos)
if err != nil {
t.Fatal(err)
}
start := time.Date(2026, 9, 16, 6, 0, 0, 0, ChinaCalendar)
cmd := PCRoomTimePointCommand{AccountID: "a", CharacterID: "1", Now: start, ClockDefinition: d, PeriodClaim: -1}
fail := false
cmd.Deliver = func(m dnfrepo.MailboxRepository, r AttendancePVFItemReward, key string) (string, error) {
id, err := dnfrepo.AppendSystemMail(ctx, m, dnfrepo.SystemMailDelivery{RecipientCharacterID: cmd.CharacterID, Source: "pcroom-test", CompletionKey: key, CreatedAt: cmd.Now, Attachments: []dnfrepo.MailAttachment{{ItemID: r.ItemID, Count: r.Count}}})
if fail {
return "", errors.New("failure after mail write")
}
return id, err
}
observe := func(from, to time.Time) {
t.Helper()
if _, err := owner.Observe(ctx, ObserveCommand{AccountID: "a", CharacterID: "1", Definition: d, IntervalFrom: from, IntervalTo: to}); err != nil {
t.Fatal(err)
}
cmd.Now = to
}
settle := func() PCRoomTimePointSnapshot {
t.Helper()
s, err := SettlePCRoomTimePoint(ctx, repos, c, cmd)
if err != nil {
t.Fatal(err)
}
return s
}
cmd.DailyClaim = 1
observe(start, start.Add(1799*time.Second))
if s := settle(); s.DailyMask != 0 || len(s.MailIDs) != 0 {
t.Fatalf("early claim=%+v", s)
}
observe(start, start.Add(1800*time.Second))
fail = true
if _, err := SettlePCRoomTimePoint(ctx, repos, c, cmd); err == nil {
t.Fatal("mail failure committed")
}
mb, _, _ := repos.Mailbox.Load(ctx, "1")
if len(mb.Mails) != 0 {
t.Fatal("failed mail leaked")
}
fail = false
if s := settle(); s.DailyMask != 1 || len(s.MailIDs) != 1 {
t.Fatalf("claim=%+v", s)
}
cmd.CharacterID = "2"
if s := settle(); s.DailyMask != 1 || len(s.MailIDs) != 0 {
t.Fatalf("cross-character replay=%+v", s)
}
cmd.DailyClaim = 0
observe(start, start.Add(3*time.Hour))
if s := settle(); s.Total != 1 || s.DailyMask != 1 {
t.Fatalf("full day independent of claim=%+v", s)
}
if s := settle(); s.Total != 1 {
t.Fatalf("double count=%+v", s)
}
for day := 1; day < 25; day++ {
at := start.AddDate(0, 0, day)
cmd.Now = at
if s := settle(); s.Seconds != 0 || s.DailyMask != 0 || s.Total != uint32(day) {
t.Fatalf("reset=%+v day=%d", s, day)
}
observe(at, at.Add(3*time.Hour))
s := settle()
if s.Total != uint32(day+1) {
t.Fatalf("count=%+v day=%d", s, day)
}
for i, n := range c.Counts {
cmd.PeriodClaim = i
s = settle()
if s.Total < n && s.PeriodMask&(1<<i) != 0 {
t.Fatalf("locked period=%+v", s)
}
if s.Total == n && len(s.MailIDs) != 1 {
t.Fatalf("threshold %d state=%+v", n, s)
}
if again := settle(); len(again.MailIDs) != 0 {
t.Fatal("period replay mailed")
}
}
cmd.PeriodClaim = -1
}
if s := settle(); s.Total != 25 || s.PeriodMask != 15 {
t.Fatalf("final=%+v", s)
}
cmd.Now = start // stale requests never clear the current ledger.
if _, err := SettlePCRoomTimePoint(ctx, repos, c, cmd); !errors.Is(err, ErrObservationStale) {
t.Fatalf("stale=%v", err)
}
cmd.Now = start.AddDate(0, 0, 24).Add(3 * time.Hour)
cmd.AccountID = "wrong"
if _, err := SettlePCRoomTimePoint(ctx, repos, c, cmd); !errors.Is(err, ErrAccountMismatch) {
t.Fatalf("ownership=%v", err)
}
}
func TestPCRoomTimePointConcurrentClaimAndLegacyCutover(t *testing.T) {
ctx := context.Background()
repos := memory.NewMemoryGroup()
c, d := pcroomTestCatalog()
now := time.Date(2026, 9, 16, 12, 0, 0, 0, ChinaCalendar)
legacy := newState(d.ID)
legacy.resetTo("2026-09-16")
legacy.creditInterval(0, 10800)
legacy.Claims["stage-1"] = claimReceipt{StageID: "stage-1", CharacterID: "1", ClaimedAt: now, Items: []CommittedItem{{SlotKey: "0:9", ItemID: 300, Delta: 1, PostCount: 1}}}
raw, err := encodeState(legacy)
if err != nil {
t.Fatal(err)
}
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: "a", Metadata: map[string]string{metadataKey(d.ID): raw}}); err != nil {
t.Fatal(err)
}
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "1", AccountID: "a"}); err != nil {
t.Fatal(err)
}
cmd := PCRoomTimePointCommand{AccountID: "a", CharacterID: "1", Now: now, ClockDefinition: d, DailyClaim: 1, PeriodClaim: -1}
cmd.Deliver = func(m dnfrepo.MailboxRepository, r AttendancePVFItemReward, key string) (string, error) {
return dnfrepo.AppendSystemMail(ctx, m, dnfrepo.SystemMailDelivery{RecipientCharacterID: "1", Source: "pcroom-test", CompletionKey: key, CreatedAt: now, Attachments: []dnfrepo.MailAttachment{{ItemID: r.ItemID, Count: r.Count}}})
}
s, err := SettlePCRoomTimePoint(ctx, repos, c, cmd)
if err != nil || s.DailyMask != 1 || s.Total != 1 || len(s.MailIDs) != 0 {
t.Fatalf("cutover=%+v %v", s, err)
}
cmd.DailyClaim = 2
var wg sync.WaitGroup
errs := make(chan error, 20)
for range 20 {
wg.Add(1)
go func() { defer wg.Done(); _, err := SettlePCRoomTimePoint(ctx, repos, c, cmd); errs <- err }()
}
wg.Wait()
close(errs)
for err := range errs {
if err != nil {
t.Fatal(err)
}
}
mb, _, err := repos.Mailbox.Load(ctx, "1")
if err != nil || len(mb.Mails) != 1 {
t.Fatalf("concurrent mail count=%d err=%v", len(mb.Mails), err)
}
}