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