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,330 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
)
|
||||
|
||||
func TestGameplayModuleRegistryRejectsDuplicateOwnership(t *testing.T) {
|
||||
handler := func(*Service, *gameSession, gameplayRequest) error { return nil }
|
||||
normalizer := func(body []byte) []byte { return body }
|
||||
tests := []struct {
|
||||
name string
|
||||
modules []gameplayModuleDefinition
|
||||
}{
|
||||
{
|
||||
name: "module name",
|
||||
modules: []gameplayModuleDefinition{
|
||||
{Name: "same"},
|
||||
{Name: "same"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy opcode",
|
||||
modules: []gameplayModuleDefinition{
|
||||
{Name: "one", LegacyHandlers: map[uint16]gameplayHandler{21: handler}},
|
||||
{Name: "two", LegacyHandlers: map[uint16]gameplayHandler{21: handler}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "upper opcode",
|
||||
modules: []gameplayModuleDefinition{
|
||||
{Name: "one", UpperHandlers: map[uint16]gameplayHandler{21: handler}},
|
||||
{Name: "two", UpperHandlers: map[uint16]gameplayHandler{21: handler}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "legacy normalizer",
|
||||
modules: []gameplayModuleDefinition{
|
||||
{Name: "one", LegacyNormalizers: map[uint16]gameplayLegacyNormalizer{21: normalizer}},
|
||||
{Name: "two", LegacyNormalizers: map[uint16]gameplayLegacyNormalizer{21: normalizer}},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, err := newGameplayModuleRegistry(test.modules...); !errors.Is(err, errGameplayModuleDuplicate) {
|
||||
t.Fatalf("error=%v want=%v", err, errGameplayModuleDuplicate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentGameplayModulesHaveIndependentRegisteredOwners(t *testing.T) {
|
||||
wantNames := []string{
|
||||
"npc-shop",
|
||||
"private-store",
|
||||
"auction",
|
||||
"cera-shop",
|
||||
"character-rename",
|
||||
"cera-package",
|
||||
"premium-service",
|
||||
"game-option",
|
||||
"guild",
|
||||
"guild-compound",
|
||||
"wedding",
|
||||
"inventory",
|
||||
"skill",
|
||||
"skill-switch-inventory",
|
||||
"equipment-disjoint",
|
||||
"avatar-compound",
|
||||
"avatar-fitting-room",
|
||||
"avatar-option-change",
|
||||
"avatar-disjoint",
|
||||
"emblem-compound",
|
||||
"expert-job",
|
||||
"booster-item",
|
||||
"vending-machine",
|
||||
"equipment-rental",
|
||||
"epic-production",
|
||||
"epic-book",
|
||||
"epic-wish",
|
||||
"guardian-gem",
|
||||
"equipment-socket",
|
||||
"equipment-emblem-control",
|
||||
"avatar-socket",
|
||||
"avatar-emblem",
|
||||
"item-grade-adjust",
|
||||
"cargo-transport",
|
||||
"pet-seal",
|
||||
"custom-ability",
|
||||
"equipment-effect",
|
||||
"aura-skin-slot",
|
||||
"quest",
|
||||
"dungeon-navigation",
|
||||
"dungeon-altar",
|
||||
"dungeon-combat-drop",
|
||||
"dungeon-death-revive",
|
||||
"dungeon-settlement-card",
|
||||
"dungeon-tournament-reward",
|
||||
"dungeon-tutorial-story",
|
||||
"death-tower",
|
||||
"town",
|
||||
"adventure-info",
|
||||
"title-book",
|
||||
"clone-title",
|
||||
"achievement",
|
||||
"emotion",
|
||||
"collectbox",
|
||||
"attendance-marble",
|
||||
"carry-gold",
|
||||
"growup-change",
|
||||
}
|
||||
if !reflect.DeepEqual(currentGameplayModules.names, wantNames) {
|
||||
t.Fatalf("module names=%v want=%v", currentGameplayModules.names, wantNames)
|
||||
}
|
||||
ownedRoutes := make(map[string]int, len(wantNames))
|
||||
for _, route := range currentGameplayModules.legacyHandlers {
|
||||
ownedRoutes[route.module]++
|
||||
}
|
||||
for _, route := range currentGameplayModules.upperHandlers {
|
||||
ownedRoutes[route.module]++
|
||||
}
|
||||
for _, route := range currentGameplayModules.legacyNormalizers {
|
||||
ownedRoutes[route.module]++
|
||||
}
|
||||
for _, module := range wantNames {
|
||||
if ownedRoutes[module] == 0 {
|
||||
t.Errorf("gameplay module %q owns no handler or normalizer route", module)
|
||||
}
|
||||
}
|
||||
|
||||
legacyOwners := map[uint16]string{
|
||||
uint16(dnfenum.CmdPacketBuyItem): "npc-shop",
|
||||
uint16(dnfenum.CmdPacketShopPurchaseCount): "npc-shop",
|
||||
uint16(dnfenum.CmdPacketBuyCerashopItem): "cera-shop",
|
||||
uint16(dnfenum.CmdPacketQueryCharacInfo): "cera-shop",
|
||||
uint16(dnfenum.CmdPacketOpenCerapackage): "cera-package",
|
||||
uint16(dnfenum.CmdPacketPremiumService): "premium-service",
|
||||
uint16(dnfenum.CmdPacketSaveGameOption1): "game-option",
|
||||
uint16(dnfenum.CmdPacketSaveGameOption2): "game-option",
|
||||
uint16(dnfenum.CmdPacketSaveGameOptionQuickchatting): "game-option",
|
||||
uint16(dnfenum.CmdPacketCompoundFlag): "guild-compound",
|
||||
uint16(dnfenum.CmdPacketCompoundGem): "guild-compound",
|
||||
uint16(dnfenum.CmdPacketSearchGuildList): "guild",
|
||||
uint16(dnfenum.CmdPacketRequestAccountGuildList): "guild",
|
||||
uint16(dnfenum.CmdPacketJoinGuildInfo): "guild",
|
||||
uint16(dnfenum.CmdPacketDisjointItem): "equipment-disjoint",
|
||||
uint16(dnfenum.CmdPacketCompoundAvatar): "avatar-compound",
|
||||
uint16(dnfenum.CmdPacketAvatarFittingRoomChange): "avatar-fitting-room",
|
||||
uint16(dnfenum.CmdPacketAvatarOptionChange): "avatar-option-change",
|
||||
uint16(dnfenum.CmdPacketDisjointAvatar): "avatar-disjoint",
|
||||
uint16(dnfenum.CmdPacketCompoundEmblem): "emblem-compound",
|
||||
uint16(dnfenum.CmdPacketUseBoosterItem): "booster-item",
|
||||
uint16(dnfenum.CmdPacketUseLotteryItem): "booster-item",
|
||||
uint16(dnfenum.CmdPacketOverflowInfo): "booster-item",
|
||||
uint16(dnfenum.CmdPacketUseVendingMachine): "vending-machine",
|
||||
uint16(dnfenum.CmdPacketRentEquipmentItem): "equipment-rental",
|
||||
uint16(dnfenum.CmdPacketEpicProductionStartFinish): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionChangeItem): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionProcess): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionMaterialCompound): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionAbilityOption): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicBookMakeItem): "epic-book",
|
||||
uint16(dnfenum.CmdPacketUpdateWishItem): "epic-wish",
|
||||
uint16(dnfenum.CmdPacketUseGem): "guardian-gem",
|
||||
uint16(currentEquipmentSocketOpenOpcode): "equipment-socket",
|
||||
uint16(currentNoBody796Opcode): "equipment-emblem-control",
|
||||
uint16(currentEquipmentEmblemAttachOpcode): "equipment-emblem-control",
|
||||
uint16(currentAvatarSocketOpenOpcode): "avatar-socket",
|
||||
uint16(currentAvatarEmblemAttachOpcode): "avatar-emblem",
|
||||
uint16(dnfenum.CmdPacketResetItemAttr): "item-grade-adjust",
|
||||
uint16(dnfenum.CmdPacketCustomAbilityEquipOption): "custom-ability",
|
||||
uint16(dnfenum.CmdPacketCustomAbilitySetEquipOption): "custom-ability",
|
||||
uint16(dnfenum.CmdPacketCustomAbilityUpgrade): "custom-ability",
|
||||
uint16(dnfenum.CmdPacketAddEquipmentEffect): "equipment-effect",
|
||||
uint16(dnfenum.CmdPacketOpenAuraSkinSlot): "aura-skin-slot",
|
||||
uint16(dnfenum.CmdPacketSetQuestTrigger): "quest",
|
||||
uint16(dnfenum.CmdPacketSaveQuestNotify): "quest",
|
||||
uint16(dnfenum.CmdPacketSaveCharacterOption): "skill",
|
||||
uint16(dnfenum.CmdPacketGuildMemerList): "guild",
|
||||
uint16(dnfenum.CmdPacketGuildInfo): "guild",
|
||||
uint16(dnfenum.CmdPacketSetSubGuildMaster): "guild",
|
||||
uint16(dnfenum.CmdPacketGuildMemerListNext): "guild",
|
||||
uint16(dnfenum.CmdPacketGuildAllMemberList): "guild",
|
||||
uint16(dnfenum.CmdPacketRefreshGuildInfo): "guild",
|
||||
uint16(dnfenum.CmdPacketOpenGuildBoard): "guild",
|
||||
uint16(dnfenum.CmdPacketRefreshGuildboard): "guild",
|
||||
uint16(dnfenum.CmdPacketNotifyMessageToGuild): "guild",
|
||||
uint16(dnfenum.CmdPacketCallGuildCreateRight): "guild",
|
||||
uint16(dnfenum.UpperMsgSelectEnter): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketMoveMap): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketPrevVillage): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketDieBloodMonster): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketBloodRoundUIPrepareFinish): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketSelectUltimateDifficulty): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketDropItem): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketUseSkill): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketDieMonster): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketGetItem): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketDieCharacter): "dungeon-death-revive",
|
||||
uint16(dnfenum.CmdPacketScoreScrollState): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketCardSelectRightState): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketSelectCard): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketEplpCommand): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketDungeonEventStoryPause): "dungeon-tutorial-story",
|
||||
uint16(dnfenum.CmdPacketSetUserPosition): "town",
|
||||
uint16(dnfenum.CmdPacketTeleport): "town",
|
||||
uint16(dnfenum.CmdPacketSoloTelepoart): "town",
|
||||
uint16(dnfenum.CmdPacketImageCommunicationEquipmentUse): "town",
|
||||
uint16(dnfenum.CmdPacketRequestAdventureInfo): "adventure-info",
|
||||
uint16(dnfenum.CmdPacketTitleBookPut): "title-book",
|
||||
uint16(currentSetCloneTitleMsgID): "clone-title",
|
||||
uint16(dnfenum.CmdPacketAchievementTrigger): "achievement",
|
||||
uint16(dnfenum.CmdPacketChangeEmotion): "emotion",
|
||||
uint16(dnfenum.CmdPacketSelectCollectbox): "collectbox",
|
||||
}
|
||||
for opcode, want := range legacyOwners {
|
||||
if route, ok := currentGameplayModules.legacyHandlers[opcode]; !ok || route.module != want {
|
||||
t.Errorf("legacy opcode=%d owner=%q found=%t want=%q", opcode, route.module, ok, want)
|
||||
}
|
||||
}
|
||||
|
||||
upperOwners := map[uint16]string{
|
||||
uint16(dnfenum.CmdPacketCompoundAvatar): "avatar-compound",
|
||||
uint16(dnfenum.CmdPacketAvatarFittingRoomChange): "avatar-fitting-room",
|
||||
uint16(dnfenum.CmdPacketAvatarOptionChange): "avatar-option-change",
|
||||
uint16(dnfenum.CmdPacketSkillSwitchInventory): "skill-switch-inventory",
|
||||
uint16(dnfenum.CmdPacketShopPurchaseCount): "npc-shop",
|
||||
uint16(dnfenum.CmdPacketRecoverStamina): "premium-service",
|
||||
uint16(dnfenum.CmdPacketPremiumService): "premium-service",
|
||||
uint16(dnfenum.CmdPacketUseLotteryItem): "booster-item",
|
||||
uint16(dnfenum.CmdPacketUseVendingMachine): "vending-machine",
|
||||
uint16(dnfenum.CmdPacketEpicProductionStartFinish): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionChangeItem): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionProcess): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionMaterialCompound): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicProductionAbilityOption): "epic-production",
|
||||
uint16(dnfenum.CmdPacketEpicBookMakeItem): "epic-book",
|
||||
uint16(dnfenum.CmdPacketUpdateWishItem): "epic-wish",
|
||||
uint16(dnfenum.CmdPacketCustomAbilityEquipOption): "custom-ability",
|
||||
uint16(dnfenum.CmdPacketCustomAbilitySetEquipOption): "custom-ability",
|
||||
uint16(dnfenum.CmdPacketCustomAbilityUpgrade): "custom-ability",
|
||||
uint16(dnfenum.UpperMsgSelectEnter): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketMoveMap): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketGiveupGame): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketBack2Village): "dungeon-navigation",
|
||||
uint16(dnfenum.CmdPacketDieBloodMonster): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketBloodRoundUIPrepareFinish): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketSelectUltimateDifficulty): "dungeon-altar",
|
||||
uint16(dnfenum.CmdPacketDropItem): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketUseSkill): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketDieMonster): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketGetItem): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketBossDieCheck): "dungeon-combat-drop",
|
||||
uint16(dnfenum.CmdPacketDieCharacter): "dungeon-death-revive",
|
||||
uint16(dnfenum.CmdPacketUseCoin): "dungeon-death-revive",
|
||||
uint16(dnfenum.CmdPacketSetPlayResult): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketScoreScrollState): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketCardSelectRightState): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketSelectCard): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketEplpCommand): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketCharacterStatistic): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketChangeTutorialFlag): "dungeon-tutorial-story",
|
||||
uint16(dnfenum.CmdPacketDungeonMissionCheckSuccess): "dungeon-tutorial-story",
|
||||
uint16(dnfenum.CmdPacketDeathTowerStageCmd): "death-tower",
|
||||
uint16(dnfenum.CmdPacketSoloTelepoart): "town",
|
||||
uint16(dnfenum.CmdPacketImageCommunicationEquipmentUse): "town",
|
||||
uint16(dnfenum.CmdPacketTitleBookGet): "title-book",
|
||||
uint16(currentSetCloneTitleMsgID): "clone-title",
|
||||
uint16(dnfenum.CmdPacketSaveCharacterOption): "skill",
|
||||
}
|
||||
for opcode, want := range upperOwners {
|
||||
if route, ok := currentGameplayModules.upperHandlers[opcode]; !ok || route.module != want {
|
||||
t.Errorf("upper opcode=%d owner=%q found=%t want=%q", opcode, route.module, ok, want)
|
||||
}
|
||||
}
|
||||
|
||||
normalizerOwners := map[uint16]string{
|
||||
uint16(dnfenum.CmdPacketSkillSwitchInventory): "skill-switch-inventory",
|
||||
uint16(dnfenum.CmdPacketShopPurchaseCount): "npc-shop",
|
||||
uint16(dnfenum.CmdPacketMoveItemspace): "inventory",
|
||||
uint16(dnfenum.CmdPacketChangeSkillslot): "skill",
|
||||
uint16(dnfenum.CmdPacketSetQuestTrigger): "quest",
|
||||
uint16(dnfenum.CmdPacketDieCharacter): "dungeon-death-revive",
|
||||
uint16(dnfenum.CmdPacketSelectCard): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketEplpCommand): "dungeon-settlement-card",
|
||||
uint16(dnfenum.CmdPacketDungeonEventStoryPause): "dungeon-tutorial-story",
|
||||
uint16(dnfenum.CmdPacketUseGem): "guardian-gem",
|
||||
uint16(dnfenum.CmdPacketDisjointItem): "equipment-disjoint",
|
||||
uint16(dnfenum.CmdPacketCompoundAvatar): "avatar-compound",
|
||||
uint16(dnfenum.CmdPacketAvatarFittingRoomChange): "avatar-fitting-room",
|
||||
uint16(dnfenum.CmdPacketDisjointAvatar): "avatar-disjoint",
|
||||
uint16(dnfenum.CmdPacketCompoundEmblem): "emblem-compound",
|
||||
}
|
||||
for opcode, want := range normalizerOwners {
|
||||
if route, ok := currentGameplayModules.legacyNormalizers[opcode]; !ok || route.module != want {
|
||||
t.Errorf("normalizer opcode=%d owner=%q found=%t want=%q", opcode, route.module, ok, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloneTitleModuleUsesCurrentNoPackOpcode568(t *testing.T) {
|
||||
if currentSetCloneTitleMsgID != uint16(dnfenum.CmdPacketSetCloneTitle) {
|
||||
t.Fatalf(
|
||||
"clone title opcode=%d enum=%d",
|
||||
currentSetCloneTitleMsgID,
|
||||
dnfenum.CmdPacketSetCloneTitle,
|
||||
)
|
||||
}
|
||||
if currentSetCloneTitleMsgID != 568 {
|
||||
t.Fatalf("clone title opcode=%d want=568/0x0238", currentSetCloneTitleMsgID)
|
||||
}
|
||||
if _, ok := currentGameplayModules.legacyHandlers[569]; ok {
|
||||
t.Fatal("legacy opcode 569/0x0239 must not be registered as clone title")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameplayModuleOwnsLegacyNormalizationBoundary(t *testing.T) {
|
||||
body := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
|
||||
got, ok := currentGameplayModules.NormalizeLegacy(uint16(dnfenum.CmdPacketChangeSkillslot), body)
|
||||
if !ok || !reflect.DeepEqual(got, body[:8]) {
|
||||
t.Fatalf("normalized=%x found=%t want=%x", got, ok, body[:8])
|
||||
}
|
||||
if got, ok := currentGameplayModules.NormalizeLegacy(0xffff, body); ok || !reflect.DeepEqual(got, body) {
|
||||
t.Fatalf("unknown normalization=%x found=%t want unchanged", got, ok)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user