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

731 lines
27 KiB
Go

package dnfbridge
import (
"context"
"encoding/binary"
"errors"
"os"
"strconv"
"strings"
"testing"
dnfdungeonadmission "longheng.io/server/internal/modules/dnf/dungeonadmission"
"longheng.io/server/internal/modules/dnf/dungeoncmd"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
"longheng.io/server/internal/modules/dnf/worldmap"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestCurrentDungeonEntryConsumesRequiredMainInventoryItemAndCanRestore(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:4": {ItemID: 4726, Count: 2},
"0:8": {ItemID: 9001, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4726, Count: 1}},
}}}
updates, restore, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || restore == nil {
t.Fatalf("updates=%+v restore=%v", updates, restore != nil)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || after.Slots["0:4"].Count != 1 {
t.Fatalf("after consume=%+v found=%t err=%v", after, found, err)
}
if err := restore(); err != nil {
t.Fatal(err)
}
restored, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || restored.Slots["0:4"].Count != 2 {
t.Fatalf("after restore=%+v found=%t err=%v", restored, found, err)
}
}
func TestCurrentTowerOfDespairEntryAcceptsEveryDirectInvitationAlternative(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
catalog, err := dnfdungeonadmission.LoadCatalog(dungeonAdmissionBridgeCatalogSource())
if err != nil {
t.Fatal(err)
}
service.dungeonAdmissionCatalog = catalog
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 11008, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4473, Count: 1}},
}}}
for _, definition := range currentTowerOfDespairEntryItemDefinitions {
t.Run(strconv.FormatInt(definition.ItemID, 10), func(t *testing.T) {
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:140": {ItemID: definition.ItemID, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 140 || admissionUpdateItemID(updates[0]) != uint32(definition.ItemID) || admissionUpdateCount(updates[0]) != 1 {
t.Fatalf("item=%d updates=%+v", definition.ItemID, updates)
}
inventory := loadAdmissionInventory(t, repositories, "19")
if stack := inventory.Slots["0:140"]; stack.ItemID != definition.ItemID || stack.Count != 1 {
t.Fatalf("item=%d inventory=%+v", definition.ItemID, inventory.Slots)
}
})
}
}
func TestCurrentTowerEntryConsumesOnlyTheMatchedFamilyInvitation(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
catalog, err := dnfdungeonadmission.LoadCatalog(dungeonAdmissionBridgeCatalogSource())
if err != nil {
t.Fatal(err)
}
service.dungeonAdmissionCatalog = catalog
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:141": {ItemID: 10161065, Count: 2},
"0:142": {ItemID: 490004952, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 5115, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 10161065, Count: 1}},
}}}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 141 || admissionUpdateItemID(updates[0]) != 10161065 || admissionUpdateCount(updates[0]) != 1 {
t.Fatalf("grief updates=%+v", updates)
}
inventory := loadAdmissionInventory(t, repositories, "19")
if inventory.Slots["0:141"].Count != 1 || inventory.Slots["0:142"].Count != 2 {
t.Fatalf("tower families were mixed: %+v", inventory.Slots)
}
}
func TestCurrentTowerOfDespairEntryPrefersCanonicalInvitation(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
catalog, err := dnfdungeonadmission.LoadCatalog(dungeonAdmissionBridgeCatalogSource())
if err != nil {
t.Fatal(err)
}
service.dungeonAdmissionCatalog = catalog
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:141": {ItemID: 490004952, Count: 2},
"0:142": {ItemID: 4473, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 11008, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4473, Count: 1}},
}}}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 142 || admissionUpdateItemID(updates[0]) != 4473 || admissionUpdateCount(updates[0]) != 1 {
t.Fatalf("canonical priority updates=%+v", updates)
}
inventory := loadAdmissionInventory(t, repositories, "19")
if inventory.Slots["0:141"].Count != 2 || inventory.Slots["0:142"].Count != 1 {
t.Fatalf("canonical priority inventory=%+v", inventory.Slots)
}
}
func TestCurrentTowerEntryDoesNotConsumeBoxesOrAllowanceItems(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
catalog, err := dnfdungeonadmission.LoadCatalog(dungeonAdmissionBridgeCatalogSource())
if err != nil {
t.Fatal(err)
}
service.dungeonAdmissionCatalog = catalog
for _, test := range []struct {
name string
dungeonID int64
canonical int64
itemID int64
}{
{name: "despair booster", dungeonID: 11008, canonical: 4473, itemID: 10002584},
{name: "despair allowance", dungeonID: 11008, canonical: 4473, itemID: 490002629},
{name: "despair action", dungeonID: 11008, canonical: 4473, itemID: 10005999},
{name: "grief booster", dungeonID: 5115, canonical: 10161065, itemID: 10161084},
} {
t.Run(test.name, func(t *testing.T) {
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:143": {ItemID: test.itemID, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: test.dungeonID, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: test.canonical, Count: 1}},
}}}
if _, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime); !errors.Is(err, errDungeonEntryItemMissing) {
t.Fatalf("item=%d err=%v", test.itemID, err)
}
inventory := loadAdmissionInventory(t, repositories, "19")
if stack := inventory.Slots["0:143"]; stack.ItemID != test.itemID || stack.Count != 1 {
t.Fatalf("item=%d was consumed: %+v", test.itemID, inventory.Slots)
}
})
}
}
func TestCurrentTowerEntryInvitationDefinitionsMatchRealPVF(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify tower entry invitation alternatives")
}
archive, err := platformpvf.Open(pvfPath)
if err != nil {
t.Fatal(err)
}
catalog, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
for family, definitions := range map[string][]currentTowerEntryItemDefinition{
"绝望之邀请函": currentTowerOfDespairEntryItemDefinitions,
"悲叹之邀请函": currentTowerOfGriefEntryItemDefinitions,
} {
for _, expected := range definitions {
definition, err := catalog.ResolveItem(uint32(expected.ItemID))
if err != nil {
t.Fatalf("%s item=%d: %v", family, expected.ItemID, err)
}
if !strings.EqualFold(definition.PVFPath, expected.PVFPath) ||
!strings.Contains(definition.Name, family) ||
!strings.EqualFold(normalizeDungeonDropStackableType(definition.StackableType), expected.StackableType) ||
strings.TrimSpace(definition.ActionType) != "" {
t.Fatalf("%s expected=%+v definition=%+v", family, expected, definition)
}
}
}
}
func TestCurrentDungeonEntryRestorePreservesUnrelatedConcurrentSlot(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:4": {ItemID: 4726, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4726, Count: 1}},
}}}
_, restore, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil || restore == nil {
t.Fatalf("consume restore=%v err=%v", restore != nil, err)
}
concurrent := loadAdmissionInventory(t, repositories, "19")
concurrent.Slots["0:9"] = dnfrepo.ItemStack{ItemID: 9009, Count: 7}
if err := repositories.Inventory.Save(context.Background(), concurrent); err != nil {
t.Fatal(err)
}
if err := restore(); err != nil {
t.Fatal(err)
}
restored := loadAdmissionInventory(t, repositories, "19")
if stack := restored.Slots["0:4"]; stack.ItemID != 4726 || stack.Count != 2 {
t.Fatalf("admission slot not restored: %+v", restored.Slots)
}
if stack := restored.Slots["0:9"]; stack.ItemID != 9009 || stack.Count != 7 {
t.Fatalf("unrelated concurrent slot was overwritten: %+v", restored.Slots)
}
}
func TestCurrentDungeonEntryRestoreConflictDoesNotPartiallyRestoreSlots(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:4": {ItemID: 4726, Count: 1},
"0:5": {ItemID: 4727, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4726, Count: 1}, {ItemID: 4727, Count: 1}},
}}}
_, restore, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil || restore == nil {
t.Fatalf("consume restore=%v err=%v", restore != nil, err)
}
concurrent := loadAdmissionInventory(t, repositories, "19")
if concurrent.Slots == nil {
concurrent.Slots = make(map[string]dnfrepo.ItemStack)
}
concurrent.Slots["0:4"] = dnfrepo.ItemStack{ItemID: 9004, Count: 3}
if err := repositories.Inventory.Save(context.Background(), concurrent); err != nil {
t.Fatal(err)
}
if err := restore(); !errors.Is(err, errDungeonEntryItemRestoreConflict) {
t.Fatalf("restore conflict err=%v", err)
}
after := loadAdmissionInventory(t, repositories, "19")
if stack := after.Slots["0:4"]; stack.ItemID != 9004 || stack.Count != 3 {
t.Fatalf("conflicting slot was overwritten: %+v", after.Slots)
}
if _, found := after.Slots["0:5"]; found {
t.Fatalf("nonconflicting admission slot was partially restored: %+v", after.Slots)
}
}
func TestCurrentDungeonEntryPrefersParticularHellFreePassFromDynamicSlot(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
316 -1
[/dungeon]
[particularhell freepass item]
490005479 1 490005974 1
[/particularhell freepass item]
`)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:4": {ItemID: 10158964, Count: 2},
"0:177": {ItemID: 490005479, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 316, Metadata: worldmap.DungeonMetadata{
DungeonType: "[particular hell]",
RequiredItems: []worldmap.TicketItem{{ItemID: 10158964, Count: 2}},
}}}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 177 || admissionUpdateItemID(updates[0]) != 490005479 || admissionUpdateCount(updates[0]) != 1 {
t.Fatalf("freepass updates=%+v", updates)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || after.Slots["0:177"].Count != 1 || after.Slots["0:4"].Count != 2 {
t.Fatalf("after freepass consume=%+v found=%t err=%v", after, found, err)
}
}
func TestCurrentDungeonEntryFallsBackToParticularHellRequiredItems(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
316 -1
[/dungeon]
[particularhell freepass item]
490005479 1
[/particularhell freepass item]
`)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:4": {ItemID: 10158964, Count: 3},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 316, Metadata: worldmap.DungeonMetadata{
DungeonType: "[particular hell]",
RequiredItems: []worldmap.TicketItem{{ItemID: 10158964, Count: 2}},
}}}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 4 || admissionUpdateCount(updates[0]) != 1 {
t.Fatalf("required-item updates=%+v", updates)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || after.Slots["0:4"].Count != 1 {
t.Fatalf("after required consume=%+v found=%t err=%v", after, found, err)
}
}
func TestCurrentDungeonEntryUsesInfernoFreePassBeforePVFItemCondition(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
7539 -1
[/dungeon]
[item condition]
3330
[/item condition]
[inferno freepass item]
490701656 1 490006449 1
[/inferno freepass item]
`)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:9": {ItemID: 3330, Count: 40},
"0:88": {ItemID: 490006449, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 7539, Metadata: worldmap.DungeonMetadata{
InfernoDungeon: true,
HellPartyRequiredItemCount: worldmap.OptionalInt{Value: 30, Set: true},
}}}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 88 || admissionUpdateCount(updates[0]) != 0 {
t.Fatalf("inferno freepass updates=%+v", updates)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || after.Slots["0:9"].Count != 40 {
t.Fatalf("after inferno pass consume=%+v found=%t err=%v", after, found, err)
}
if _, passFound := after.Slots["0:88"]; passFound {
t.Fatalf("consumed inferno pass remained: %+v", after.Slots["0:88"])
}
}
func TestCurrentDungeonEntryFallsBackToHellItemConditionCount(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
310 -1
[/dungeon]
[hell dungeon]
1
[item condition]
3330
[/item condition]
[hell freepass item]
490006398 1
[/hell freepass item]
`)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:9": {ItemID: 3330, Count: 40},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{
Request: dungeoncmd.SelectDungeonRequest{SelectionMode: 1, RuntimeToken: 0xffff},
Dungeon: worldmap.Dungeon{ID: 310, Metadata: worldmap.DungeonMetadata{
HellPartyRequiredItemCount: worldmap.OptionalInt{Value: 28, Set: true},
}},
}
updates, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime)
if err != nil {
t.Fatal(err)
}
if len(updates) != 1 || admissionUpdateSlot(updates[0]) != 9 || admissionUpdateCount(updates[0]) != 12 {
t.Fatalf("hell material updates=%+v", updates)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found || after.Slots["0:9"].Count != 12 {
t.Fatalf("after hell material consume=%+v found=%t err=%v", after, found, err)
}
}
func TestCurrentDungeonEntryRejectsMissingRequiredMainInventoryItem(t *testing.T) {
service, session, _, _ := newPremiumServiceTestRuntime(t)
runtime := &runtimeDungeonState{
Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4726, Count: 1}},
}},
}
if _, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime); err == nil {
t.Fatal("missing ticket was accepted")
}
}
func TestCurrentDungeonEntryMissingRequirementDoesNotPartiallyConsume(t *testing.T) {
service, session, repositories, _ := newPremiumServiceTestRuntime(t)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:177": {ItemID: 4726, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
runtime := &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{
{ItemID: 4726, Count: 1},
{ItemID: 4727, Count: 1},
},
}}}
if _, _, err := service.consumeCurrentDungeonEntryItems(context.Background(), session, runtime); !errors.Is(err, errDungeonEntryItemMissing) {
t.Fatalf("error=%v", err)
}
after, found, err := repositories.Inventory.Load(context.Background(), "19")
if err != nil || !found {
t.Fatalf("load after rejection: found=%t err=%v", found, err)
}
if stack := after.Slots["0:177"]; stack.ItemID != 4726 || stack.Count != 2 {
t.Fatalf("first requirement was partially consumed: %+v", stack)
}
}
func TestCurrentDungeonEntryPartyConsumesEachMembersOwnAdmissionChoice(t *testing.T) {
service, leader, repositories, _ := newPremiumServiceTestRuntime(t)
follower := &gameSession{selectedCharacterID: 20}
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
316 -1
[/dungeon]
[particularhell freepass item]
490005479 1
[/particularhell freepass item]
`)
for characterID, slots := range map[string]map[string]dnfrepo.ItemStack{
"19": {
"0:177": {ItemID: 490005479, Count: 1},
"0:4": {ItemID: 10158964, Count: 2},
},
"20": {
"0:178": {ItemID: 10158964, Count: 2},
},
} {
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: characterID,
Slots: slots,
}); err != nil {
t.Fatal(err)
}
}
newRuntime := func() *runtimeDungeonState {
return &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 316, Metadata: worldmap.DungeonMetadata{
DungeonType: "[particular hell]",
RequiredItems: []worldmap.TicketItem{{ItemID: 10158964, Count: 2}},
}}}
}
prepared := []*runtimePartyPreparedDungeonEntry{{session: follower, runtime: newRuntime()}}
batch, err := service.consumeCurrentDungeonEntryPartyItems(
context.Background(), leader, newRuntime(), prepared)
if err != nil {
t.Fatal(err)
}
leaderUpdates := batch.updatesFor(leader)
followerUpdates := batch.updatesFor(follower)
if len(leaderUpdates) != 1 || admissionUpdateSlot(leaderUpdates[0]) != 177 ||
admissionUpdateItemID(leaderUpdates[0]) != ^uint32(0) || admissionUpdateCount(leaderUpdates[0]) != 0 {
t.Fatalf("leader freepass updates=%+v", leaderUpdates)
}
if len(followerUpdates) != 1 || admissionUpdateSlot(followerUpdates[0]) != 178 ||
admissionUpdateItemID(followerUpdates[0]) != ^uint32(0) || admissionUpdateCount(followerUpdates[0]) != 0 {
t.Fatalf("follower material updates=%+v", followerUpdates)
}
if len(prepared[0].entryItemUpdates) != 1 ||
admissionUpdateSlot(prepared[0].entryItemUpdates[0]) != 178 {
t.Fatalf("follower did not retain its own op14 projection: %+v", prepared[0].entryItemUpdates)
}
leaderInventory := loadAdmissionInventory(t, repositories, "19")
followerInventory := loadAdmissionInventory(t, repositories, "20")
if _, found := leaderInventory.Slots["0:177"]; found || leaderInventory.Slots["0:4"].Count != 2 {
t.Fatalf("leader admission choice=%+v", leaderInventory.Slots)
}
if _, found := followerInventory.Slots["0:178"]; found {
t.Fatalf("follower admission choice=%+v", followerInventory.Slots)
}
}
func TestCurrentDungeonEntryPartyFollowerMissingRollsBackEveryEarlierMember(t *testing.T) {
service, leader, repositories, _ := newPremiumServiceTestRuntime(t)
follower := &gameSession{selectedCharacterID: 20}
service.worldMapTable = currentDungeonEntryAdmissionTestWorldMap(t, `
[dungeon]
316 -1
[/dungeon]
[particularhell freepass item]
490005479 1
[/particularhell freepass item]
`)
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:177": {ItemID: 490005479, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "20",
Slots: map[string]dnfrepo.ItemStack{},
}); err != nil {
t.Fatal(err)
}
runtime := func() *runtimeDungeonState {
return &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 316, Metadata: worldmap.DungeonMetadata{
DungeonType: "[particular hell]",
RequiredItems: []worldmap.TicketItem{{ItemID: 10158964, Count: 2}},
}}}
}
prepared := []*runtimePartyPreparedDungeonEntry{{session: follower, runtime: runtime()}}
if _, err := service.consumeCurrentDungeonEntryPartyItems(
context.Background(), leader, runtime(), prepared,
); !errors.Is(err, errDungeonEntryItemMissing) {
t.Fatalf("party missing error=%v", err)
}
leaderInventory := loadAdmissionInventory(t, repositories, "19")
if stack := leaderInventory.Slots["0:177"]; stack.ItemID != 490005479 || stack.Count != 1 {
t.Fatalf("leader freepass was not restored after follower rejection: %+v", leaderInventory.Slots)
}
if len(prepared[0].entryItemUpdates) != 0 {
t.Fatalf("rejected follower retained stale op14 projection: %+v", prepared[0].entryItemUpdates)
}
}
func TestCurrentDungeonEntryLeaderRuntimeCommitFailureRestoresEveryPartyMember(t *testing.T) {
service, leader, repositories, _ := newPremiumServiceTestRuntime(t)
follower := &gameSession{selectedCharacterID: 20}
for _, characterID := range []string{"19", "20"} {
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: characterID,
Slots: map[string]dnfrepo.ItemStack{
"0:180": {ItemID: 4726, Count: 1},
},
}); err != nil {
t.Fatal(err)
}
}
newRuntime := func() *runtimeDungeonState {
return &runtimeDungeonState{Dungeon: worldmap.Dungeon{ID: 120, Metadata: worldmap.DungeonMetadata{
RequiredItems: []worldmap.TicketItem{{ItemID: 4726, Count: 1}},
}}}
}
prepared := []*runtimePartyPreparedDungeonEntry{{
session: follower,
runtime: newRuntime(),
}}
batch, err := service.consumeCurrentDungeonEntryPartyItems(
context.Background(), leader, newRuntime(), prepared)
if err != nil {
t.Fatal(err)
}
// The follower mailbox is released after its deduction. Preserve a later
// unrelated owner mutation when the leader runtime subsequently fails.
followerConcurrent := loadAdmissionInventory(t, repositories, "20")
if followerConcurrent.Slots == nil {
followerConcurrent.Slots = make(map[string]dnfrepo.ItemStack)
}
followerConcurrent.Slots["0:181"] = dnfrepo.ItemStack{ItemID: 9181, Count: 4}
if err := repositories.Inventory.Save(context.Background(), followerConcurrent); err != nil {
t.Fatal(err)
}
// A runtime without a worldmap session is rejected at the exact leader
// commit boundary, after every member's admission has been persisted.
if err := service.commitDungeonRuntimeWithPartyAdmission(
leader,
newRuntime(),
prepared,
batch,
); err == nil {
t.Fatal("invalid leader runtime commit unexpectedly succeeded")
}
for _, characterID := range []string{"19", "20"} {
inventory := loadAdmissionInventory(t, repositories, characterID)
if stack := inventory.Slots["0:180"]; stack.ItemID != 4726 || stack.Count != 1 {
t.Fatalf("character %s admission was not restored after leader commit failure: %+v", characterID, inventory.Slots)
}
if characterID == "20" {
if stack := inventory.Slots["0:181"]; stack.ItemID != 9181 || stack.Count != 4 {
t.Fatalf("follower concurrent slot was overwritten: %+v", inventory.Slots)
}
}
}
// The compensation owner is idempotent: an error-path retry cannot apply a
// second restore over later inventory state.
if err := service.restoreCurrentDungeonEntryInventoryBatch(batch); err != nil {
t.Fatal(err)
}
}
func TestDungeonEntryAdmissionDoesNotConjoinEventOrAddedItems(t *testing.T) {
service := &Service{}
for _, metadata := range []worldmap.DungeonMetadata{
{
RequiredItems: []worldmap.TicketItem{{ItemID: 1001, Count: 2}},
EventRequiredItems: []worldmap.TicketItem{{ItemID: 2001, Count: 1}},
},
{
RequiredItems: []worldmap.TicketItem{{ItemID: 1001, Count: 2}},
AddedRequiredItems: []worldmap.TicketItem{{ItemID: 3001, Count: 3}},
},
} {
_, err := service.resolveCurrentDungeonEntryAdmissionPlan(&runtimeDungeonState{
Dungeon: worldmap.Dungeon{ID: 9000, Metadata: metadata},
})
if err == nil {
t.Fatalf("event/schedule admission became an ordinary material conjunction: %+v", metadata)
}
}
}
func TestSelectDungeonEntryItemMissingFailureBody(t *testing.T) {
got := buildSelectDungeonFailureBody(currentSelectDungeonEntryItemMissingReason)
want := []byte{0, 17, 0}
if string(got) != string(want) {
t.Fatalf("failure body=%x want=%x", got, want)
}
}
func currentDungeonEntryAdmissionTestWorldMap(t *testing.T, areaDocument string) *worldmap.Table {
t.Helper()
table, err := worldmap.LoadSource(context.Background(), bridgePVFSource{
worldmap.DefaultWorldMapList: "39 `metrocenter.wdm`\n",
"worldmap/metrocenter.wdm": areaDocument,
}, worldmap.Options{SkipMaps: true, SkipDungeons: true})
if err != nil {
t.Fatal(err)
}
return table
}
func admissionUpdateSlot(entry currentItemListEntry) int16 {
return int16(binary.LittleEndian.Uint16(entry.data[0:2]))
}
func admissionUpdateItemID(entry currentItemListEntry) uint32 {
return binary.LittleEndian.Uint32(entry.data[2:6])
}
func admissionUpdateCount(entry currentItemListEntry) uint32 {
return binary.LittleEndian.Uint32(entry.data[6:10])
}
func loadAdmissionInventory(
t *testing.T,
repositories dnfrepo.Group,
characterID string,
) dnfrepo.InventoryRecord {
t.Helper()
inventory, found, err := repositories.Inventory.Load(context.Background(), characterID)
if err != nil || !found {
t.Fatalf("load admission inventory %s: found=%t err=%v", characterID, found, err)
}
return inventory
}