Files
90CNServer/server/internal/services/dnfbridge/avatar_fitting_room_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

176 lines
7.0 KiB
Go

package dnfbridge
import (
"context"
"encoding/binary"
"fmt"
"path/filepath"
"strings"
"testing"
"longheng.io/server/internal/modules/dnf/jobmap"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestCurrentAvatarFittingRoomProtocolRoundTripShape(t *testing.T) {
body := make([]byte, currentAvatarFittingRoomRequestSize)
for index := 0; index < currentAvatarFittingRoomPartCount; index++ {
offset := currentAvatarFittingRoomPrefixSize + index*currentAvatarFittingRoomRecordSize
for byteIndex := 0; byteIndex < 8; byteIndex++ {
body[offset+byteIndex] = 0xff
}
body[offset+8] = byte(index)
}
request, err := parseCurrentAvatarFittingRoomRequest(body)
if err != nil {
t.Fatal(err)
}
changed := [currentAvatarFittingRoomPartCount]bool{true, false, true}
response := buildCurrentAvatarFittingRoomSuccessBody(request, changed)
if len(response) != 1+8*13 || response[0] != 1 || response[1+9] != 1 || response[1+13+9] != 0 || response[1+2*13+9] != 1 {
t.Fatalf("response=%x", response)
}
}
func TestRealPVFAvatarFittingRoomConfigAndTicket(t *testing.T) {
pvfPath := filepath.Clean(filepath.Join("..", "..", "..", "..", "runtime", "data", "dnf", "Script.pvf"))
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Skipf("runtime Script.pvf unavailable: %v", err)
}
config, err := loadCurrentAvatarFittingRoomConfig(archive)
if err != nil {
t.Fatal(err)
}
if config.TicketItemID != 490702492 || config.GaugeThreshold != 20 || config.GaugeRewardID != 490702493 || config.GaugeRewardCount != 1 || config.NormalRate != 90 || config.RareRate != 10 || len(config.Sets) != 10 {
t.Fatalf("config=%+v", config)
}
rare, normal := 0, 0
for _, set := range config.Sets {
if set.Rare {
rare++
} else {
normal++
}
}
if rare != 2 || normal != 8 {
t.Fatalf("sets rare=%d normal=%d", rare, normal)
}
catalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
service := &Service{pvfItemCatalog: catalog, automaticRobotAvatarConfig: &config}
for job := 0; job <= jobmap.MaxID; job++ {
set, setErr := service.automaticRobotAvatarSet(byte(job), uint64(job+1))
if setErr != nil {
t.Fatalf("automatic robot avatar job=%d: %v", job, setErr)
}
supported := false
for _, candidate := range config.Sets {
if _, ok := candidate.Jobs[byte(job)]; ok {
supported = true
break
}
}
if !supported {
for index, itemID := range set {
if itemID != 0 {
t.Fatalf("unsupported automatic robot avatar job=%d part=%d item=%d", job, index, itemID)
}
}
continue
}
for index, itemID := range set {
if itemID == 0 {
t.Fatalf("automatic robot avatar job=%d part=%d is empty", job, index)
}
}
}
definition, err := catalog.ResolveItem(config.TicketItemID)
if err != nil {
t.Fatal(err)
}
isTicket, err := currentPVFAvatarFittingRoomTicket(archive, definition)
if err != nil || !isTicket {
t.Fatalf("ticket definition=%+v isTicket=%t err=%v", definition, isTicket, err)
}
}
func TestCurrentAvatarFittingRoomCommitUsesOneAtomicEightPartTransaction(t *testing.T) {
ctx := context.Background()
source := avatarCompoundPVFTestSource{}
var config strings.Builder
config.WriteString("[ticket index]\n490702492\n[vip gauge]\n20 490702493 1\n[rate]\n100 0\n[fitting room avatar]\n0 `[swordman]`\n")
definitions := make(map[uint32]dungeonDropItemDefinition)
equipmentEntries := make(map[string]dnfrepo.EquipmentEntry)
request := currentAvatarFittingRoomRequest{}
for index, part := range currentAvatarFittingRoomPartNames {
currentID, outputID := uint32(1000+index), uint32(2000+index)
fmt.Fprintf(&config, "`%s` %d\n", part, outputID)
currentPath, outputPath := fmt.Sprintf("equipment/current_%d.equ", index), fmt.Sprintf("equipment/output_%d.equ", index)
equipmentType := fmt.Sprintf("[%s avatar]", part)
if part == "neck" {
equipmentType = "[breast avatar]"
}
if part == "belt" {
equipmentType = "[waist avatar]"
}
source[currentPath] = fmt.Sprintf("[equipment type] `%s`\n", equipmentType)
source[outputPath] = fmt.Sprintf("[equipment type] `%s`\n", equipmentType)
definitions[currentID] = dungeonDropItemDefinition{ItemID: currentID, Kind: dungeonDropItemEquipment, PVFPath: currentPath, EquipmentType: equipmentType}
definitions[outputID] = dungeonDropItemDefinition{ItemID: outputID, Kind: dungeonDropItemEquipment, PVFPath: outputPath, EquipmentType: equipmentType}
raw := make([]byte, 119)
binary.LittleEndian.PutUint32(raw[2:6], currentID)
slot := currentAvatarFittingRoomEquipmentSlots[index]
equipmentEntries[fmt.Sprintf("%d", slot)] = dnfrepo.EquipmentEntry{SlotIndex: slot, ItemID: int64(currentID), RawEntry: raw}
request.Records[index].ObjectIndex = int32(3000 + index)
request.Records[index].CurrentItemID = int32(currentID)
request.Records[index].PartCode = byte(index)
}
config.WriteString("[/fitting room avatar]\n")
source[currentAvatarFittingRoomPVFPath] = config.String()
definitions[490702493] = dungeonDropItemDefinition{ItemID: 490702493, Kind: dungeonDropItemStackable, PVFPath: "stackable/reward.stk", StackableType: "[etc]", StackLimit: 1000, SlotStart: 9, SlotEnd: 64}
catalog := &pvfDungeonDropCatalog{source: source, itemCache: definitions, itemRefs: make(map[uint32]dungeonDropItemReference)}
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "19", AccountID: "dnf:1", Job: "0"}); err != nil {
t.Fatal(err)
}
ticketRaw := make([]byte, 119)
binary.LittleEndian.PutUint32(ticketRaw[6:10], 8)
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "19", Slots: map[string]dnfrepo.ItemStack{
"0:10": {ItemID: 490702492, Count: 8, RawEntry: ticketRaw},
}}); err != nil {
t.Fatal(err)
}
if err := repositories.Equipment.Save(ctx, dnfrepo.EquipmentRecord{CharacterID: "19", Entries: equipmentEntries}); err != nil {
t.Fatal(err)
}
if err := repositories.Settings.Save(ctx, dnfrepo.SettingsRecord{Scope: "character:19:avatar_fitting_room", Values: map[string]string{"used_parts_total": "19"}}); err != nil {
t.Fatal(err)
}
service := &Service{options: options{accountPrefix: defaultAccountPrefix}, repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }}
session := &gameSession{accountID: "dnf:1", selectedCharacterID: 19}
result, err := service.commitCurrentAvatarFittingRoom(ctx, session, catalog, request)
if err != nil {
t.Fatal(err)
}
if result.Domain.TicketConsumed != 8 || result.Domain.RewardGranted != 1 || result.Domain.GaugeAfter != 27 {
t.Fatalf("result=%+v", result)
}
for index, changed := range result.Changed {
if !changed {
t.Fatalf("part=%d not changed", index)
}
}
equipment, _, _ := repositories.Equipment.Load(ctx, "19")
for index, slot := range currentAvatarFittingRoomEquipmentSlots {
entry := equipment.Entries[fmt.Sprintf("%d", slot)]
if entry.ItemID != int64(2000+index) {
t.Fatalf("slot=%d entry=%+v", slot, entry)
}
}
}