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

279 lines
9.9 KiB
Go

package companion
import (
"errors"
"reflect"
"strings"
"sync"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/repository"
)
// Synthetic fixtures test policies only, not any real DNF job/skill ABI.
func fixture(t *testing.T) (repository.CompanionSourceState, Selection, Profile) {
t.Helper()
data := repository.CompanionSourceState{
Owner: repository.CharacterRecord{CharacterID: "1", AccountID: "account-test"},
Source: repository.CharacterRecord{CharacterID: "2", AccountID: "account-test", Name: "Fixture", Job: "0", Level: 40, Stats: map[string]int64{"grow_type": 1}},
Equipment: repository.EquipmentRecord{CharacterID: "2", Entries: map[string]repository.EquipmentEntry{
"weapon": {SlotIndex: 12, ItemID: 10000, RawEntry: []byte{1, 2}, Extra: map[string]string{"test": "unchanged"}},
}},
Skill: repository.SkillRecord{CharacterID: "2", Skills: map[int64]repository.SkillState{
10: {Level: 3, Enabled: true}, 20: {Level: 7, Enabled: true}, 30: {Level: 4, Enabled: false},
}, Cooldowns: map[int64]time.Time{10: time.Unix(100, 0)}}, HasEquipment: true, HasSkill: true,
}
s := Selection{AccountID: "account-test", OwnerCharacterID: "1", SourceCharacterID: "2", OnlineStateKnown: true,
ClientSHA256: strings.Repeat("a", 64), PVFSHA256: strings.Repeat("b", 64)}
p := Profile{ID: "synthetic-policy-test-only", Version: 1, EvidenceID: "unit-test-not-native-evidence",
ClientSHA256: s.ClientSHA256, PVFSHA256: s.PVFSHA256, Job: 0, GrowType: 1,
BasicAttackVerified: true, SkillLevelLimits: map[int64]int{10: 5}}
return data, s, p
}
func prepared(t *testing.T) Snapshot {
t.Helper()
data, s, p := fixture(t)
snapshot, err := Prepare(data, s, p, time.Unix(1000, 0))
if err != nil {
t.Fatal(err)
}
return snapshot
}
func runKey() RunKey {
return RunKey{Instance: "test-instance", SessionGeneration: 1, OwnerCharacterID: "1", LifecycleToken: 2, SpawnSequence: 1, Nonce: strings.Repeat("c", 64)}
}
func TestCompanionProductionProfilesFailClosed(t *testing.T) {
if len(VerifiedProfiles()) != 0 {
t.Fatal("unverified native profile advertised")
}
data, s, p := fixture(t)
p.BasicAttackVerified = false
if _, err := Prepare(data, s, p, time.Unix(1000, 0)); !errors.Is(err, ErrProfile) {
t.Fatal(err)
}
}
func TestCompanionConfigRevisionAndDisable(t *testing.T) {
c := DefaultConfig()
if _, err := NextConfig(c, 1, true, "2", "1", nil); !errors.Is(err, ErrRevision) {
t.Fatal(err)
}
if _, err := NextConfig(c, 0, true, "1", "1", nil); !errors.Is(err, ErrSelf) {
t.Fatal(err)
}
if _, err := NextConfig(c, 0, true, "2", "1", ErrProfile); !errors.Is(err, ErrProfile) {
t.Fatal(err)
}
next, err := NextConfig(c, 0, false, "2", "1", ErrProfile)
if err != nil || next.Enabled || next.Revision != 1 || c.Revision != 0 {
t.Fatalf("%+v %v", next, err)
}
c.Revision = ^uint64(0)
if _, err := NextConfig(c, c.Revision, false, "", "1", nil); !errors.Is(err, ErrRevision) {
t.Fatal(err)
}
}
func TestCompanionInvalidCharacterIDs(t *testing.T) {
for _, id := range []string{"0", "01", " 2", "65536", "-1", "2.0", ""} {
t.Run(id, func(t *testing.T) {
if validCharacterID(id) {
t.Fatal("accepted invalid ID")
}
})
}
}
func TestCompanionSelectionRejections(t *testing.T) {
tests := []struct {
name string
edit func(*repository.CompanionSourceState, *Selection, *Profile)
want error
}{
{"self", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.SourceCharacterID = "1" }, ErrSelf},
{"other-account", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.AccountID = "another" }, ErrAccount},
{"deleted", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.Stats["delete_flag"] = 1 }, ErrSource},
{"online", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.SourceOnline = true }, ErrOnline},
{"online-unknown", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { s.OnlineStateKnown = false }, ErrOnlineUnknown},
{"client-version", func(d *repository.CompanionSourceState, s *Selection, p *Profile) {
s.ClientSHA256 = strings.Repeat("d", 64)
}, ErrAssets},
{"job", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Source.Job = "3" }, ErrProfile},
{"missing-grow", func(d *repository.CompanionSourceState, s *Selection, p *Profile) {
delete(d.Source.Stats, "grow_type")
}, ErrProfile},
{"missing-skills", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.HasSkill = false }, ErrSnapshot},
{"cross-equipment", func(d *repository.CompanionSourceState, s *Selection, p *Profile) { d.Equipment.CharacterID = "3" }, ErrSnapshot},
{"duplicate-slot", func(d *repository.CompanionSourceState, s *Selection, p *Profile) {
d.Equipment.Entries["duplicate"] = d.Equipment.Entries["weapon"]
}, ErrSnapshot},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
data, s, p := fixture(t)
tc.edit(&data, &s, &p)
if _, err := Prepare(data, s, p, time.Unix(1000, 0)); !errors.Is(err, tc.want) {
t.Fatalf("got %v want %v", err, tc.want)
}
})
}
}
func TestCompanionSnapshotDetachedAndStable(t *testing.T) {
data, s, p := fixture(t)
a, err := Prepare(data, s, p, time.Unix(1000, 0))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(a.Skills, []SkillSpec{{ID: 10, Level: 3}}) {
t.Fatal(a.Skills)
}
b, err := Prepare(data, s, p, time.Unix(1000, 0))
if err != nil || a.Fingerprint() != b.Fingerprint() {
t.Fatal("unstable fingerprint")
}
a.Equipment[0].ItemID = 123
a.Skills[0].Level = 1
if data.Equipment.Entries["weapon"].ItemID != 10000 || data.Skill.Skills[10].Level != 3 {
t.Fatal("source mutation")
}
data.Source.Name = "changed"
data.Skill.Skills[10] = repository.SkillState{Level: 5, Enabled: true}
if b.Name != "Fixture" || b.Skills[0].Level != 3 {
t.Fatal("snapshot mutation")
}
}
func TestCompanionEmptySnapshotClonePreservesFingerprint(t *testing.T) {
data, s, p := fixture(t)
data.Equipment.Entries = map[string]repository.EquipmentEntry{}
data.Skill.Skills = nil
a, err := Prepare(data, s, p, time.Unix(1000, 0))
if err != nil {
t.Fatal(err)
}
if a.Fingerprint() != a.Clone().Fingerprint() {
t.Fatal("empty-list clone changed content hash")
}
}
func TestCompanionExpiredEquipmentExcluded(t *testing.T) {
data, s, p := fixture(t)
gear := data.Equipment.Entries["weapon"]
gear.ExpireAt = time.Unix(999, 0)
data.Equipment.Entries["weapon"] = gear
a, err := Prepare(data, s, p, time.Unix(1000, 0))
if err != nil || len(a.Equipment) != 0 {
t.Fatalf("%+v %v", a, err)
}
}
func TestCompanionLifecycleFullRun(t *testing.T) {
key := runKey()
source := prepared(t)
i, err := NewInstance(key, source, 3, 1)
if err != nil {
t.Fatal(err)
}
source.Name = "external change"
event := func(seq uint64, kind string, room uint64) Event {
return Event{Run: key, Sequence: seq, Kind: kind, RoomGeneration: room, ObjectKey: 24, ObjectGeneration: 7}
}
apply := func(e Event) {
t.Helper()
if err := i.Apply(e, map[uint16]bool{1: true, 400: true}); err != nil {
t.Fatal(e.Kind, err)
}
}
apply(event(1, "room_ready", 1))
apply(event(2, "native_created", 1))
if i.Owns(key, 24, 7) {
t.Fatal("owner allowed before binding")
}
if err := i.Apply(event(3, "activate", 1), nil); !errors.Is(err, ErrTransition) {
t.Fatal(err)
}
apply(event(3, "owner_bound", 1))
apply(event(4, "activate", 1))
apply(event(4, "activate", 1))
if !i.Owns(key, 24, 7) || i.Owns(key, 24, 8) {
t.Fatal("owner generation validation")
}
apply(event(5, "room_leaving", 1))
apply(event(6, "room_ready", 2))
if i.Status().ObjectKey != 24 || i.Snapshot().Name != "Fixture" || i.Status().ConfigRevision != 3 {
t.Fatal("cross-room instance replaced")
}
apply(event(7, "dead", 2))
if err := i.Apply(event(8, "activate", 2), nil); !errors.Is(err, ErrTransition) {
t.Fatal("dead actor revived", err)
}
if !i.Owns(key, 24, 7) {
t.Fatal("in-flight ownership lost prematurely")
}
apply(event(8, "retire", 2))
apply(event(9, "disposed", 2))
if i.Owns(key, 24, 7) {
t.Fatal("disposed owner retained")
}
if err := i.Apply(event(10, "room_ready", 3), nil); err == nil {
t.Fatal("disposed run resurrected")
}
}
func TestCompanionLifecycleRejectsStaleAndCollision(t *testing.T) {
key := runKey()
i, err := NewInstance(key, prepared(t), 1, 1)
if err != nil {
t.Fatal(err)
}
stale := key
stale.LifecycleToken++
if err := i.Apply(Event{Run: stale, Sequence: 1, Kind: "room_ready", RoomGeneration: 1}, nil); !errors.Is(err, ErrStaleRun) {
t.Fatal(err)
}
if err := i.Apply(Event{Run: key, Sequence: 1, Kind: "room_ready", RoomGeneration: 1}, nil); err != nil {
t.Fatal(err)
}
for _, n := range []uint16{0, 1, 400, 65535} {
e := Event{Run: key, Sequence: 2, Kind: "native_created", RoomGeneration: 1, ObjectKey: n, ObjectGeneration: 1}
if err := i.Apply(e, map[uint16]bool{400: true}); !errors.Is(err, ErrCollision) {
t.Fatalf("key %d: %v", n, err)
}
}
if i.Status().LastSequence != 1 {
t.Fatal("rejected event advanced ledger")
}
if err := i.Apply(Event{Run: key, Sequence: 1, Kind: "skip"}, nil); !errors.Is(err, ErrTransition) {
t.Fatal("sequence reused")
}
e := Event{Run: key, Sequence: 2, Kind: "native_created", RoomGeneration: 1, ObjectKey: 24, ObjectGeneration: 1}
if err := i.Apply(e, nil); !errors.Is(err, ErrCollision) {
t.Fatal("missing collision evidence accepted")
}
}
func TestCompanionLifecycleConcurrentDuplicates(t *testing.T) {
key := runKey()
i, err := NewInstance(key, prepared(t), 1, 1)
if err != nil {
t.Fatal(err)
}
e := Event{Run: key, Sequence: 1, Kind: "room_ready", RoomGeneration: 1}
var wg sync.WaitGroup
for n := 0; n < 32; n++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := i.Apply(e, nil); err != nil {
t.Error(err)
}
_ = i.Snapshot()
_ = i.Status()
}()
}
wg.Wait()
if i.Status().LastSequence != 1 || i.Status().Phase != Ready {
t.Fatal(i.Status())
}
clone := i.Snapshot()
clone.Equipment[0].ItemID = 12
if i.Snapshot().Equipment[0].ItemID == 12 {
t.Fatal("status query mutated run")
}
}