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,440 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
// 塔罗牌夹具:512 是 `[stackable type]` = `[monster tarot card]` 的消耗品,
|
||||
// `[lottery use cost]` 100000 金币,`[int data]` 是 (item, weight, count) 三元组。
|
||||
//
|
||||
// 中奖项刻意选 600/610(夹具里的 `[material]` 堆叠品):这条用例只关心 op27 通道与
|
||||
// 金币扣款,堆叠品能把这两个断言做得最干净。**混合表(装备行)由另外两条用例覆盖**:
|
||||
//
|
||||
// - tarot_lottery_mixed_table_test.go:内存夹具里第一行就是装备,钉住"抽到装备也发得出去";
|
||||
// - TestRealTarotLotteryCardsGrantEveryRow:真档四张卡 × 每一行的端到端验收。
|
||||
func currentTarotLotteryTestSource() dungeonDropCatalogTestSource {
|
||||
source := currentBoosterTestSource()
|
||||
source["stackable/stackable.lst"] += "512 `cash/tarot_card.stk`\n"
|
||||
source["stackable/cash/tarot_card.stk"] = "[stackable type]\n`[monster tarot card]` 19\n" +
|
||||
"[stack limit]\n100\n" +
|
||||
"[lottery use cost]\n100000\n" +
|
||||
"[int data]\n600 10000 3 610 10000 1\n[/int data]\n"
|
||||
return source
|
||||
}
|
||||
|
||||
func mustCurrentTarotLotteryTestCatalog(t *testing.T) *pvfDungeonDropCatalog {
|
||||
t.Helper()
|
||||
catalog, err := newPVFDungeonDropCatalog(currentTarotLotteryTestSource())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
// tarotLotteryTestFixture 起一个只有一个角色、背包里只有一张塔罗牌的服务。
|
||||
// 槽位 70 落在 `[monster tarot card]` 自己的 PVF 页(dungeonDropStackableSlotRange
|
||||
// 的 default 分支 65..120),中奖项 600 是 `[material]`,落在 121..176,两者不打架。
|
||||
const (
|
||||
tarotLotteryTestCardItemID = uint32(512)
|
||||
tarotLotteryTestCardSlot = int16(70)
|
||||
tarotLotteryTestGold = int64(200000)
|
||||
tarotLotteryTestGoldCost = int64(100000)
|
||||
tarotLotteryTestRewardStack = uint32(3)
|
||||
)
|
||||
|
||||
func newTarotLotteryTestFixture(
|
||||
t *testing.T,
|
||||
cardCount int64,
|
||||
gold int64,
|
||||
) (*Service, *gameSession, *bufferConn, context.Context, dnfrepo.Group) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
catalog := mustCurrentTarotLotteryTestCatalog(t)
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "19", AccountID: "account-1", Level: 90,
|
||||
Stats: map[string]int64{"gold": gold},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "19",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: int64(tarotLotteryTestCardItemID), Count: cardCount},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := currentBoosterTestService(catalog, repositories)
|
||||
// 抽奖的随机源:0 = 落在这个权重表的第一个三元组上(600)。
|
||||
service.dungeonChoice = func(int) (int, error) { return 0, nil }
|
||||
session := &gameSession{conn: connection, connID: "tarot-lottery", selectedCharacterID: 19}
|
||||
return service, session, connection, ctx, repositories
|
||||
}
|
||||
|
||||
func openTarotLotteryCard(t *testing.T, service *Service, session *gameSession, slot int16) {
|
||||
t.Helper()
|
||||
if err := service.handleGameCommand(
|
||||
session,
|
||||
byte(dnfenum.GameCmdCommand),
|
||||
uint16(dnfenum.CmdPacketUseLotteryItem),
|
||||
[]byte{0, 0, byte(slot), byte(slot >> 8)},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 主用例:客户端发 class-1/op27(实机 2026-09-30T11:52:47Z 就是这个形状),服务端
|
||||
// 必须把它当塔罗牌抽奖而不是"非随机罐"拒绝 —— 也就是本轮的现场问题本身。
|
||||
func TestCurrentTarotLotteryItemAnswersOp27AndChargesTheDeclaredGold(t *testing.T) {
|
||||
service, session, connection, ctx, repositories := newTarotLotteryTestFixture(t, 2, tarotLotteryTestGold)
|
||||
|
||||
openTarotLotteryCard(t, service, session, tarotLotteryTestCardSlot)
|
||||
|
||||
result, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if result.Header.MsgID != uint16(dnfenum.CmdPacketUseLotteryItem) {
|
||||
t.Fatalf("第一个包不是 op27: header=%+v body=%x", result.Header, result.Body)
|
||||
}
|
||||
if len(result.Body) != currentLotteryNativeResultBodySize || result.Body[0] != 1 {
|
||||
t.Fatalf("op27 结果包形状不对: len=%d want=%d body=%x", len(result.Body), currentLotteryNativeResultBodySize, result.Body)
|
||||
}
|
||||
if slot := int16(uint16(result.Body[1]) | uint16(result.Body[2])<<8); slot != tarotLotteryTestCardSlot {
|
||||
t.Fatalf("op27 结果包源槽=%d want %d", slot, tarotLotteryTestCardSlot)
|
||||
}
|
||||
|
||||
// 花钱就必须把权威余额推给客户端;形状与罐子开出金币时那一个钱包包一致
|
||||
// (lottery_item.go:115-124)。
|
||||
wallet, trailing := splitGameServerUpperPacket(t, rest)
|
||||
if wallet.Header.Classification != 0 ||
|
||||
wallet.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Fatalf("第二个包不是钱包同步: header=%+v body=%x", wallet.Header, wallet.Body)
|
||||
}
|
||||
wantGold := tarotLotteryTestGold - tarotLotteryTestGoldCost
|
||||
if wantBody := buildCurrentGoldStateBody(wantGold); string(wallet.Body) != string(wantBody) {
|
||||
t.Fatalf("钱包包=%x want=%x", wallet.Body, wantBody)
|
||||
}
|
||||
// op27 之后**不得**出现道具表包:那会替换客户端动画仍握着的道具对象
|
||||
// (lottery_item.go:107-114 的既有教训)。钱包包是唯一允许的后续包。
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("op27 之后多了包: %x", trailing)
|
||||
}
|
||||
|
||||
character, found, err := repositories.Character.Load(ctx, "19")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character: %v found=%t", err, found)
|
||||
}
|
||||
if got := character.Stats["gold"]; got != wantGold {
|
||||
t.Fatalf("金币=%d want %d([lottery use cost] 必须真的扣掉)", got, wantGold)
|
||||
}
|
||||
inventory, found, err := repositories.Inventory.Load(ctx, "19")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory: %v found=%t", err, found)
|
||||
}
|
||||
if stack := inventory.Slots["0:70"]; stack.ItemID != int64(tarotLotteryTestCardItemID) || stack.Count != 1 {
|
||||
t.Fatalf("源塔罗牌槽=%+v want item=%d count=1", stack, tarotLotteryTestCardItemID)
|
||||
}
|
||||
// 中奖项 600 必须真的进背包(3 个),且落点用 Owner 写回的真实槽位。
|
||||
granted := int64(0)
|
||||
for key, stack := range inventory.Slots {
|
||||
if key == "0:70" || stack.ItemID != 600 {
|
||||
continue
|
||||
}
|
||||
granted += stack.Count
|
||||
}
|
||||
if granted != int64(tarotLotteryTestRewardStack) {
|
||||
t.Fatalf("中奖项 600 数量=%d want %d(背包=%+v)", granted, tarotLotteryTestRewardStack, inventory.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
// 金币不足:整次使用必须在动任何东西之前中止 —— 源卡不消耗、金币不变、回 op27 失败包。
|
||||
func TestCurrentTarotLotteryItemFailsClosedWhenGoldIsShort(t *testing.T) {
|
||||
service, session, connection, ctx, repositories := newTarotLotteryTestFixture(t, 2, tarotLotteryTestGoldCost-1)
|
||||
|
||||
openTarotLotteryCard(t, service, session, tarotLotteryTestCardSlot)
|
||||
|
||||
rejected, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if rejected.Header.MsgID != uint16(dnfenum.CmdPacketUseLotteryItem) || rejected.Body[0] != 0 {
|
||||
t.Fatalf("应回 op27 失败包: header=%+v body=%x", rejected.Header, rejected.Body)
|
||||
}
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("被拒的使用不该产生后续包: %x", trailing)
|
||||
}
|
||||
character, _, _ := repositories.Character.Load(ctx, "19")
|
||||
if got := character.Stats["gold"]; got != tarotLotteryTestGoldCost-1 {
|
||||
t.Fatalf("被拒的使用改了金币: %d", got)
|
||||
}
|
||||
inventory, _, _ := repositories.Inventory.Load(ctx, "19")
|
||||
if stack := inventory.Slots["0:70"]; stack.Count != 2 {
|
||||
t.Fatalf("被拒的使用消耗了源卡: %+v", inventory.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
// 分派判据只认 `[monster tarot card]`:罐子(`[upgradable legacy]`)必须原样落回既有
|
||||
// op27 路径。这是"修 A 不坏 B"的那条断言 —— 一旦有人把判据放宽,这里先红。
|
||||
func TestCurrentTarotLotteryDispatchLeavesEveryOtherOp27ItemAlone(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
catalog := mustCurrentTarotLotteryTestCatalog(t)
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "19", AccountID: "account-1", Level: 90,
|
||||
Stats: map[string]int64{"gold": 0},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
jar := int64(510) // `[upgradable legacy]`:既有的 op27 抽奖罐
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "19",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: int64(tarotLotteryTestCardItemID), Count: 1},
|
||||
"0:71": {ItemID: jar, Count: 1},
|
||||
"0:72": {ItemID: 600, Count: 1}, // 普通材料:既不是塔罗牌也不是罐子
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service := currentBoosterTestService(catalog, repositories)
|
||||
session := &gameSession{conn: &bufferConn{}, connID: "tarot-dispatch", selectedCharacterID: 19}
|
||||
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
slot int16
|
||||
handled bool
|
||||
}{
|
||||
{"tarot card is taken over", 70, true},
|
||||
{"legacy jar keeps the booster route", 71, false},
|
||||
{"ordinary material keeps the booster route", 72, false},
|
||||
{"empty slot keeps the booster route", 90, false},
|
||||
} {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
handled, err := service.tryHandleCurrentTarotLotteryItem(ctx, session, catalog, false, test.slot)
|
||||
if err != nil {
|
||||
t.Fatalf("slot=%d err=%v", test.slot, err)
|
||||
}
|
||||
if handled != test.handled {
|
||||
t.Fatalf("slot=%d handled=%t want %t", test.slot, handled, test.handled)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 真档验收:真塔罗牌的 `[stackable type]` 必须命中分派判据,并且中奖项里的
|
||||
// **每一行**都必须真的发得出去(不再是"装备行干净拒绝")。
|
||||
//
|
||||
// 这条用例的历史:上一轮发现真档 `[int data]` 里混着 9 位数的装备 id,而 inventory 的
|
||||
// grantRandomRewardItem 只接受 kind=stackable,于是"落在装备行上的抽取"发不出奖;
|
||||
// 当时把它钉成"干净 fail closed"。现场 2026-10-03 服主报「塔罗牌用不了」之后追下去,
|
||||
// 发现那不只是边角:`[int data]` 里装备行按权重占了大半,而且服务端**从来没真正掷过
|
||||
// 骰子**(roll 恒为 0 → 永远第一行),所以这些卡 100% 落在装备行上、100% 被拒。
|
||||
//
|
||||
// 现在两件事都修了,于是本用例改成双向断言:
|
||||
//
|
||||
// 1. 每一行、每一张卡都必须回 op27 成功,并且源卡消耗 + 金币扣掉;
|
||||
// 2. **装备行必须是一行真正的装备**:item_kind=equipment、数量 1、0x77 行、
|
||||
// PVF 的 [durability] 落在 +0x0B、[sealing] 落在 +0x0D,且带一个合法品级种子。
|
||||
//
|
||||
// 任何一行掉回"成功位=0"都会被这条用例抓住。
|
||||
func TestRealTarotLotteryCardsGrantEveryRow(t *testing.T) {
|
||||
archive, err := platformpvf.Open(realLukeRewardPVFPath())
|
||||
if err != nil {
|
||||
t.Skipf("runtime Script.pvf unavailable: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
catalog, err := newPVFDungeonDropCatalog(archive)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 10159717 就是服主那只(暗之偶像阿努比斯 塔罗牌,stackable/10159001/10159717.stk)。
|
||||
for _, cardID := range []uint32{10159697, 10159698, 10159700, 10159717} {
|
||||
cardID := cardID
|
||||
t.Run("card", func(t *testing.T) {
|
||||
definition, err := catalog.ResolveItem(cardID)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve card %d: %v", cardID, err)
|
||||
}
|
||||
if got := normalizeDungeonDropStackableType(definition.StackableType); got != currentTarotLotteryStackableType {
|
||||
t.Fatalf("card %d stackable_type=%q want %q (path=%s)",
|
||||
cardID, got, currentTarotLotteryStackableType, definition.PVFPath)
|
||||
}
|
||||
if len(definition.LotteryEntries) == 0 {
|
||||
t.Fatalf("card %d has no [int data] lottery rows (path=%s)", cardID, definition.PVFPath)
|
||||
}
|
||||
|
||||
grantedRows, equipmentRows := 0, 0
|
||||
// 每一行的起始 roll:累计权重边界。
|
||||
roll := int64(0)
|
||||
for index, entry := range definition.LotteryEntries {
|
||||
rowRoll := roll
|
||||
roll += entry.Weight
|
||||
reward, resolveErr := resolveCurrentMagicBoxRewardItem(catalog, catalog.source, entry.ItemID)
|
||||
if resolveErr != nil {
|
||||
t.Errorf("card %d row %d item=%d: resolve reward: %v", cardID, index, entry.ItemID, resolveErr)
|
||||
continue
|
||||
}
|
||||
// 端到端:把 roll 钉在这一行上,走完整的 op27 使用路径。
|
||||
service, session, connection, repos := newRealTarotLotteryFixture(t, ctx, catalog, cardID, rowRoll)
|
||||
openTarotLotteryCard(t, service, session, tarotLotteryTestCardSlot)
|
||||
result, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if result.Header.MsgID != uint16(dnfenum.CmdPacketUseLotteryItem) || len(result.Body) == 0 {
|
||||
t.Fatalf("card %d row %d item=%d: 第一个回包不是 op27 header=%+v body=%x",
|
||||
cardID, index, entry.ItemID, result.Header, result.Body)
|
||||
}
|
||||
character, _, _ := repos.Character.Load(ctx, "19")
|
||||
inventory, _, _ := repos.Inventory.Load(ctx, "19")
|
||||
source := inventory.Slots["0:70"]
|
||||
goldAfter := character.Stats["gold"]
|
||||
|
||||
if result.Body[0] != 1 {
|
||||
t.Errorf("card %d row %d item=%d kind=%q: 被拒了 body=%x(每一行都必须发得出去)",
|
||||
cardID, index, entry.ItemID, reward.Kind, result.Body)
|
||||
continue
|
||||
}
|
||||
grantedRows++
|
||||
if len(result.Body) != currentLotteryNativeResultBodySize {
|
||||
t.Errorf("card %d row %d item=%d: op27 结果包长=%d want=%d body=%x",
|
||||
cardID, index, entry.ItemID, len(result.Body), currentLotteryNativeResultBodySize, result.Body)
|
||||
}
|
||||
// 花钱就必须推权威余额;钱包包是 op27 之后**唯一**允许的后续包
|
||||
// (不能有 op14 道具行/op13 整表:那会替换客户端动画握着的对象)。
|
||||
wallet, rest := splitGameServerUpperPacket(t, trailing)
|
||||
if wallet.Header.Classification != 0 ||
|
||||
wallet.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) {
|
||||
t.Errorf("card %d row %d item=%d: op27 之后的包不是钱包同步 header=%+v body=%x",
|
||||
cardID, index, entry.ItemID, wallet.Header, wallet.Body)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Errorf("card %d row %d item=%d: op27+钱包之后还多了包 %x",
|
||||
cardID, index, entry.ItemID, rest)
|
||||
}
|
||||
if goldAfter != 0 {
|
||||
t.Errorf("card %d row %d item=%d: 成功后金币=%d want 0", cardID, index, entry.ItemID, goldAfter)
|
||||
}
|
||||
if source.ItemID != 0 || source.Count != 0 {
|
||||
t.Errorf("card %d row %d item=%d: 成功后源卡还在 %+v", cardID, index, entry.ItemID, source)
|
||||
}
|
||||
|
||||
row := result.Body[3 : 3+currentItemListEntryWireSize]
|
||||
if got := binary.LittleEndian.Uint32(row[2:6]); got != uint32(entry.ItemID) {
|
||||
t.Errorf("card %d row %d: op27 行里的道具 id=%d want %d", cardID, index, got, entry.ItemID)
|
||||
}
|
||||
|
||||
if strings.EqualFold(reward.Kind, "equipment") {
|
||||
equipmentRows++
|
||||
grantedKey, granted, ok := findRealTarotLotteryGrantedEquipment(inventory, int64(entry.ItemID))
|
||||
if !ok {
|
||||
// 源槽 70 是卡本身,奖励装备落在 9..64,不可能被卡挡住。
|
||||
t.Errorf("card %d row %d item=%d: 背包里找不到发出去的装备(slots=%v)",
|
||||
cardID, index, entry.ItemID, realTarotLotterySlotSummary(inventory))
|
||||
continue
|
||||
}
|
||||
if granted.Count != 1 {
|
||||
t.Errorf("card %d row %d item=%d: 装备数量=%d want 1(装备一格一件,不叠加)",
|
||||
cardID, index, entry.ItemID, granted.Count)
|
||||
}
|
||||
if got := granted.Extra["item_kind"]; got != "equipment" {
|
||||
t.Errorf("card %d row %d item=%d: item_kind=%q want equipment", cardID, index, entry.ItemID, got)
|
||||
}
|
||||
if len(granted.RawEntry) != currentItemListEntryWireSize {
|
||||
t.Errorf("card %d row %d item=%d: RawEntry 长=%d want %d",
|
||||
cardID, index, entry.ItemID, len(granted.RawEntry), currentItemListEntryWireSize)
|
||||
}
|
||||
if reward.Seal && granted.Extra["seal_flag"] != "1" {
|
||||
t.Errorf("card %d row %d item=%d: [sealing] 没写进 seal_flag(extra=%v)",
|
||||
cardID, index, entry.ItemID, granted.Extra)
|
||||
}
|
||||
if durability := int64(binary.LittleEndian.Uint16(row[0x0B:0x0D])); durability != int64(reward.Durability) {
|
||||
t.Errorf("card %d row %d item=%d: op27 行耐久=%d want %d(PVF [durability])",
|
||||
cardID, index, entry.ItemID, durability, reward.Durability)
|
||||
}
|
||||
if seed, parseErr := strconv.ParseUint(granted.Extra["quality_seed"], 10, 32); parseErr != nil || seed == 0 {
|
||||
t.Errorf("card %d row %d item=%d: 品级种子=%q 不合法(客户端会画成最下级)",
|
||||
cardID, index, entry.ItemID, granted.Extra["quality_seed"])
|
||||
}
|
||||
t.Logf("card=%d row=%d item=%d weight=%d kind=equipment durability=%d seal=%t -> GRANTED slot=%s",
|
||||
cardID, index, entry.ItemID, entry.Weight, reward.Durability, reward.Seal, grantedKey)
|
||||
} else {
|
||||
t.Logf("card=%d row=%d item=%d weight=%d count=%d kind=%q -> GRANTED",
|
||||
cardID, index, entry.ItemID, entry.Weight, entry.Count, reward.Kind)
|
||||
}
|
||||
}
|
||||
t.Logf("card=%d rows=%d granted=%d equipment=%d",
|
||||
cardID, len(definition.LotteryEntries), grantedRows, equipmentRows)
|
||||
if grantedRows != len(definition.LotteryEntries) {
|
||||
t.Errorf("card %d: 只发出去 %d/%d 行", cardID, grantedRows, len(definition.LotteryEntries))
|
||||
}
|
||||
if equipmentRows == 0 {
|
||||
t.Errorf("card %d: 一行装备都没抽到 —— 混合表用例失去意义", cardID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// findRealTarotLotteryGrantedEquipment 在角色背包里找那件发出去的装备,返回它的行键。
|
||||
func findRealTarotLotteryGrantedEquipment(inventory dnfrepo.InventoryRecord, itemID int64) (string, dnfrepo.ItemStack, bool) {
|
||||
keys := make([]string, 0, len(inventory.Slots))
|
||||
for key, stack := range inventory.Slots {
|
||||
if stack.ItemID == itemID && stack.Count > 0 {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return "", dnfrepo.ItemStack{}, false
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys[0], inventory.Slots[keys[0]], true
|
||||
}
|
||||
|
||||
func realTarotLotterySlotSummary(inventory dnfrepo.InventoryRecord) []string {
|
||||
keys := make([]string, 0, len(inventory.Slots))
|
||||
for key, stack := range inventory.Slots {
|
||||
keys = append(keys, fmt.Sprintf("%s=%d x%d", key, stack.ItemID, stack.Count))
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// newRealTarotLotteryFixture 与内存夹具同形,但用真档目录与真塔罗牌 id,并把随机源
|
||||
// 钉在指定 roll 上。注意真档卡的 `[lottery use cost]` 是 100000,所以起始金币给 100000,
|
||||
// 成功之后余额必须是 0。
|
||||
func newRealTarotLotteryFixture(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
catalog *pvfDungeonDropCatalog,
|
||||
cardID uint32,
|
||||
roll int64,
|
||||
) (*Service, *gameSession, *bufferConn, dnfrepo.Group) {
|
||||
t.Helper()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "19", AccountID: "account-1", Level: 90,
|
||||
Stats: map[string]int64{"gold": 100000},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "19",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:70": {ItemID: int64(cardID), Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := currentBoosterTestService(catalog, repositories)
|
||||
service.dungeonChoice = func(int) (int, error) { return int(roll), nil }
|
||||
session := &gameSession{conn: connection, connID: "tarot-real", selectedCharacterID: 19}
|
||||
return service, session, connection, repositories
|
||||
}
|
||||
Reference in new issue
Block a user