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,313 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfquest "longheng.io/server/internal/modules/dnf/quest"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
func TestCurrentDungeonAltarQuestMonsterDropAutomaticallyRetriesTransactionFailureOnce(t *testing.T) {
|
||||
service, session, runtime, _, connection := prepareCurrentDungeonAltarRuntimeTest(t)
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service.gameplayTimers = queue
|
||||
service.questCatalog = currentDungeonAltarQuestDropTestCatalog(t)
|
||||
service.pvfItemCatalog = &pvfDungeonDropCatalog{
|
||||
source: bridgePVFSource{},
|
||||
itemCache: map[uint32]dungeonDropItemDefinition{
|
||||
4363: {
|
||||
ItemID: 4363, Kind: dungeonDropItemStackable, StackableType: "[quest]",
|
||||
StackLimit: 999, SlotStart: 177, SlotEnd: 232,
|
||||
},
|
||||
},
|
||||
}
|
||||
// Freeze the real quest-5651 altar monster identity into the server-owned
|
||||
// sequence while retaining the compact one-room runtime fixture.
|
||||
runtime.Dungeon.ID = 11006
|
||||
runtime.Request.DungeonID = 11006
|
||||
runtime.altar.boundMonsters[0].MonsterCode = 56004
|
||||
|
||||
repositories, ok := service.repositoryGroup()
|
||||
if !ok {
|
||||
t.Fatal("repository group unavailable")
|
||||
}
|
||||
seededCharacter, found, err := repositories.Character.Load(context.Background(), "99")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character for reward isolation found=%t err=%v", found, err)
|
||||
}
|
||||
seededCharacter.Stats["exp"] = 54321
|
||||
seededCharacter.Stats["gold"] = 12345
|
||||
if err := repositories.Character.Save(context.Background(), seededCharacter); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "99",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
5651: {Status: "active"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
||||
CharacterID: "99",
|
||||
Slots: map[string]dnfrepo.ItemStack{},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
availableRepositories := repositories
|
||||
unavailableRepositories := repositories
|
||||
unavailableRepositories.CharacterSettlement = nil
|
||||
service.repositoryProvider = func() (dnfrepo.Group, bool) { return unavailableRepositories, true }
|
||||
|
||||
if err := service.handleCurrentDungeonAltarReady(session, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queue.fire(queue.task(t, 0), false)
|
||||
var sequence uint16
|
||||
for candidate, owner := range runtime.altar.activeSequences {
|
||||
if owner.MonsterCode != 56004 {
|
||||
t.Fatalf("sequence=%d monster=%d", candidate, owner.MonsterCode)
|
||||
}
|
||||
sequence = candidate
|
||||
}
|
||||
if sequence == 0 {
|
||||
t.Fatal("altar monster sequence missing")
|
||||
}
|
||||
body := []byte{1, byte(sequence), byte(sequence >> 8)}
|
||||
beforeFailure := connection.write.Len()
|
||||
if err := service.handleCurrentDungeonAltarDeaths(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if connection.write.Len() != beforeFailure {
|
||||
t.Fatalf("failed transaction emitted packets: %x", connection.write.Bytes()[beforeFailure:])
|
||||
}
|
||||
if _, active := runtime.altar.activeSequences[sequence]; !active {
|
||||
t.Fatal("failed quest-drop transaction permanently retired sequence")
|
||||
}
|
||||
retryTask := queue.task(t, 1)
|
||||
if retryTask.delay != currentDungeonAltarQuestDropRetryDelay {
|
||||
t.Fatalf("quest-drop retry delay=%s want=%s", retryTask.delay, currentDungeonAltarQuestDropRetryDelay)
|
||||
}
|
||||
pending := runtime.altar.questDropRetry
|
||||
if pending == nil || len(pending.Sequences) != 1 || pending.Sequences[0] != sequence ||
|
||||
len(pending.Deaths) != 1 || pending.Deaths[0].Sequence != sequence || pending.Version == 0 {
|
||||
t.Fatalf("quest-drop retry was not frozen: %+v", pending)
|
||||
}
|
||||
|
||||
service.repositoryProvider = func() (dnfrepo.Group, bool) { return availableRepositories, true }
|
||||
characterBefore, found, err := availableRepositories.Character.Load(context.Background(), "99")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character before death found=%t err=%v", found, err)
|
||||
}
|
||||
nextObjectKeyBefore := runtime.NextObjectKey
|
||||
beforeSuccess := connection.write.Len()
|
||||
if err := service.fireCurrentDungeonAltarQuestDropRetry(
|
||||
session,
|
||||
runtime,
|
||||
runtime.lifecycleToken+1,
|
||||
pending.Room,
|
||||
pending.Version,
|
||||
retryTask.name,
|
||||
queue.Now(),
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if connection.write.Len() != beforeSuccess {
|
||||
t.Fatalf("stale lifecycle retry emitted packet: %x", connection.write.Bytes()[beforeSuccess:])
|
||||
}
|
||||
if _, active := runtime.altar.activeSequences[sequence]; !active {
|
||||
t.Fatal("stale lifecycle retry retired sequence")
|
||||
}
|
||||
queue.fire(retryTask, false)
|
||||
if _, active := runtime.altar.activeSequences[sequence]; active {
|
||||
t.Fatal("server-owned retry did not retire sequence")
|
||||
}
|
||||
if _, retired := runtime.altar.retiredSequences[sequence]; !retired {
|
||||
t.Fatal("server-owned retry did not record retired sequence")
|
||||
}
|
||||
if runtime.altar.questDropRetry != nil || runtime.altar.questDropRetryTimerName != "" {
|
||||
t.Fatalf("successful server retry remained armed: pending=%+v timer=%q",
|
||||
runtime.altar.questDropRetry, runtime.altar.questDropRetryTimerName)
|
||||
}
|
||||
stream := connection.write.Bytes()[beforeSuccess:]
|
||||
itemUpdates := 0
|
||||
for len(stream) != 0 {
|
||||
packet, rest := splitGameServerUpperPacket(t, stream)
|
||||
if packet.Header.Classification == 0 && packet.Header.MsgID == uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
itemUpdates++
|
||||
} else if packet.Header.Classification != 0 || packet.Header.MsgID != currentDungeonAltarDifficultyUIMsgID {
|
||||
t.Fatalf("altar quest death emitted unsupported ordinary reward packet: class=%d msg=%d body=%x",
|
||||
packet.Header.Classification, packet.Header.MsgID, packet.Body)
|
||||
}
|
||||
stream = rest
|
||||
}
|
||||
if itemUpdates != 1 {
|
||||
t.Fatalf("quest item update packets=%d", itemUpdates)
|
||||
}
|
||||
inventory, found, err := availableRepositories.Inventory.Load(context.Background(), "99")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||||
}
|
||||
stack := inventory.Slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 177)]
|
||||
if stack.ItemID != 4363 || stack.Count != 1 {
|
||||
t.Fatalf("quest-5651 item stack=%+v", stack)
|
||||
}
|
||||
characterAfter, found, err := availableRepositories.Character.Load(context.Background(), "99")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character after death found=%t err=%v", found, err)
|
||||
}
|
||||
if !reflect.DeepEqual(characterAfter.Stats, characterBefore.Stats) {
|
||||
t.Fatalf("altar task-drop path changed ordinary character rewards: before=%v after=%v", characterBefore.Stats, characterAfter.Stats)
|
||||
}
|
||||
if characterAfter.Stats["exp"] != 54321 || characterAfter.Stats["gold"] != 12345 ||
|
||||
runtime.settlementMonsterExperienceTotal != 0 {
|
||||
t.Fatalf("altar task-drop path granted ordinary exp/gold: stats=%v monster_exp_total=%d",
|
||||
characterAfter.Stats, runtime.settlementMonsterExperienceTotal)
|
||||
}
|
||||
if runtime.DropOwner != nil || runtime.NextObjectKey != nextObjectKeyBefore {
|
||||
t.Fatalf("altar task-drop path created ordinary ground drops: owner=%+v next=%d want_next=%d",
|
||||
runtime.DropOwner, runtime.NextObjectKey, nextObjectKeyBefore)
|
||||
}
|
||||
|
||||
beforeStale := connection.write.Len()
|
||||
queue.fire(retryTask, true)
|
||||
if connection.write.Len() != beforeStale {
|
||||
t.Fatalf("stale completed retry emitted packet: %x", connection.write.Bytes()[beforeStale:])
|
||||
}
|
||||
inventory, found, err = availableRepositories.Inventory.Load(context.Background(), "99")
|
||||
if err != nil || !found || inventory.Slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 177)].Count != 1 {
|
||||
t.Fatalf("stale completed retry duplicated quest item: inventory=%+v found=%t err=%v", inventory, found, err)
|
||||
}
|
||||
|
||||
beforeDuplicate := connection.write.Len()
|
||||
if err := service.handleCurrentDungeonAltarDeaths(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if connection.write.Len() != beforeDuplicate {
|
||||
t.Fatalf("duplicate sequence emitted packet: %x", connection.write.Bytes()[beforeDuplicate:])
|
||||
}
|
||||
inventory, found, err = availableRepositories.Inventory.Load(context.Background(), "99")
|
||||
if err != nil || !found || inventory.Slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, 177)].Count != 1 {
|
||||
t.Fatalf("duplicate sequence granted quest item twice: inventory=%+v found=%t err=%v", inventory, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentDungeonAltarQuestDropBatchesMaximumDeathListIntoOneTransaction(t *testing.T) {
|
||||
service, session, runtime, _, _ := prepareCurrentDungeonAltarRuntimeTest(t)
|
||||
service.questCatalog = currentDungeonAltarQuestDropTestCatalog(t)
|
||||
repositories, ok := service.repositoryGroup()
|
||||
if !ok {
|
||||
t.Fatal("repository group unavailable")
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "99",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
999999: {Status: "active"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
counter := &countingCurrentDungeonAltarSettlement{base: repositories.CharacterSettlement}
|
||||
repositories.CharacterSettlement = counter
|
||||
service.repositoryProvider = func() (dnfrepo.Group, bool) { return repositories, true }
|
||||
|
||||
deaths := make([]currentDungeonAltarDeathSequence, 0, currentDungeonAltarMaximumDeathSequences)
|
||||
for sequence := uint16(1); sequence <= currentDungeonAltarMaximumDeathSequences; sequence++ {
|
||||
deaths = append(deaths, currentDungeonAltarDeathSequence{
|
||||
Sequence: sequence,
|
||||
Owner: currentDungeonAltarSequenceOwner{
|
||||
RoomGeneration: runtime.altar.Room.Generation,
|
||||
RoundGeneration: 2,
|
||||
RoomKey: runtime.altar.Room.Key,
|
||||
MonsterCode: 56004,
|
||||
QuestDropSeed: uint32(sequence),
|
||||
},
|
||||
})
|
||||
}
|
||||
if err := service.commitCurrentDungeonAltarQuestDropsLocked(session, runtime, deaths); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if counter.calls != 1 {
|
||||
t.Fatalf("maximum op255 batch transactions=%d want=1", counter.calls)
|
||||
}
|
||||
if len(runtime.altar.questDropResults) != currentDungeonAltarMaximumDeathSequences {
|
||||
t.Fatalf("cached no-drop results=%d want=%d", len(runtime.altar.questDropResults), currentDungeonAltarMaximumDeathSequences)
|
||||
}
|
||||
for _, death := range deaths {
|
||||
if result, found := runtime.altar.questDropResults[death.Sequence]; !found || !result.Delivered || len(result.Body) != 0 {
|
||||
t.Fatalf("sequence=%d cached result=%+v found=%t", death.Sequence, result, found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealScriptPVFQuest5651UsesTypeOneEnemyRewardItem(t *testing.T) {
|
||||
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
|
||||
if pvfPath == "" {
|
||||
pvfPath = os.Getenv("DNF_WORLDMAP_REAL_PVF_SMOKE")
|
||||
}
|
||||
if pvfPath == "" {
|
||||
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE or DNF_WORLDMAP_REAL_PVF_SMOKE to verify quest 5651")
|
||||
}
|
||||
archive, err := platformpvf.Open(pvfPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
index, err := dnfpvf.Build(context.Background(), archive, 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)
|
||||
}
|
||||
definition, found := catalog.Find(5651)
|
||||
if !found {
|
||||
t.Fatal("real quest 5651 missing")
|
||||
}
|
||||
for _, entry := range definition.EnemyRewardItems {
|
||||
if entry.EnemyType == 1 && entry.EnemyCode == 56004 && entry.ItemID == 4363 && entry.DropRate == 100 {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("quest 5651 enemy reward rows=%+v", definition.EnemyRewardItems)
|
||||
}
|
||||
|
||||
type countingCurrentDungeonAltarSettlement struct {
|
||||
base dnfrepo.CharacterSettlementUnitOfWork
|
||||
calls int
|
||||
}
|
||||
|
||||
func (counter *countingCurrentDungeonAltarSettlement) WithinCharacterSettlement(
|
||||
ctx context.Context,
|
||||
characterID string,
|
||||
apply func(dnfrepo.Group) error,
|
||||
) error {
|
||||
counter.calls++
|
||||
return counter.base.WithinCharacterSettlement(ctx, characterID, apply)
|
||||
}
|
||||
|
||||
func currentDungeonAltarQuestDropTestCatalog(t *testing.T) *dnfquest.Catalog {
|
||||
t.Helper()
|
||||
source := bridgePVFSource{
|
||||
dnfquest.DefaultList: "5651 `altar_5651.qst`\n",
|
||||
"n_quest/altar_5651.qst": "[grade]\n`[normal]`\n" +
|
||||
"[type]\n`[seeking]`\n" +
|
||||
"[int data]\n4363 1\n" +
|
||||
"[enemy reward item]\n56004 1 11006 -1 4363 1 100 999\n[/enemy reward item]\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