服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
1038 lines
38 KiB
Go
1038 lines
38 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"math"
|
|
"strconv"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
"longheng.io/server/internal/modules/dnf/dungeoncmd"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
|
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
|
)
|
|
|
|
func TestBuildCurrentDungeonNormalPickupResultMatchesCurrentEXEOp39ItemBranch(t *testing.T) {
|
|
body, err := buildCurrentDungeonNormalPickupResultBody(0x44332211, 401, 121)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(body) != currentDungeonPickupResultSize {
|
|
t.Fatalf("body len=%d", len(body))
|
|
}
|
|
if binary.LittleEndian.Uint32(body[0:4]) != 0x44332211 ||
|
|
binary.LittleEndian.Uint16(body[4:6]) != 401 ||
|
|
!bytes.Equal(body[6:14], make([]byte, 8)) ||
|
|
binary.LittleEndian.Uint16(body[14:16]) != 401 ||
|
|
binary.LittleEndian.Uint16(body[16:18]) != 121 ||
|
|
body[18] != 0 {
|
|
t.Fatalf("body=%x", body)
|
|
}
|
|
for _, test := range []struct {
|
|
drop uint32
|
|
picker uint16
|
|
slot uint16
|
|
}{{picker: 1, slot: 1}, {drop: 1, slot: 1}, {drop: 1, picker: 1}} {
|
|
if _, err := buildCurrentDungeonNormalPickupResultBody(test.drop, test.picker, test.slot); !errors.Is(err, errDungeonPickupResponseInvalid) {
|
|
t.Fatalf("invalid response values accepted: %+v err=%v", test, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCurrentDungeonGoldPickupEffectMatchesCurrentEXEOp557(t *testing.T) {
|
|
body, err := buildCurrentDungeonGoldPickupEffectBody(979)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(body) != currentDungeonGoldPickupEffectSize || binary.LittleEndian.Uint16(body) != 979 {
|
|
t.Fatalf("body=%x", body)
|
|
}
|
|
for _, amount := range []uint32{0, math.MaxUint16 + 1} {
|
|
if _, err := buildCurrentDungeonGoldPickupEffectBody(amount); !errors.Is(err, errDungeonPickupResponseInvalid) {
|
|
t.Fatalf("invalid amount=%d accepted err=%v", amount, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAddCurrentDungeonPickupKeepsMaterialsInPVFCategorySlots(t *testing.T) {
|
|
material := dungeonDropItemDefinition{
|
|
ItemID: 3227,
|
|
Kind: dungeonDropItemStackable,
|
|
PVFPath: "stackable/material/test_drop.stk",
|
|
StackableType: "[material]",
|
|
StackLimit: 999,
|
|
SlotStart: 121,
|
|
SlotEnd: 176,
|
|
}
|
|
|
|
t.Run("existing quick-slot stack is ignored in favor of material category", func(t *testing.T) {
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:4": {ItemID: 3227, Count: 2},
|
|
"0:121": {ItemID: 3227, Count: 5},
|
|
}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, material, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 121 || record.Slots["0:4"].Count != 2 || record.Slots["0:121"].Count != 6 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("same material-category stack is merged", func(t *testing.T) {
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:121": {ItemID: 3227, Count: 5},
|
|
}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, material, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 121 || record.Slots["0:121"].Count != 6 || len(record.Slots) != 1 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("material subtype four uses independent material page", func(t *testing.T) {
|
|
materialFour := material
|
|
materialFour.StackableType = "[material] 4"
|
|
materialFour.SlotStart = 345
|
|
materialFour.SlotEnd = 359
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, materialFour, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 345 || record.Slots["0:345"].ItemID != 3227 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
if _, exists := record.Slots["0:3"]; exists {
|
|
t.Fatalf("material subtype four entered quick slot: %+v", record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("waste still prefers item quick slots", func(t *testing.T) {
|
|
waste := material
|
|
waste.StackableType = "[waste]"
|
|
waste.SlotStart = 65
|
|
waste.SlotEnd = 120
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, waste, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 3 || record.Slots["0:3"].ItemID != 3227 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("full quick slots fall back to original category stack", func(t *testing.T) {
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:121": {ItemID: 3227, Count: 5},
|
|
}}
|
|
for slot := int16(3); slot <= 8; slot++ {
|
|
record.Slots[currentDungeonPickupMainSlotKey(slot)] = dnfrepo.ItemStack{
|
|
ItemID: 900000 + int64(slot),
|
|
Count: 1,
|
|
}
|
|
}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, material, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 121 || record.Slots["0:121"].Count != 6 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("PVF stack limit zero merges as unlimited", func(t *testing.T) {
|
|
unlimited := material
|
|
unlimited.ItemID = 3050
|
|
unlimited.StackLimit = 0
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{
|
|
"0:136": {ItemID: 3050, Count: 1},
|
|
}}
|
|
for slot := int16(3); slot <= 8; slot++ {
|
|
record.Slots[currentDungeonPickupMainSlotKey(slot)] = dnfrepo.ItemStack{
|
|
ItemID: 900000 + int64(slot),
|
|
Count: 1,
|
|
}
|
|
}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, unlimited, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 136 || record.Slots["0:136"].Count != 2 || len(record.Slots) != 7 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
})
|
|
|
|
t.Run("quest type keeps original category", func(t *testing.T) {
|
|
quest := material
|
|
quest.StackableType = "[quest]"
|
|
quest.SlotStart = 177
|
|
quest.SlotEnd = 232
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, quest, 1)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if slot != 177 || record.Slots["0:177"].ItemID != 3227 {
|
|
t.Fatalf("slot=%d inventory=%+v", slot, record.Slots)
|
|
}
|
|
if _, exists := record.Slots["0:3"]; exists {
|
|
t.Fatalf("quest item entered quick slot: %+v", record.Slots)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAddCurrentDungeonPickupPersistsPVFStackableUsePeriodInRaw77(t *testing.T) {
|
|
expirationDate := time.Date(2028, time.August, 15, 22, 0, 0, 0, time.UTC)
|
|
definition := dungeonDropItemDefinition{
|
|
ItemID: 490702319,
|
|
Kind: dungeonDropItemStackable,
|
|
PVFPath: "stackable/cash/chn_490702319.stk",
|
|
StackableType: "[booster]",
|
|
StackLimit: 1000,
|
|
SlotStart: 65,
|
|
SlotEnd: 120,
|
|
ExpirationDate: expirationDate,
|
|
}
|
|
record := dnfrepo.InventoryRecord{Slots: map[string]dnfrepo.ItemStack{}}
|
|
slot, err := addCurrentDungeonPickupToInventory(&record, definition, 20)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stack := record.Slots[currentDungeonPickupMainSlotKey(int16(slot))]
|
|
if !stack.ExpireAt.Equal(expirationDate) || stack.Extra["expire_time"] != "1849989600" || stack.Extra["expire_unix"] != "1849989600" ||
|
|
stack.Extra["item_kind"] != string(dungeonDropItemStackable) ||
|
|
len(stack.RawEntry) != currentItemListEntryWireSize || binary.LittleEndian.Uint16(stack.RawEntry[0x0B:0x0D]) != 0xFFFF ||
|
|
binary.LittleEndian.Uint32(stack.RawEntry[0x6E:0x72]) != 0 {
|
|
t.Fatalf("inserted expiring stack=%+v row=%x", stack, stack.RawEntry)
|
|
}
|
|
|
|
stale := stack
|
|
stale.Extra = nil
|
|
stale.RawEntry = append([]byte(nil), stale.RawEntry...)
|
|
binary.LittleEndian.PutUint16(stale.RawEntry[0x0B:0x0D], 0)
|
|
record.Slots[currentDungeonPickupMainSlotKey(int16(slot))] = stale
|
|
mergedSlot, err := addCurrentDungeonPickupToInventory(&record, definition, 5)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
merged := record.Slots[currentDungeonPickupMainSlotKey(int16(mergedSlot))]
|
|
if mergedSlot != slot || merged.Count != 25 || !merged.ExpireAt.Equal(expirationDate) || merged.Extra["expire_time"] != "1849989600" ||
|
|
binary.LittleEndian.Uint16(merged.RawEntry[0x0B:0x0D]) != 0xFFFF || binary.LittleEndian.Uint32(merged.RawEntry[0x6E:0x72]) != 0 {
|
|
t.Fatalf("merged expiring stack slot=%d stack=%+v", mergedSlot, merged)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonNormalMonsterDropRequiresRuntimePVFRules(t *testing.T) {
|
|
service, runtime, _ := prepareCurrentDungeonDropTest(t, 2, 3227)
|
|
monster := runtime.Room.Snapshot().Monsters[0]
|
|
if _, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99)); !errors.Is(err, errCurrentDungeonDropRulesUnavailable) {
|
|
t.Fatalf("normal-drop planner error=%v", err)
|
|
}
|
|
if runtime.NextObjectKey != 403 || runtime.DropOwner != nil {
|
|
t.Fatalf("deferred planner mutated runtime next=%d owner=%+v", runtime.NextObjectKey, runtime.DropOwner)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonGoldDropRegistersRealAmountForPickup(t *testing.T) {
|
|
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
|
|
source, ok := service.dungeonMonsterTable.source.(bridgePVFSource)
|
|
if !ok {
|
|
t.Fatalf("test source type=%T", service.dungeonMonsterTable.source)
|
|
}
|
|
source[dungeonCardDropRulePath] = currentDungeonGoldDropTestRuleText()
|
|
source[dungeonCardGoldReferencePath] = "[gold drop ref table]\n20 100 0\n"
|
|
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
|
|
currentDungeonGoldReferencesByCatalog.Delete(service.dungeonMonsterTable)
|
|
|
|
monster := runtime.Room.Snapshot().Monsters[0]
|
|
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var goldWire *currentDungeonDeathDropWire
|
|
for index := range wires {
|
|
if binary.LittleEndian.Uint32(wires[index].Item.data[0x02:0x06]) == 0 {
|
|
goldWire = &wires[index]
|
|
break
|
|
}
|
|
}
|
|
if goldWire == nil {
|
|
t.Fatalf("gold drop was not planned: wires=%+v", wires)
|
|
}
|
|
wireAmount := binary.LittleEndian.Uint32(goldWire.Item.data[0x06:0x0A])
|
|
if wireAmount != 120 {
|
|
t.Fatalf("gold wire amount=%d want 120", wireAmount)
|
|
}
|
|
registered := runtime.DropOwner.byObjectKey[goldWire.SceneObjectKey]
|
|
if registered == nil || !registered.isGold() || registered.Amount != wireAmount {
|
|
t.Fatalf("registered gold drop=%+v wire_amount=%d", registered, wireAmount)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonDropPlannerSkipsPersistedDropObjectKeys(t *testing.T) {
|
|
service, runtime, _ := prepareCurrentDungeonDropTest(t, 0, 3227)
|
|
source, ok := service.dungeonMonsterTable.source.(bridgePVFSource)
|
|
if !ok {
|
|
t.Fatalf("test source type=%T", service.dungeonMonsterTable.source)
|
|
}
|
|
source[dungeonCardDropRulePath] = currentDungeonGoldDropTestRuleText()
|
|
source[dungeonCardGoldReferencePath] = "[gold drop ref table]\n20 100 0\n"
|
|
currentDungeonMonsterDropRulesByCatalog.Delete(service.dungeonMonsterTable)
|
|
currentDungeonGoldReferencesByCatalog.Delete(service.dungeonMonsterTable)
|
|
|
|
staleNextObjectKey := runtime.NextObjectKey
|
|
owner := newRuntimeDungeonDropOwner()
|
|
if err := owner.registerBatch([]*runtimeDungeonDrop{{
|
|
ObjectKey: staleNextObjectKey,
|
|
SceneSlot: 1,
|
|
OwnerActorObjectKey: currentSceneActorObjectKey(99),
|
|
Room: runtimeDungeonRoomKey{X: -1, Y: -1, MapID: -1},
|
|
Status: runtimeDungeonDropConsumed,
|
|
}}, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.DropOwner = owner
|
|
|
|
monster := runtime.Room.Snapshot().Monsters[0]
|
|
wires, err := service.planCurrentDungeonMonsterDrops(runtime, monster, currentSceneActorObjectKey(99))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(wires) == 0 || wires[0].SceneObjectKey <= staleNextObjectKey {
|
|
t.Fatalf("drop planner reused persisted object key: wires=%+v prior=%d", wires, staleNextObjectKey)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonGoldPickupSendsRemoveEffectThenAbsoluteState(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 0, 3227)
|
|
connection := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-gold-pickup-effect-test",
|
|
selectedCharacterID: 99,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
scene, ok := runtime.Session.Scene()
|
|
if !ok {
|
|
t.Fatal("missing dungeon scene")
|
|
}
|
|
objectKey := runtime.NextObjectKey
|
|
drop := &runtimeDungeonDrop{
|
|
ObjectKey: objectKey,
|
|
SceneSlot: 1,
|
|
Amount: 979,
|
|
OwnerActorObjectKey: currentSceneActorObjectKey(99),
|
|
Room: runtimeDungeonRoomKeyFromScene(scene),
|
|
Status: runtimeDungeonDropAvailable,
|
|
}
|
|
owner := newRuntimeDungeonDropOwner()
|
|
if err := owner.registerBatch([]*runtimeDungeonDrop{drop}, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.DropOwner = owner
|
|
runtime.NextObjectKey++
|
|
|
|
pickupBody := currentDungeonPickupTestBody(objectKey)
|
|
if err := service.handleCurrentDungeonPickup(session, pickupBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
removePacket, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
effectPacket, rest := splitGameServerUpperPacket(t, rest)
|
|
goldStatePacket, trailing := splitGameServerUpperPacket(t, rest)
|
|
wantRemove, err := buildCurrentDungeonGoldPickupResultBody(objectKey, currentSceneActorObjectKey(99))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removePacket.Header.Classification != 0 || removePacket.Header.MsgID != currentDungeonPickupResultOpcode ||
|
|
!bytes.Equal(removePacket.Body, wantRemove) {
|
|
t.Fatalf("remove header=%+v body=%x", removePacket.Header, removePacket.Body)
|
|
}
|
|
if effectPacket.Header.Classification != 0 || effectPacket.Header.MsgID != currentDungeonGoldPickupEffectOpcode ||
|
|
len(effectPacket.Body) != currentDungeonGoldPickupEffectSize || binary.LittleEndian.Uint16(effectPacket.Body) != 979 {
|
|
t.Fatalf("effect header=%+v body=%x", effectPacket.Header, effectPacket.Body)
|
|
}
|
|
if goldStatePacket.Header.Classification != 0 || goldStatePacket.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
|
!bytes.Equal(goldStatePacket.Body, buildCurrentGoldStateBody(979)) || len(trailing) != 0 {
|
|
t.Fatalf("gold state header=%+v body=%x trailing=%x", goldStatePacket.Header, goldStatePacket.Body, trailing)
|
|
}
|
|
character, found, err := repositories.Character.Load(context.Background(), "99")
|
|
if err != nil || !found || character.Stats["gold"] != 979 {
|
|
t.Fatalf("character found=%t err=%v stats=%+v", found, err, character.Stats)
|
|
}
|
|
if drop.Status != runtimeDungeonDropConsumed || drop.GoldAfter != 979 {
|
|
t.Fatalf("consumed gold drop=%+v", drop)
|
|
}
|
|
|
|
connection.write.Reset()
|
|
if err := service.handleCurrentDungeonPickup(session, pickupBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
replayRemove, replayRest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
replayGoldState, replayTrailing := splitGameServerUpperPacket(t, replayRest)
|
|
if replayRemove.Header.MsgID != currentDungeonPickupResultOpcode ||
|
|
replayGoldState.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
|
len(replayTrailing) != 0 {
|
|
t.Fatalf("replay remove=%+v state=%+v trailing=%x", replayRemove.Header, replayGoldState.Header, replayTrailing)
|
|
}
|
|
character, _, err = repositories.Character.Load(context.Background(), "99")
|
|
if err != nil || character.Stats["gold"] != 979 {
|
|
t.Fatalf("replay changed gold err=%v stats=%+v", err, character.Stats)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonNormalPickupIsOwnedTransactionalAndIdempotent(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 2, 3227)
|
|
connection := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-drop-pickup-test",
|
|
selectedCharacterID: 99,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
monster := runtime.Room.Snapshot().Monsters[0]
|
|
deathBody := make([]byte, dungeoncmd.DieMonsterRequestSize)
|
|
binary.LittleEndian.PutUint32(deathBody[0:4], monster.ObjectKey)
|
|
binary.LittleEndian.PutUint16(deathBody[4:6], currentSceneActorObjectKey(99))
|
|
if err := service.handleDungeonMonsterDeath(session, deathBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
deathPacket, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if deathPacket.Header.MsgID != uint16(dnfenum.CmdPacketNotifyDieMonster) ||
|
|
deathPacket.Header.Classification != 0 {
|
|
t.Fatalf("death packet header=%+v rest=%x", deathPacket.Header, rest)
|
|
}
|
|
assertCurrentDungeonFinalClearTail(t, rest)
|
|
if len(deathPacket.Body) != currentDungeonZeroDropDeathBodySize || deathPacket.Body[2] != 0 {
|
|
t.Fatalf("death body len=%d count=%d body=%x", len(deathPacket.Body), deathPacket.Body[2], deathPacket.Body)
|
|
}
|
|
firstDropObjectKey := seedCurrentDungeonPickupDrop(t, service, runtime, 3227, currentSceneActorObjectKey(99))
|
|
|
|
connection.write.Reset()
|
|
pickupBody := currentDungeonPickupTestBody(firstDropObjectKey)
|
|
pickupFrame, err := dnfproto.BuildChannelPacket(
|
|
uint16(dnfenum.CmdPacketGetItem),
|
|
pickupBody,
|
|
1,
|
|
dnfproto.DefaultChannelClassification,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := service.handleGameUpper(session, pickupFrame); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pickupPacket, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
itemUpdate, trailing := splitGameServerUpperPacket(t, rest)
|
|
wantPickup, err := buildCurrentDungeonNormalPickupResultBody(firstDropObjectKey, currentSceneActorObjectKey(99), 121)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pickupPacket.Header.MsgID != currentDungeonPickupResultOpcode ||
|
|
pickupPacket.Header.Classification != 0 ||
|
|
!bytes.Equal(pickupPacket.Body, wantPickup) || len(trailing) != 0 {
|
|
t.Fatalf("pickup packet header=%+v body=%x trailing=%x", pickupPacket.Header, pickupPacket.Body, trailing)
|
|
}
|
|
if itemUpdate.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
|
itemUpdate.Header.Classification != 0 ||
|
|
len(itemUpdate.Body) != 3+currentItemListEntryWireSize ||
|
|
itemUpdate.Body[0] != dnfrepo.MainInventoryListType ||
|
|
binary.LittleEndian.Uint16(itemUpdate.Body[1:3]) != 1 ||
|
|
binary.LittleEndian.Uint16(itemUpdate.Body[3:5]) != 121 ||
|
|
binary.LittleEndian.Uint32(itemUpdate.Body[5:9]) != 3227 ||
|
|
binary.LittleEndian.Uint32(itemUpdate.Body[9:13]) != 1 {
|
|
t.Fatalf("pickup item update header=%+v body=%x", itemUpdate.Header, itemUpdate.Body)
|
|
}
|
|
inventory, found, err := repositories.Inventory.Load(context.Background(), "99")
|
|
if err != nil || !found {
|
|
t.Fatalf("inventory found=%t err=%v", found, err)
|
|
}
|
|
stack := inventory.Slots["0:121"]
|
|
if stack.ItemID != 3227 || stack.Count != 1 || stack.Extra["pvf_path"] != "stackable/material/test_drop.stk" ||
|
|
stack.Extra["stack_limit"] != "999" || len(stack.RawEntry) != currentItemListEntryWireSize {
|
|
t.Fatalf("persisted stack=%+v", stack)
|
|
}
|
|
reloadBody, reloadSource, reloadCount, reloadOK := service.buildCurrentItemListBodyForSession(
|
|
context.Background(),
|
|
session,
|
|
dnfrepo.MainInventoryListType,
|
|
)
|
|
if !reloadOK || reloadSource != "inventory" || reloadCount != 1 ||
|
|
len(reloadBody) != 5+currentItemListEntryWireSize ||
|
|
reloadBody[0] != dnfrepo.MainInventoryListType ||
|
|
binary.LittleEndian.Uint16(reloadBody[3:5]) != 1 {
|
|
t.Fatalf(
|
|
"persisted pickup reload ok=%t source=%q count=%d body=%x",
|
|
reloadOK,
|
|
reloadSource,
|
|
reloadCount,
|
|
reloadBody,
|
|
)
|
|
}
|
|
reloadedEntry := reloadBody[5:]
|
|
if binary.LittleEndian.Uint16(reloadedEntry[0:2]) != 121 ||
|
|
binary.LittleEndian.Uint32(reloadedEntry[2:6]) != 3227 ||
|
|
binary.LittleEndian.Uint32(reloadedEntry[6:10]) != 1 {
|
|
t.Fatalf("persisted pickup reloaded entry=%x", reloadedEntry)
|
|
}
|
|
drop := runtime.DropOwner.byObjectKey[firstDropObjectKey]
|
|
if drop.Status != runtimeDungeonDropConsumed || drop.DestinationSlot != 121 || len(drop.PickupResponseBody) != currentDungeonPickupResultSize {
|
|
t.Fatalf("consumed drop=%+v", drop)
|
|
}
|
|
|
|
connection.write.Reset()
|
|
if err := service.handleCurrentDungeonPickup(session, pickupBody); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
replayPacket, replayRest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
replayUpdate, replayTrailing := splitGameServerUpperPacket(t, replayRest)
|
|
if !bytes.Equal(replayPacket.Body, wantPickup) || len(replayTrailing) != 0 ||
|
|
!bytes.Equal(replayUpdate.Body, itemUpdate.Body) {
|
|
t.Fatalf("replay body=%x update=%x trailing=%x", replayPacket.Body, replayUpdate.Body, replayTrailing)
|
|
}
|
|
inventory, _, err = repositories.Inventory.Load(context.Background(), "99")
|
|
if err != nil || inventory.Slots["0:121"].Count != 1 {
|
|
t.Fatalf("duplicate pickup changed inventory=%+v err=%v", inventory.Slots, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonPickupRoutesCubeFragmentsToSharedCrystalSlots(t *testing.T) {
|
|
for _, test := range []struct {
|
|
itemID uint32
|
|
slot int16
|
|
}{
|
|
{3033, 354},
|
|
{3034, 355},
|
|
{3035, 356},
|
|
{3036, 357},
|
|
{3037, 358},
|
|
{3262, 359},
|
|
} {
|
|
t.Run(strconv.FormatUint(uint64(test.itemID), 10), func(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 1, test.itemID)
|
|
connection := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-cube-pickup-test",
|
|
selectedCharacterID: 99,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
dropObjectKey := seedCurrentDungeonPickupDrop(t, service, runtime, test.itemID, currentSceneActorObjectKey(99))
|
|
|
|
if err := service.handleCurrentDungeonPickup(session, currentDungeonPickupTestBody(dropObjectKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pickup, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
update, trailing := splitGameServerUpperPacket(t, rest)
|
|
if pickup.Header.MsgID != currentDungeonPickupResultOpcode || len(trailing) != 0 ||
|
|
update.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) || update.Body[0] != dnfrepo.MainInventoryListType ||
|
|
binary.LittleEndian.Uint16(update.Body[1:3]) != 1 || binary.LittleEndian.Uint16(update.Body[3:5]) != uint16(test.slot) ||
|
|
binary.LittleEndian.Uint32(update.Body[5:9]) != test.itemID || binary.LittleEndian.Uint32(update.Body[9:13]) != 1 {
|
|
t.Fatalf("cube pickup=%+v update=%x trailing=%x", pickup.Header, update.Body, trailing)
|
|
}
|
|
account, found, err := repositories.AccountInventory.Load(context.Background(), "account-1")
|
|
if err != nil || !found {
|
|
t.Fatalf("account inventory found=%t err=%v", found, err)
|
|
}
|
|
if stack := account.Slots[dnfrepo.AccountSharedInventorySlotKey(test.slot)]; stack.ItemID != int64(test.itemID) || stack.Count != 1 {
|
|
t.Fatalf("shared crystal stack=%+v", stack)
|
|
}
|
|
inventory, found, err := repositories.Inventory.Load(context.Background(), "99")
|
|
if err != nil || !found || len(inventory.Slots) != 0 {
|
|
t.Fatalf("character inventory found=%t slots=%+v err=%v", found, inventory.Slots, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonPickupConsumesOnlyRequestedGroundObject(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 1, 3227)
|
|
connection := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-manual-pickup-test",
|
|
selectedCharacterID: 99,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
firstObjectKey := seedCurrentDungeonPickupDrop(
|
|
t,
|
|
service,
|
|
runtime,
|
|
3227,
|
|
currentSceneActorObjectKey(99),
|
|
)
|
|
first := runtime.DropOwner.byObjectKey[firstObjectKey]
|
|
second := *first
|
|
second.ObjectKey = runtime.NextObjectKey
|
|
second.SceneSlot = first.SceneSlot + 1
|
|
third := *first
|
|
third.ObjectKey = runtime.NextObjectKey + 1
|
|
third.SceneSlot = first.SceneSlot + 2
|
|
if err := runtime.DropOwner.registerBatch([]*runtimeDungeonDrop{&second, &third}, third.SceneSlot+1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.NextObjectKey += 2
|
|
|
|
if err := service.handleCurrentDungeonPickup(session, currentDungeonPickupTestBody(second.ObjectKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if runtime.DropOwner.byObjectKey[second.ObjectKey].Status != runtimeDungeonDropConsumed {
|
|
t.Fatalf("selected drop status=%d, want consumed", runtime.DropOwner.byObjectKey[second.ObjectKey].Status)
|
|
}
|
|
for _, objectKey := range []uint32{firstObjectKey, third.ObjectKey} {
|
|
if status := runtime.DropOwner.byObjectKey[objectKey].Status; status != runtimeDungeonDropAvailable {
|
|
t.Fatalf("unselected drop %d status=%d, want available", objectKey, status)
|
|
}
|
|
}
|
|
inventory, found, err := repositories.Inventory.Load(context.Background(), "99")
|
|
if err != nil || !found {
|
|
t.Fatalf("inventory found=%t err=%v", found, err)
|
|
}
|
|
if stack := inventory.Slots["0:121"]; stack.ItemID != 3227 || stack.Count != 1 {
|
|
t.Fatalf("manual pickup stack=%+v", stack)
|
|
}
|
|
|
|
pickupPacket, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if pickupPacket.Header.MsgID != currentDungeonPickupResultOpcode ||
|
|
binary.LittleEndian.Uint32(pickupPacket.Body[0:4]) != second.ObjectKey {
|
|
t.Fatalf("pickup header=%+v body=%x", pickupPacket.Header, pickupPacket.Body)
|
|
}
|
|
itemUpdate, trailing := splitGameServerUpperPacket(t, rest)
|
|
if itemUpdate.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) || len(trailing) != 0 {
|
|
t.Fatalf("item update header=%+v trailing=%x", itemUpdate.Header, trailing)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonPickupTransactionFailureLeavesDropAvailableAndSendsNothing(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 1, 3227)
|
|
connection := &bufferConn{}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-drop-rollback-test",
|
|
selectedCharacterID: 99,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
dropObjectKey := seedCurrentDungeonPickupDrop(
|
|
t,
|
|
service,
|
|
runtime,
|
|
3227,
|
|
currentSceneActorObjectKey(99),
|
|
)
|
|
wantErr := errors.New("forced pickup save failure")
|
|
failingInventory := pickupFailingInventoryRepository{InventoryRepository: repositories.Inventory, err: wantErr}
|
|
failingGroup := repositories
|
|
failingGroup.CharacterItems = pickupTestCharacterItemUnitOfWork{
|
|
base: repositories.CharacterItems,
|
|
inventory: failingInventory,
|
|
equipment: repositories.Equipment,
|
|
}
|
|
service.repositoryProvider = func() (dnfrepo.Group, bool) { return failingGroup, true }
|
|
|
|
connection.write.Reset()
|
|
if err := service.handleCurrentDungeonPickup(session, currentDungeonPickupTestBody(dropObjectKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if connection.write.Len() != 0 {
|
|
t.Fatalf("failed transaction emitted packet=%x", connection.write.Bytes())
|
|
}
|
|
if drop := runtime.DropOwner.byObjectKey[dropObjectKey]; drop.Status != runtimeDungeonDropAvailable || len(drop.PickupResponseBody) != 0 {
|
|
t.Fatalf("failed transaction consumed drop=%+v", drop)
|
|
}
|
|
inventory, _, err := repositories.Inventory.Load(context.Background(), "99")
|
|
if err != nil || len(inventory.Slots) != 0 {
|
|
t.Fatalf("failed transaction changed inventory=%+v err=%v", inventory.Slots, err)
|
|
}
|
|
}
|
|
|
|
func TestCurrentDungeonPickupRepositoryWriteDoesNotBlockSessionEvents(t *testing.T) {
|
|
service, runtime, repositories := prepareCurrentDungeonDropTest(t, 1, 3227)
|
|
connection := &bufferConn{}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "dungeon-pickup-event-loop-test",
|
|
selectedCharacterID: 99,
|
|
characterGeneration: 1,
|
|
ctx: ctx,
|
|
dungeon: dungeonSessionState{runtime: runtime},
|
|
}
|
|
if err := service.startGameSessionEvents(ctx, session); err != nil {
|
|
cancel()
|
|
t.Fatal(err)
|
|
}
|
|
|
|
releaseWrite := make(chan struct{})
|
|
var releaseWriteOnce sync.Once
|
|
releaseRepositoryWrite := func() {
|
|
releaseWriteOnce.Do(func() { close(releaseWrite) })
|
|
}
|
|
defer func() {
|
|
releaseRepositoryWrite()
|
|
cancel()
|
|
stopCtx, stopCancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer stopCancel()
|
|
_ = service.stopGameSessionEvents(stopCtx, session)
|
|
}()
|
|
writeEntered := make(chan struct{})
|
|
writeDeadline := make(chan time.Duration, 1)
|
|
blockingInventory := &pickupBlockingInventoryRepository{
|
|
InventoryRepository: repositories.Inventory,
|
|
entered: writeEntered,
|
|
release: releaseWrite,
|
|
deadline: writeDeadline,
|
|
}
|
|
blockingGroup := repositories
|
|
blockingGroup.CharacterItems = pickupTestCharacterItemUnitOfWork{
|
|
base: repositories.CharacterItems,
|
|
inventory: blockingInventory,
|
|
equipment: repositories.Equipment,
|
|
}
|
|
service.repositoryProvider = func() (dnfrepo.Group, bool) { return blockingGroup, true }
|
|
dropObjectKey := seedCurrentDungeonPickupDrop(
|
|
t,
|
|
service,
|
|
runtime,
|
|
3227,
|
|
currentSceneActorObjectKey(99),
|
|
)
|
|
|
|
pickupDone := make(chan error, 1)
|
|
go func() {
|
|
pickupDone <- service.callGameSession(ctx, session, "blocked-pickup", func() error {
|
|
return service.handleCurrentDungeonPickup(session, currentDungeonPickupTestBody(dropObjectKey))
|
|
})
|
|
}()
|
|
select {
|
|
case <-writeEntered:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("pickup repository write did not start")
|
|
}
|
|
select {
|
|
case remaining := <-writeDeadline:
|
|
if remaining <= createWriteTimeout || remaining > currentDungeonPickupWriteTimeout {
|
|
t.Fatalf("pickup repository deadline remaining=%s want=(%s,%s]", remaining, createWriteTimeout, currentDungeonPickupWriteTimeout)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("pickup repository write did not observe a context deadline")
|
|
}
|
|
if err := service.callGameSession(ctx, session, "pending-pickup-replay", func() error {
|
|
return service.handleCurrentDungeonPickup(session, currentDungeonPickupTestBody(dropObjectKey))
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
session.dungeon.mu.Lock()
|
|
pendingDrop := runtime.DropOwner.byObjectKey[dropObjectKey]
|
|
pendingStatus := pendingDrop.Status
|
|
pendingAttempt := pendingDrop.PickupAttempt
|
|
session.dungeon.mu.Unlock()
|
|
if pendingStatus != runtimeDungeonDropPending || pendingAttempt != 1 {
|
|
t.Fatalf("pending replay changed drop status=%d attempt=%d", pendingStatus, pendingAttempt)
|
|
}
|
|
if connection.write.Len() != 0 {
|
|
t.Fatalf("pending replay emitted a premature packet=%x", connection.write.Bytes())
|
|
}
|
|
|
|
nextEventRan := make(chan struct{})
|
|
go func() {
|
|
_ = service.callGameSession(ctx, session, "next-dungeon-event", func() error {
|
|
close(nextEventRan)
|
|
return nil
|
|
})
|
|
}()
|
|
select {
|
|
case <-nextEventRan:
|
|
case <-time.After(250 * time.Millisecond):
|
|
t.Fatal("a blocked pickup repository write stalled the complete session event loop")
|
|
}
|
|
select {
|
|
case err := <-pickupDone:
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case <-time.After(250 * time.Millisecond):
|
|
t.Fatal("pickup handler remained on the session event loop while persistence was pending")
|
|
}
|
|
|
|
releaseRepositoryWrite()
|
|
var wire []byte
|
|
completionDeadline := time.Now().Add(2 * time.Second)
|
|
for {
|
|
var status runtimeDungeonDropStatus
|
|
if err := service.callGameSession(ctx, session, "pickup-completion-barrier", func() error {
|
|
session.dungeon.mu.Lock()
|
|
status = runtime.DropOwner.byObjectKey[dropObjectKey].Status
|
|
session.dungeon.mu.Unlock()
|
|
if status == runtimeDungeonDropConsumed {
|
|
wire = append([]byte(nil), connection.write.Bytes()...)
|
|
}
|
|
return nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status == runtimeDungeonDropConsumed {
|
|
break
|
|
}
|
|
if time.Now().After(completionDeadline) {
|
|
t.Fatalf("pickup persistence did not complete; drop status=%d", status)
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if calls := blockingInventory.calls.Load(); calls != 1 {
|
|
t.Fatalf("pending replay performed %d repository writes, want 1", calls)
|
|
}
|
|
pickupPacket, rest := splitGameServerUpperPacket(t, wire)
|
|
itemUpdate, trailing := splitGameServerUpperPacket(t, rest)
|
|
wantPickup, err := buildCurrentDungeonNormalPickupResultBody(
|
|
dropObjectKey,
|
|
currentSceneActorObjectKey(99),
|
|
121,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pickupPacket.Header.MsgID != currentDungeonPickupResultOpcode ||
|
|
!bytes.Equal(pickupPacket.Body, wantPickup) ||
|
|
itemUpdate.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
|
len(trailing) != 0 {
|
|
t.Fatalf(
|
|
"async pickup packet=%+v body=%x update=%+v trailing=%x",
|
|
pickupPacket.Header,
|
|
pickupPacket.Body,
|
|
itemUpdate.Header,
|
|
trailing,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestRuntimeDungeonDropOwnerRejectsStaleRoomAndActor(t *testing.T) {
|
|
owner := newRuntimeDungeonDropOwner()
|
|
drop := &runtimeDungeonDrop{
|
|
ObjectKey: 700,
|
|
SceneSlot: 1,
|
|
OwnerActorObjectKey: 401,
|
|
Room: runtimeDungeonRoomKey{X: 1, Y: 2, MapID: 3},
|
|
Status: runtimeDungeonDropAvailable,
|
|
}
|
|
if err := owner.registerBatch([]*runtimeDungeonDrop{drop}, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := owner.owned(700, runtimeDungeonRoomKey{X: 9, Y: 2, MapID: 3}, 401); !errors.Is(err, errDungeonDropRoomMismatch) {
|
|
t.Fatalf("stale-room error=%v", err)
|
|
}
|
|
if _, err := owner.owned(700, drop.Room, 402); !errors.Is(err, errDungeonDropActorMismatch) {
|
|
t.Fatalf("actor error=%v", err)
|
|
}
|
|
}
|
|
|
|
func seedCurrentDungeonPickupDrop(
|
|
t *testing.T,
|
|
service *Service,
|
|
runtime *runtimeDungeonState,
|
|
itemID uint32,
|
|
ownerActorObjectKey uint16,
|
|
) uint32 {
|
|
t.Helper()
|
|
catalog, err := service.dungeonMonsterCatalog()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
drops, err := catalog.DropCatalog()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
definition, err := drops.ResolveItem(itemID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
scene, ok := runtime.Session.Scene()
|
|
if !ok {
|
|
t.Fatal("missing dungeon scene")
|
|
}
|
|
objectKey := runtime.NextObjectKey
|
|
if objectKey == 0 {
|
|
t.Fatal("missing next object key")
|
|
}
|
|
drop := &runtimeDungeonDrop{
|
|
ObjectKey: objectKey,
|
|
SceneSlot: 1,
|
|
Item: definition,
|
|
Amount: 1,
|
|
OwnerActorObjectKey: ownerActorObjectKey,
|
|
Room: runtimeDungeonRoomKeyFromScene(scene),
|
|
Status: runtimeDungeonDropAvailable,
|
|
}
|
|
owner := newRuntimeDungeonDropOwner()
|
|
if err := owner.registerBatch([]*runtimeDungeonDrop{drop}, 2); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runtime.DropOwner = owner
|
|
runtime.NextObjectKey++
|
|
return objectKey
|
|
}
|
|
|
|
func prepareCurrentDungeonDropTest(
|
|
t *testing.T,
|
|
fixedDropCount int,
|
|
itemID uint32,
|
|
) (*Service, *runtimeDungeonState, dnfrepo.Group) {
|
|
t.Helper()
|
|
source := bridgeDungeonPVF(false)
|
|
source["map/dungeon/test/start.map"] = "[map name]\n`start`\n" +
|
|
"[dungeon]\n700\n" +
|
|
"[type]\n`[start]`\n" +
|
|
"[monster]\n3001 10 0 100 200 0 0 " + strconv.Itoa(fixedDropCount) + " `[fixed]` `[normal]`\n"
|
|
source["monster/test.gob"] += "[item]\n" + strconv.FormatUint(uint64(itemID), 10) + " 10\n[/item]\n"
|
|
source[dungeonDropStackableList] = strconv.FormatUint(uint64(itemID), 10) + " `material/test_drop.stk`\n"
|
|
source[dungeonDropEquipmentList] = "9001 `weapon/test_drop.equ`\n"
|
|
source["stackable/material/test_drop.stk"] = "[name]\n`Test Drop`\n[stackable type]\n`[material]`\n[stack limit]\n999\n[attach type]\n`[free]`\n"
|
|
source["equipment/weapon/test_drop.equ"] = "[name]\n`Test Equipment`\n[durability]\n50\n[attach type]\n`[trade]`\n"
|
|
table, resolver, monsters := loadBridgeDungeonStaticData(t, source)
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
|
CharacterID: "99",
|
|
AccountID: "account-1",
|
|
Level: 20,
|
|
Stats: map[string]int64{"fatigue": 100},
|
|
}); 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)
|
|
}
|
|
service := &Service{
|
|
options: options{accountID: "account-1", gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
worldMapTable: table,
|
|
worldMapResolver: resolver,
|
|
dungeonMonsterTable: monsters,
|
|
dungeonChoice: func(int) (int, error) { return 0, nil },
|
|
dungeonSeed: func() (uint32, error) { return 0x12345678, nil },
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{selectedCharacterID: 99}
|
|
runtime, _, err := service.prepareDungeonRuntime(
|
|
context.Background(),
|
|
session,
|
|
dungeoncmd.SelectDungeonRequest{DungeonID: 700, Difficulty: 1},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := runtime.Room.AnnounceAllActors(runtime.Session); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return service, runtime, repositories
|
|
}
|
|
|
|
func currentDungeonPickupTestBody(dropObjectKey uint32) []byte {
|
|
body := make([]byte, dungeoncmd.GetItemRequestSize)
|
|
binary.LittleEndian.PutUint32(body[0:4], dropObjectKey)
|
|
body[5] = 7
|
|
for index, value := range []uint16{100, 200, 300, 400, 500, 600, 700} {
|
|
binary.LittleEndian.PutUint16(body[6+index*2:], value)
|
|
}
|
|
return body
|
|
}
|
|
|
|
func currentDungeonGoldDropTestRuleText() string {
|
|
return `[drop prob count]
|
|
1
|
|
[drop prob]
|
|
1 200 10000 0 0 0 0
|
|
[basis of rarity dicision]
|
|
700000 964900 990000 1000000 1000001 1000002 1000003
|
|
545500 899500 999500 1000000 1000001 1000002 1000003
|
|
500000 944900 999900 1000000 1000001 1000002 1000003
|
|
700000 944900 999000 1000000 1000001 1000002 1000003
|
|
[party member drop bonusrate]
|
|
1 1 1 1 1 1.3 1.6 2 1 1.2 1.4 1.6 1 1.2 1.4 1.6 1 1.2 1.4 1.6
|
|
[dungeon difficulty drop bonusrate]
|
|
1.00 1.00 1.00 1.40 1.10 1.00 1.40 1.60 2.20 2.00 1.00 1.40 1.60 2.20 2.00 1.00 1.40 1.60 2.20 2.00 1.00 1.40 1.60 2.20 2.00
|
|
[monster type drop bonusrate]
|
|
1.00 2.00 3.00 6.60 1.00 2.00 3.00 4.00 0.20 2.00 7.00 16.80 1.00 2.00 3.00 4.00 1.00 2.00 3.00 4.00
|
|
[item drop ref table]
|
|
1 0 3
|
|
[first boss/named mob hunting]
|
|
1 1
|
|
[condition rate]
|
|
95 5 93 7 90 10 85 15
|
|
[gold quantity]
|
|
1 2 4 6
|
|
[gold volume]
|
|
100 110
|
|
[item drop rarity control]
|
|
2 13 17 0 0 3 13 17 0 0
|
|
[/item drop rarity control]
|
|
`
|
|
}
|
|
|
|
type pickupFailingInventoryRepository struct {
|
|
dnfrepo.InventoryRepository
|
|
err error
|
|
}
|
|
|
|
type pickupBlockingInventoryRepository struct {
|
|
dnfrepo.InventoryRepository
|
|
once sync.Once
|
|
entered chan struct{}
|
|
release <-chan struct{}
|
|
deadline chan<- time.Duration
|
|
calls atomic.Int32
|
|
}
|
|
|
|
func (repository *pickupBlockingInventoryRepository) Save(
|
|
ctx context.Context,
|
|
record dnfrepo.InventoryRecord,
|
|
) error {
|
|
repository.calls.Add(1)
|
|
if deadline, ok := ctx.Deadline(); ok && repository.deadline != nil {
|
|
select {
|
|
case repository.deadline <- time.Until(deadline):
|
|
default:
|
|
}
|
|
}
|
|
repository.once.Do(func() { close(repository.entered) })
|
|
select {
|
|
case <-repository.release:
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
return repository.InventoryRepository.Save(ctx, record)
|
|
}
|
|
|
|
func (repository pickupFailingInventoryRepository) Save(context.Context, dnfrepo.InventoryRecord) error {
|
|
return repository.err
|
|
}
|
|
|
|
type pickupTestCharacterItemUnitOfWork struct {
|
|
base dnfrepo.CharacterItemUnitOfWork
|
|
inventory dnfrepo.InventoryRepository
|
|
equipment dnfrepo.EquipmentRepository
|
|
}
|
|
|
|
func (unit pickupTestCharacterItemUnitOfWork) WithinCharacterItems(
|
|
ctx context.Context,
|
|
characterID string,
|
|
apply func(dnfrepo.InventoryRepository, dnfrepo.EquipmentRepository) error,
|
|
) error {
|
|
return unit.base.WithinCharacterItems(ctx, characterID, func(
|
|
_ dnfrepo.InventoryRepository,
|
|
_ dnfrepo.EquipmentRepository,
|
|
) error {
|
|
return apply(unit.inventory, unit.equipment)
|
|
})
|
|
}
|