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,140 @@
|
||||
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 = ¤tAvatarFittingRoomConfig{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
|
||||
}
|
||||
Reference in new issue
Block a user