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,187 @@
package robot
import (
"context"
"errors"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
type settingsFixture struct {
record dnfrepo.SettingsRecord
has bool
err error
}
func (f *settingsFixture) Check(context.Context) error { return nil }
func (f *settingsFixture) Load(_ context.Context, key string) (dnfrepo.SettingsRecord, bool, error) {
if key != SettingsScope {
return dnfrepo.SettingsRecord{}, false, errors.New("unexpected scope")
}
return dnfrepo.CloneSettings(f.record), f.has, f.err
}
func (f *settingsFixture) Save(_ context.Context, record dnfrepo.SettingsRecord) error {
if f.err != nil {
return f.err
}
f.record, f.has = dnfrepo.CloneSettings(record), true
return nil
}
func TestOwnerRoundTripsNormalizedProfiles(t *testing.T) {
repo := &settingsFixture{}
owner := NewOwner(func() (dnfrepo.Group, bool) {
return dnfrepo.Group{Settings: repo}, true
})
owner.now = func() time.Time { return time.Unix(123, 0) }
want := Config{Profiles: []Profile{
{ID: " beta ", AccountID: " account-b ", CharacterID: 2, ChannelID: 19},
{ID: "alpha", AccountID: "account-a", CharacterID: 1, ChannelID: 6, AutoStart: true, X: 20, Y: 30},
}}
saved, err := owner.Save(context.Background(), want)
if err != nil {
t.Fatalf("Save: %v", err)
}
if len(saved.Profiles) != 2 || saved.Profiles[0].ID != "alpha" || saved.Profiles[1].ID != "beta" {
t.Fatalf("unexpected normalized profiles: %+v", saved.Profiles)
}
loaded, err := owner.Load(context.Background())
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(loaded.Profiles) != 2 || loaded.Profiles[0].AccountID != "account-a" || loaded.Profiles[1].AccountID != "account-b" {
t.Fatalf("unexpected loaded profiles: %+v", loaded.Profiles)
}
saved.Profiles[0].AccountID = "mutated"
if loaded.Profiles[0].AccountID != "account-a" {
t.Fatal("owner result leaked mutable profile storage")
}
}
func TestNormalizeRejectsConflictingCharacterOwnership(t *testing.T) {
_, err := Normalize(Config{Profiles: []Profile{
{ID: "a", AccountID: "one", CharacterID: 7, ChannelID: 19},
{ID: "b", AccountID: "two", CharacterID: 7, ChannelID: 19},
}})
if !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize error = %v, want ErrInvalid", err)
}
}
func TestNormalizeAutomationPolicyAndCloneMessages(t *testing.T) {
config, err := Normalize(Config{
Profiles: []Profile{{ID: "robot", AccountID: "account", CharacterID: 1, ChannelID: 19}},
Automation: AutomationPolicy{
Enabled: true, MoveIntervalMinSeconds: 5, MoveIntervalMaxSeconds: 10,
ChatIntervalMinSeconds: 30, ChatIntervalMaxSeconds: 60,
ChatMessages: []string{" hello ", "world"},
},
})
if err != nil {
t.Fatalf("Normalize: %v", err)
}
if config.Automation.MoveRadius != 80 || config.Automation.ChatMode != 3 || config.Automation.ChatMessages[0] != "hello" {
t.Fatalf("unexpected normalized automation: %+v", config.Automation)
}
clone := CloneConfig(config)
clone.Automation.ChatMessages[0] = "mutated"
if config.Automation.ChatMessages[0] != "hello" {
t.Fatal("CloneConfig leaked automation message storage")
}
}
func TestNormalizePreservesSupportedAutomaticFleetVersion(t *testing.T) {
config, err := Normalize(Config{AutomaticFleetVersion: 7})
if err != nil || config.AutomaticFleetVersion != 7 || CloneConfig(config).AutomaticFleetVersion != 7 {
t.Fatalf("automatic fleet version config=%+v err=%v", config, err)
}
if _, err := Normalize(Config{AutomaticFleetVersion: 8}); err == nil {
t.Fatal("expected unsupported automatic fleet version rejection")
}
}
func TestNormalizeValidatesProfileMovementBounds(t *testing.T) {
profile := Profile{
ID: "bounded", AccountID: "account", CharacterID: 1, ChannelID: 19,
X: 500, Y: 200, MoveBoundsSet: true, MoveMinX: 400, MoveMaxX: 600, MoveMinY: 190, MoveMaxY: 210,
}
if _, err := Normalize(Config{Profiles: []Profile{profile}}); err != nil {
t.Fatalf("Normalize valid bounds: %v", err)
}
profile.X = 700
if _, err := Normalize(Config{Profiles: []Profile{profile}}); !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize outside bounds error = %v, want ErrInvalid", err)
}
profile = Profile{ID: "orphan", AccountID: "account", CharacterID: 2, ChannelID: 19, MoveMaxX: 1}
if _, err := Normalize(Config{Profiles: []Profile{profile}}); !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize orphan bounds error = %v, want ErrInvalid", err)
}
}
func TestNormalizeAutomationMegaphone(t *testing.T) {
config, err := Normalize(Config{Automation: AutomationPolicy{
ChatMessages: []string{"hello"},
MegaphoneIntervalMinSeconds: 900, MegaphoneIntervalMaxSeconds: 1800,
MegaphoneProfileModulo: 50,
}})
if err != nil {
t.Fatalf("Normalize megaphone: %v", err)
}
if config.Automation.MegaphoneProfileModulo != 50 {
t.Fatalf("megaphone policy = %+v", config.Automation)
}
if _, err := Normalize(Config{Automation: AutomationPolicy{
MegaphoneIntervalMinSeconds: 1, MegaphoneIntervalMaxSeconds: 2, MegaphoneProfileModulo: 1,
}}); !errors.Is(err, ErrInvalid) {
t.Fatalf("megaphone without messages error = %v, want ErrInvalid", err)
}
}
func TestNormalizeRejectsPartialAutomationIntervals(t *testing.T) {
_, err := Normalize(Config{Automation: AutomationPolicy{MoveIntervalMinSeconds: 5}})
if !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize error = %v, want ErrInvalid", err)
}
_, err = Normalize(Config{Automation: AutomationPolicy{
ChatIntervalMinSeconds: 5, ChatIntervalMaxSeconds: 5,
}})
if !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize error = %v, want ErrInvalid", err)
}
}
func TestNormalizeAutomationPrivateStore(t *testing.T) {
config, err := Normalize(Config{Automation: AutomationPolicy{
Enabled: true, StoreIntervalMinSeconds: 30, StoreIntervalMaxSeconds: 60,
StoreDurationSeconds: 120, StoreUnitPrice: 10000, StoreTitle: " robot shop ",
}})
if err != nil {
t.Fatalf("Normalize: %v", err)
}
if config.Automation.StoreConcurrent != 1 || config.Automation.StoreMaximumRows != 8 || config.Automation.StoreTitle != "robot shop" {
t.Fatalf("store automation = %+v", config.Automation)
}
_, err = Normalize(Config{Automation: AutomationPolicy{
StoreIntervalMinSeconds: 30, StoreIntervalMaxSeconds: 60,
StoreDurationSeconds: 120, StoreTitle: "missing price",
}})
if !errors.Is(err, ErrInvalid) {
t.Fatalf("Normalize missing price error = %v, want ErrInvalid", err)
}
}
func TestNormalizeAutomationScaling(t *testing.T) {
config, err := Normalize(Config{Automation: AutomationPolicy{Enabled: true, TargetOnline: 12}})
if err != nil {
t.Fatalf("Normalize scaling: %v", err)
}
policy := config.Automation
if policy.ScaleIntervalSeconds != 5 || policy.ScaleUpBatch != 2 || policy.ScaleDownBatch != 2 {
t.Fatalf("scaling defaults = %+v", policy)
}
if _, err := Normalize(Config{Automation: AutomationPolicy{TargetOnline: MaxProfiles + 1}}); err == nil {
t.Fatal("oversized scaling target unexpectedly accepted")
}
}