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

141 lines
6.5 KiB
Go

package dnfbridge
import (
"context"
"strconv"
"strings"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/channelcatalog"
"longheng.io/server/internal/modules/dnf/channelinfo"
"longheng.io/server/internal/modules/dnf/jobmap"
"longheng.io/server/internal/modules/dnf/premium"
dnfprogression "longheng.io/server/internal/modules/dnf/progression"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
)
func TestEnsureAutomaticRobotProfileCreatesThenReusesNativeCharacter(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
experience, err := dnfprogression.LoadExperienceTable(context.Background(), dungeonDropCatalogTestSource{
dnfprogression.ExperienceTablePath: strings.Repeat("100 ", 100),
})
if err != nil {
t.Fatal(err)
}
index, err := channelinfo.LoadFile("testdata/channel_info.etc")
if err != nil {
t.Fatal(err)
}
channels, err := channelcatalog.New(index, channelcatalog.Options{ServerID: 1})
if err != nil {
t.Fatal(err)
}
service := &Service{
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
experienceTable: experience,
catalog: channels,
questCatalog: automaticRobotTestQuestCatalog(t),
}
job := int((automaticRobotSeed(1) >> 8) % uint64(jobmap.MaxID))
level := currentAdventureCharacterLevelCap
prepareTestCharacterInitialization(service, byte(job))
prepareAutomaticRobotAvatarCatalog(service, byte(job))
service.initialSPTable[level] = 100
first, err := service.EnsureAutomaticRobotProfile(context.Background(), 1)
if err != nil {
t.Fatal(err)
}
legacy, found, err := repositories.Character.Load(context.Background(), strconv.Itoa(int(first.CharacterID)))
if err != nil || !found {
t.Fatalf("load automatic character: found=%v err=%v", found, err)
}
legacy = dnfrepo.CloneCharacter(legacy)
legacy.Level = 55
legacy.Stats[currentDungeonTutorialCompletedKey] = 0
legacy.Stats["tutorial_reward_progress_38"] = 0
legacy.Stats[dnfrepo.CharacterStoryDigestLastLevelStatKey] = 0
legacy.Stats[dnfrepo.CharacterStoryDigestMigrationVersionStatKey] = 0
legacy.Stats["ex_equip_slot_stat"] = 0
if err := repositories.Character.Save(context.Background(), legacy); err != nil {
t.Fatal(err)
}
second, err := service.EnsureAutomaticRobotProfile(context.Background(), 1)
if err != nil {
t.Fatal(err)
}
if first != second || first.CharacterID == 0 || first.AutoStart || first.ChannelID != automaticRobotPreferredChannelID || first.ID != "auto-generated-001" ||
first.Village != automaticRobotTownID || first.Area != automaticRobotTownAreaID {
t.Fatalf("automatic profiles differ: first=%+v second=%+v", first, second)
}
characters, err := repositories.Character.ListByAccount(context.Background(), first.AccountID, dnfrepo.MaxCharacterSlotLimit)
if err != nil || len(characters) != 1 || characters[0].Level != level || characters[0].Job != strconv.Itoa(job) ||
characters[0].Stats[currentDungeonTutorialCompletedKey] != currentDungeonTutorialCompleteFlag ||
characters[0].Stats["tutorial_reward_progress_38"] != 1 ||
characters[0].Stats[dnfrepo.CharacterStoryDigestLastLevelStatKey] != int64(currentAdventureCharacterLevelCap) ||
characters[0].Stats[dnfrepo.CharacterStoryDigestMigrationVersionStatKey] != int64(dnfrepo.CurrentCharacterStoryDigestMigrationVersion) ||
characters[0].Stats["ex_equip_slot_stat"] != gmAllEquipSlotsMask ||
characters[0].Stats["town_id"] != automaticRobotTownID || characters[0].Stats["area_id"] != automaticRobotTownAreaID {
t.Fatalf("automatic native characters = %+v err=%v", characters, err)
}
quests, found, err := repositories.Quest.Load(context.Background(), characters[0].CharacterID)
if err != nil || !found || quests.States[900001].Status != "completed" || quests.States[900002].Status != "completed" {
t.Fatalf("automatic main story = %+v found=%t err=%v", quests, found, err)
}
equipment, found, err := repositories.Equipment.Load(context.Background(), characters[0].CharacterID)
if err != nil || !found {
t.Fatalf("automatic avatar equipment: found=%t err=%v", found, err)
}
for index, slot := range currentAvatarFittingRoomEquipmentSlots {
entry := equipment.Entries[strconv.Itoa(int(slot))]
if entry.ItemID != int64(810000+index) || len(entry.RawEntry) == 0 {
t.Fatalf("automatic avatar part=%d slot=%d entry=%+v", index, slot, entry)
}
}
account, found, err := repositories.Account.Load(context.Background(), first.AccountID)
if err != nil || !found || !premium.Active(account, premium.TypeAuction, time.Now().UTC()) ||
!premium.Active(account, premium.TypePrivateStore, time.Now().UTC()) {
t.Fatalf("automatic store contract account=%+v found=%t err=%v", account, found, err)
}
}
func prepareAutomaticRobotAvatarCatalog(service *Service, job byte) {
items := [currentAvatarFittingRoomPartCount]uint32{}
definitions := make(map[uint32]dungeonDropItemDefinition, len(items))
for index := range items {
itemID := uint32(810000 + index)
items[index] = itemID
equipmentType := "[" + currentAvatarFittingRoomPartNames[index] + " avatar]"
if index == 3 {
equipmentType = "[breast avatar]"
} else if index == 5 {
equipmentType = "[waist avatar]"
}
definitions[itemID] = dungeonDropItemDefinition{ItemID: itemID, Kind: dungeonDropItemEquipment, EquipmentType: equipmentType}
}
service.automaticRobotAvatarConfig = &currentAvatarFittingRoomConfig{Sets: []currentAvatarFittingRoomSet{{Jobs: map[byte][currentAvatarFittingRoomPartCount]uint32{job: items}}}}
service.pvfItemCatalog = &pvfDungeonDropCatalog{source: avatarCompoundPVFTestSource{}, itemCache: definitions}
}
func automaticRobotTestQuestCatalog(t *testing.T) *dnfquest.Catalog {
t.Helper()
source := questListTestSource{
dnfquest.DefaultList: "900001 `automatic_robot_main.qst`\n900002 `automatic_robot_cap_main.qst`\n",
"n_quest/automatic_robot_main.qst": "[grade]\n`[epic]`\n[level]\n1 99\n[job]\n`[all]`\n[exposed by npc]\n1\n[type]\n`[meet npc]`\n",
"n_quest/automatic_robot_cap_main.qst": "[grade]\n`[epic]`\n[level]\n90 99\n[job]\n`[all]`\n[exposed by npc]\n1\n[type]\n`[meet npc]`\n[pre required quest]\n900001\n",
}
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
if err != nil {
t.Fatal(err)
}
catalog, err := dnfquest.Load(context.Background(), index)
if err != nil {
t.Fatal(err)
}
return catalog
}