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,278 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user