Files
90CNServer/server/internal/modules/dnf/companion/projection_test.go
T
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

265 lines
8.5 KiB
Go

package companion
import (
"reflect"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/equip"
"longheng.io/server/internal/modules/dnf/repository"
)
func visualFixture(
sourceID, name, job string,
grow int64,
weapon int64,
avatars map[string]repository.EquipmentEntry,
) (repository.CompanionSourceState, Selection) {
entries := map[string]repository.EquipmentEntry{
"weapon": {
SlotIndex: 12,
ItemID: weapon,
RawEntry: []byte{1, 2, 3},
Extra: map[string]string{
"item_kind": "equipment",
"equipment_type": "17",
"current_exe_equipment_type": "12",
"pvf_path": "equipment/character/test/weapon/test.equ",
},
},
}
for key, value := range avatars {
entries[key] = value
}
state := repository.CompanionSourceState{
Owner: repository.CharacterRecord{
CharacterID: "1",
AccountID: "account-test",
Stats: map[string]int64{"delete_flag": 0},
},
Source: repository.CharacterRecord{
CharacterID: sourceID,
AccountID: "account-test",
Name: name,
Job: job,
Level: 86,
Stats: map[string]int64{"delete_flag": 0, "grow_type": grow},
},
Equipment: repository.EquipmentRecord{
CharacterID: sourceID,
Entries: entries,
},
HasEquipment: true,
}
selection := Selection{
AccountID: "account-test",
OwnerCharacterID: "1",
SourceCharacterID: sourceID,
OnlineStateKnown: true,
}
return state, selection
}
func avatarFixture(slot int16, item int64, equipmentType string) repository.EquipmentEntry {
return repository.EquipmentEntry{
SlotIndex: slot,
ItemID: item,
RawEntry: []byte{4, 5, 6},
Extra: map[string]string{
"item_kind": "avatar",
"equipment_type": equipmentType,
"pvf_path": "equipment/character/test/avatar/test.equ",
},
}
}
func TestVisualProjectionSwitchesABAndDoesNotMutateSource(t *testing.T) {
aState, aSelection := visualFixture(
"2", "DETSCS", "0", 35, 101010912,
map[string]repository.EquipmentEntry{
"hat": avatarFixture(0, 401550018, "[hat avatar]"),
"hair": avatarFixture(1, 101560869, "[hair avatar]"),
},
)
bState, bSelection := visualFixture(
"8", "DETSDT", "13", 3, 114000248,
map[string]repository.EquipmentEntry{
"weapon-avatar": avatarFixture(10, 414600011, "[weapon avatar]"),
},
)
aBefore := repository.CompanionSourceState{
Owner: repository.CloneCharacter(aState.Owner),
Source: repository.CloneCharacter(aState.Source),
Equipment: repository.CloneEquipment(aState.Equipment),
HasEquipment: aState.HasEquipment,
}
bBefore := repository.CompanionSourceState{
Owner: repository.CloneCharacter(bState.Owner),
Source: repository.CloneCharacter(bState.Source),
Equipment: repository.CloneEquipment(bState.Equipment),
HasEquipment: bState.HasEquipment,
}
now := time.Unix(1_800_000_000, 0)
a, err := BuildVisualProjection(aState, aSelection, now)
if err != nil {
t.Fatal(err)
}
b, err := BuildVisualProjection(bState, bSelection, now)
if err != nil {
t.Fatal(err)
}
if a.Name != "DETSCS" || b.Name != "DETSDT" ||
a.Job != 0 || b.Job != 13 ||
a.GrowType != 35 || b.GrowType != 3 {
t.Fatalf("identity projection A=%+v B=%+v", a, b)
}
if a.Weapon == nil || b.Weapon == nil ||
a.Weapon.ItemID != 101010912 ||
b.Weapon.ItemID != 114000248 {
t.Fatalf("weapon projection A=%+v B=%+v", a.Weapon, b.Weapon)
}
if len(a.Avatars) != 2 || len(b.Avatars) != 1 ||
a.Avatars[0].DisplayItemID != 401550018 ||
b.Avatars[0].DisplayItemID != 414600011 {
t.Fatalf("avatar projection A=%+v B=%+v", a.Avatars, b.Avatars)
}
if a.Fingerprint() == b.Fingerprint() {
t.Fatal("different source characters produced same visual fingerprint")
}
if !reflect.DeepEqual(aState.Owner, aBefore.Owner) ||
!reflect.DeepEqual(aState.Source, aBefore.Source) ||
!reflect.DeepEqual(aState.Equipment, aBefore.Equipment) ||
!reflect.DeepEqual(bState.Owner, bBefore.Owner) ||
!reflect.DeepEqual(bState.Source, bBefore.Source) ||
!reflect.DeepEqual(bState.Equipment, bBefore.Equipment) {
t.Fatal("BuildVisualProjection mutated source records")
}
a.Weapon.ItemID = 1
a.Avatars[0].ItemID = 2
if aState.Equipment.Entries["weapon"].ItemID != 101010912 ||
aState.Equipment.Entries["hat"].ItemID != 401550018 {
t.Fatal("returned projection aliases source equipment")
}
}
func TestVisualProjectionAcceptsFreshCharacterWeaponWithoutDerivedKind(t *testing.T) {
state, selection := visualFixture(
"2", "DETXX", "11", 0, 101010912, nil,
)
weapon := state.Equipment.Entries["weapon"]
delete(weapon.Extra, "item_kind")
delete(weapon.Extra, "current_exe_equipment_type")
weapon.Extra["pvf_path"] = "equipment/character/swordman/weapon/katana/101010912.equ"
weapon.Extra["equipment_type"] = "17"
state.Equipment.Entries["weapon"] = weapon
got, err := BuildVisualProjection(
state, selection, time.Unix(1_800_000_000, 0),
)
if err != nil {
t.Fatal(err)
}
if got.Weapon == nil || got.Weapon.ItemID != 101010912 {
t.Fatalf("fresh-character weapon projection=%+v", got.Weapon)
}
}
func TestVisualProjectionUsesCloneAvatarDisplayID(t *testing.T) {
clone := avatarFixture(3, 499999999, "[coat avatar]")
clone.Extra[equip.CloneAvatarDisplayItemIDExtraKey] = "412500009"
state, selection := visualFixture(
"8", "DETSDT", "13", 3, 114000248,
map[string]repository.EquipmentEntry{"coat": clone},
)
got, err := BuildVisualProjection(
state, selection, time.Unix(1_800_000_000, 0),
)
if err != nil {
t.Fatal(err)
}
if len(got.Avatars) != 1 ||
got.Avatars[0].ItemID != 499999999 ||
got.Avatars[0].DisplayItemID != 412500009 {
t.Fatalf("clone avatar projection=%+v", got.Avatars)
}
}
func TestVisualProjectionAcceptsPrimaryAndSupportWeapon(t *testing.T) {
state, selection := visualFixture("13", "Vagabond", "11", 4, 28246, nil)
secondary := state.Equipment.Entries["weapon"]
secondary.SlotIndex, secondary.ItemID = 24, 28239
state.Equipment.Entries["support"] = secondary
before := repository.CloneEquipment(state.Equipment)
got, err := BuildVisualProjection(state, selection, time.Unix(1_800_000_000, 0))
if err != nil || got.Weapon == nil || got.Weapon.Slot != 12 || got.Weapon.ItemID != 28246 {
t.Fatalf("valid dual weapons rejected or primary changed: %+v err=%v", got, err)
}
if !reflect.DeepEqual(before, state.Equipment) {
t.Fatal("display projection changed source equipment")
}
delete(state.Equipment.Entries, "weapon")
got, err = BuildVisualProjection(state, selection, time.Unix(1_800_000_000, 0))
if err != nil || got.Weapon != nil {
t.Fatalf("support weapon substituted for missing primary: %+v err=%v", got, err)
}
}
func TestVisualProjectionRejectsDuplicateWeapons(t *testing.T) {
state, selection := visualFixture(
"2", "DETSCS", "0", 35, 101010912, nil,
)
state.Equipment.Entries["weapon2"] = repository.EquipmentEntry{
SlotIndex: 20,
ItemID: 101010913,
Extra: map[string]string{
"item_kind": "equipment",
"pvf_path": "equipment/character/test/weapon/other.equ",
},
}
if _, err := BuildVisualProjection(
state, selection, time.Unix(1_800_000_000, 0),
); err != ErrSnapshot {
t.Fatalf("duplicate weapon err=%v want=%v", err, ErrSnapshot)
}
}
func TestProjectionSettingsSelectExactDifferentCharacter(t *testing.T) {
scope, err := SettingsScope("3")
if err != nil || scope != "character:3:companion-v1" {
t.Fatalf("scope=%q err=%v", scope, err)
}
record := repository.SettingsRecord{
Scope: scope,
Values: map[string]string{
"source_character_id": "2",
},
}
source, found, err := SourceCharacterIDFromSettings(record, "3")
if err != nil || !found || source != "2" {
t.Fatalf("source=%q found=%t err=%v", source, found, err)
}
record.Values["source_character_id"] = "8"
source, found, err = SourceCharacterIDFromSettings(record, "3")
if err != nil || !found || source != "8" {
t.Fatalf("source=%q found=%t err=%v", source, found, err)
}
for _, tc := range []struct {
name string
record repository.SettingsRecord
owner string
want error
}{
{"self", repository.SettingsRecord{Scope: scope, Values: map[string]string{"source_character_id": "3"}}, "3", ErrSelf},
{"leading zero", repository.SettingsRecord{Scope: scope, Values: map[string]string{"source_character_id": "02"}}, "3", ErrSelection},
{"wrong scope", repository.SettingsRecord{Scope: "character:4:companion-v1", Values: map[string]string{"source_character_id": "2"}}, "3", ErrSelection},
} {
if _, _, got := SourceCharacterIDFromSettings(tc.record, tc.owner); got != tc.want {
t.Fatalf("%s err=%v want=%v", tc.name, got, tc.want)
}
}
}