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

338 lines
11 KiB
Go

package companion
import (
"reflect"
"strings"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/repository"
)
func TestDefaultAIConfigIsProductConfig(t *testing.T) {
got := DefaultAIConfig()
want := AIConfig{BuffPolicy: "leader_first", ComboPreset: "auto", ComboEnabled: true, AOEMinTargets: 3, ProtectLeader: true}
if !reflect.DeepEqual(got, want) {
t.Fatalf("default AI=%+v want=%+v", got, want)
}
}
func TestAIConfigRejectsInvalidProductValues(t *testing.T) {
ai := DefaultAIConfig()
for name, edit := range map[string]func(*AIConfig){
"buff_policy": func(v *AIConfig) { v.BuffPolicy = "unknown" },
"aoe_low": func(v *AIConfig) { v.AOEMinTargets = 1 },
"aoe_high": func(v *AIConfig) { v.AOEMinTargets = 7 },
"preset": func(v *AIConfig) { v.ComboPreset = strings.Repeat("x", 65) },
} {
t.Run(name, func(t *testing.T) {
value := ai
edit(&value)
if err := value.Validate(); err != ErrSelection {
t.Fatalf("validate=%v want=%v", err, ErrSelection)
}
})
}
}
func TestAIConfigSettingsRoundTrip(t *testing.T) {
want := Config{SchemaVersion: 1, Revision: 3, Enabled: true, SourceCharacterID: "8", SourceCharacterIDs: []string{"8"}, AI: DefaultAIConfig(), AIProfiles: map[string]AIProfile{
"8": {Behavior: "aggressive", ClassID: 9, BuffPolicy: "leader_first", ComboPreset: "sword_guard", ComboEnabled: true, AOEMinTargets: 3, ProtectLeader: true},
}}
record, err := AccountConfigSettingsRecord("ai-round-trip", want, time.Date(2026, 10, 3, 5, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
if record.Values["ai_schema_version"] != "2" || record.Values["ai_defaults"] == "" {
t.Fatalf("product AI fields missing: %+v", record.Values)
}
for key := range record.Values {
if strings.HasPrefix(key, "ai_") && key != "ai_schema_version" && key != "ai_defaults" && key != "ai_profiles" {
t.Fatalf("legacy AI field was written: %q", key)
}
}
got, err := ConfigFromAccountSettings(record, "ai-round-trip")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("round trip=%+v want=%+v", got, want)
}
}
func TestAITuningSettingsRoundTrip(t *testing.T) {
tuning := &AITuning{ThinkIntervalMS: 200, FollowDistance: 60, HealLeaderPercent: 70, StuckMS: 900, DetourMS: 1100, GiveUpMS: 4000}
want := Config{
SchemaVersion: 1, Revision: 4, Enabled: true,
SourceCharacterID: "8", SourceCharacterIDs: []string{"8"},
AIProfiles: map[string]AIProfile{"8": {
Behavior: "assist", ClassID: 9, BuffPolicy: AIBuffPolicyLeaderFirst,
ComboPreset: "priest_support", ComboEnabled: false, AOEMinTargets: 3,
ProtectLeader: true, Tuning: tuning,
}},
}
record, err := AccountConfigSettingsRecord("ai-tuning", want, time.Date(2026, 10, 4, 5, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
got, err := ConfigFromAccountSettings(record, "ai-tuning")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("tuning round trip=%+v want=%+v", got, want)
}
}
func TestLegacyAIInternalFieldsAreIgnoredAndCustomMigrates(t *testing.T) {
record := repository.SettingsRecord{
Scope: "account:ai-legacy:companion-v1",
Values: map[string]string{
"schema_version": "1", "revision": "2", "enabled": "true",
"source_character_id": "8", "source_character_ids": "8",
"ai_tempo": "custom", "ai_leader_leash_x": "999", "ai_give_up_ms": "20000",
"ai_profiles": `{"8":{"behavior":"custom","class_id":9,"combo_preset":"auto","combo_enabled":true}}`,
},
}
cfg, err := ConfigFromAccountSettings(record, "ai-legacy")
if err != nil {
t.Fatal(err)
}
profile := cfg.AIProfiles["8"]
if profile.Behavior != "assist" || profile.BuffPolicy != AIBuffPolicyLeaderFirst || profile.AOEMinTargets != 3 || !profile.ProtectLeader {
t.Fatalf("legacy profile was not normalized: %+v", profile)
}
if !cfg.AI.IsZero() {
t.Fatalf("legacy internal scalar fields were retained: %+v", cfg.AI)
}
}
func TestAIProfileRejectsInvalidModeOrPreset(t *testing.T) {
for name, profile := range map[string]AIProfile{
"mode": {Behavior: "wander", ComboPreset: "auto"},
"preset": {Behavior: "assist", ComboPreset: strings.Repeat("x", 65)},
} {
t.Run(name, func(t *testing.T) {
if err := profile.Validate(); err != ErrSelection {
t.Fatalf("validate=%v want=%v", err, ErrSelection)
}
})
}
}
func TestConfigFromSettingsLegacySourceRow(t *testing.T) {
record := repository.SettingsRecord{
Scope: "character:3:companion-v1",
Values: map[string]string{"source_character_id": "2"},
}
cfg, err := ConfigFromSettings(record, "3")
if err != nil {
t.Fatalf("ConfigFromSettings legacy: %v", err)
}
if !cfg.Enabled || cfg.Revision != 0 || cfg.SourceCharacterID != "2" {
t.Fatalf("legacy cfg = %+v", cfg)
}
}
func TestConfigSettingsRecordRoundTrip(t *testing.T) {
now := time.Date(2026, 9, 25, 5, 0, 0, 0, time.UTC)
want := Config{SchemaVersion: 1, Revision: 7, Enabled: true, BalanceEnabled: true, RandomEquipmentEnabled: true, SourceCharacterID: "8", SourceCharacterIDs: []string{"8"}}
record, err := ConfigSettingsRecord("3", want, now)
if err != nil {
t.Fatalf("ConfigSettingsRecord: %v", err)
}
if !record.UpdatedAt.Equal(now) {
t.Fatalf("updated_at = %v want %v", record.UpdatedAt, now)
}
got, err := ConfigFromSettings(record, "3")
if err != nil {
t.Fatalf("ConfigFromSettings: %v", err)
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("round trip = %+v want %+v", got, want)
}
}
func TestConfigFromSettingsRejectsNonCanonicalRevision(t *testing.T) {
record := repository.SettingsRecord{
Scope: "character:3:companion-v1",
Values: map[string]string{
"schema_version": "1",
"revision": "007",
"enabled": "true",
"source_character_id": "2",
},
}
if _, err := ConfigFromSettings(record, "3"); err != ErrSelection {
t.Fatalf("err = %v want %v", err, ErrSelection)
}
}
func TestSourceCharacterIDFromSettingsDisabled(t *testing.T) {
record := repository.SettingsRecord{
Scope: "character:3:companion-v1",
Values: map[string]string{
"schema_version": "1",
"revision": "2",
"enabled": "false",
"source_character_id": "8",
},
}
source, selected, err := SourceCharacterIDFromSettings(record, "3")
if err != nil {
t.Fatalf("SourceCharacterIDFromSettings: %v", err)
}
if selected || source != "" {
t.Fatalf("disabled source = %q selected=%v", source, selected)
}
}
func TestNextConfigPersistsBalanceSwitch(t *testing.T) {
current := Config{
SchemaVersion: 1,
Revision: 4,
Enabled: true,
BalanceEnabled: false,
SourceCharacterID: "2",
}
next, err := NextConfig(current, 4, true, "2", "3", nil, true)
if err != nil {
t.Fatal(err)
}
if next.Revision != 5 || !next.Enabled || !next.BalanceEnabled ||
next.SourceCharacterID != "2" {
t.Fatalf("next=%+v", next)
}
original, err := NextConfig(next, 5, true, "2", "3", nil, false)
if err != nil {
t.Fatal(err)
}
if original.BalanceEnabled {
t.Fatalf("balance switch did not turn off: %+v", original)
}
}
func TestNextConfigPersistsProgressionSwitchDefaultOff(t *testing.T) {
current := Config{
SchemaVersion: 1, Revision: 4, Enabled: true,
SourceCharacterID: "2", SourceCharacterIDs: []string{"2"},
}
next, err := NextConfigMany(current, 4, true, []string{"2"}, "3", false, true)
if err != nil {
t.Fatal(err)
}
if !next.ProgressionEnabled || next.BalanceEnabled {
t.Fatalf("next=%+v", next)
}
record, err := AccountConfigSettingsRecord("account-progression", next, time.Date(2026, 9, 25, 5, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
if record.Values[configProgressionKey] != "true" {
t.Fatalf("stored progression switch=%q", record.Values[configProgressionKey])
}
legacy := repository.SettingsRecord{
Scope: "account:account-progression:companion-v1",
Values: map[string]string{
"schema_version": "1", "revision": "1", "enabled": "true",
"source_character_id": "2",
},
}
legacyCfg, err := ConfigFromAccountSettings(legacy, "account-progression")
if err != nil {
t.Fatal(err)
}
if legacyCfg.ProgressionEnabled {
t.Fatalf("legacy config unexpectedly enabled progression: %+v", legacyCfg)
}
}
func TestNextConfigPersistsRandomEquipmentSwitchDefaultOff(t *testing.T) {
current := Config{
SchemaVersion: 1, Revision: 4, Enabled: true,
SourceCharacterID: "2", SourceCharacterIDs: []string{"2"},
}
next, err := NextConfigMany(current, 4, true, []string{"2"}, "3", false, false, true)
if err != nil {
t.Fatal(err)
}
if !next.RandomEquipmentEnabled || next.BalanceEnabled || next.ProgressionEnabled {
t.Fatalf("next=%+v", next)
}
record, err := AccountConfigSettingsRecord("account-random-equipment", next, time.Date(2026, 9, 25, 5, 0, 0, 0, time.UTC))
if err != nil {
t.Fatal(err)
}
if record.Values[configRandomEquipmentKey] != "true" {
t.Fatalf("stored random equipment switch=%q", record.Values[configRandomEquipmentKey])
}
legacy := repository.SettingsRecord{
Scope: "account:account-random-equipment:companion-v1",
Values: map[string]string{
"schema_version": "1", "revision": "1", "enabled": "true",
"source_character_id": "2",
},
}
legacyCfg, err := ConfigFromAccountSettings(legacy, "account-random-equipment")
if err != nil {
t.Fatal(err)
}
if legacyCfg.RandomEquipmentEnabled {
t.Fatalf("legacy config unexpectedly enabled random equipment: %+v", legacyCfg)
}
}
func TestAccountConfigSettingsRoundTrip(t *testing.T) {
now := time.Date(2026, 9, 25, 15, 0, 0, 0, time.FixedZone("CST", 8*60*60))
cfg := Config{
SchemaVersion: 1,
Revision: 7,
Enabled: true,
BalanceEnabled: true,
RandomEquipmentEnabled: true,
SourceCharacterID: "11",
SourceCharacterIDs: []string{"11"},
}
record, err := AccountConfigSettingsRecord("account-projection", cfg, now)
if err != nil {
t.Fatal(err)
}
if record.Scope != "account:account-projection:companion-v1" {
t.Fatalf("scope=%q", record.Scope)
}
got, err := ConfigFromAccountSettings(record, "account-projection")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, cfg) {
t.Fatalf("got=%+v want=%+v", got, cfg)
}
if _, err := ConfigFromAccountSettings(record, "other-account"); err != ErrSelection {
t.Fatalf("wrong-account scope err=%v", err)
}
}
func TestAccountConfigSettingsMultiSourceRoundTrip(t *testing.T) {
now := time.Date(2026, 9, 27, 2, 0, 0, 0, time.FixedZone("CST", 8*60*60))
cfg := Config{
SchemaVersion: 1, Revision: 9, Enabled: true,
SourceCharacterID: "2",
SourceCharacterIDs: []string{"2", "8", "11"},
}
record, err := AccountConfigSettingsRecord("account-multi", cfg, now)
if err != nil {
t.Fatal(err)
}
if got := record.Values[sourceCharactersSettingKey]; got != "2,8,11" {
t.Fatalf("stored sources=%q", got)
}
got, err := ConfigFromAccountSettings(record, "account-multi")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, cfg) {
t.Fatalf("got=%+v want=%+v", got, cfg)
}
}