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