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

228 lines
9.6 KiB
Go

package dnfbridge
import (
"bytes"
"context"
"os"
"testing"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
"longheng.io/server/internal/modules/dnf/premium"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
dnfrevivecoin "longheng.io/server/internal/modules/dnf/revivecoin"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestCurrentBoosterContractRewardsActivateWithoutBackpackCapacity(t *testing.T) {
catalog := mustCurrentBoosterSpecialRewardCatalog(t)
repositories := dnfrepomemory.NewMemoryGroup()
ctx := context.Background()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "19", AccountID: "account-1"}); err != nil {
t.Fatal(err)
}
slots := make(map[string]dnfrepo.ItemStack)
for slot := int16(65); slot <= 120; slot++ {
slots[currentCeraShopInventorySlotKey(dnfrepo.MainInventoryListType, slot)] = dnfrepo.ItemStack{ItemID: 700, Count: 1}
}
// Keep a second box in the source slot. The entire consumable category is
// therefore occupied even after this one open is consumed.
slots["0:95"] = dnfrepo.ItemStack{ItemID: 520, Count: 2}
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "19", Slots: slots}); err != nil {
t.Fatal(err)
}
service := currentBoosterTestService(catalog, repositories)
service.premiumCatalog.contractsByItem = map[int64]currentPremiumContractInfo{
43: {ItemID: 43, PremiumType: 27, DurationSeconds: 86400},
30: {ItemID: 30, PremiumType: 22, DurationSeconds: 86400},
10000391: {ItemID: 10000391, PremiumType: premium.TypeCrystal, DurationSeconds: 86400},
2660703: {ItemID: 2660703, PremiumType: 84, DurationSeconds: 86400},
}
service.premiumCatalog.crystalCubeIDs = [6]int64{700, 701, 702, 703, 704, 705}
connection := &bufferConn{}
session := &gameSession{conn: connection, connID: "booster-contract-test", selectedCharacterID: 19}
if err := service.handleGameCommand(
session,
byte(dnfenum.GameCmdCommand),
uint16(dnfenum.CmdPacketUseBoosterItem),
[]byte{95, 0},
); err != nil {
t.Fatal(err)
}
ack, _ := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(ack.Body) == 0 || ack.Body[0] != 1 {
t.Fatalf("op160 failure body=%x", ack.Body)
}
inventory, found, err := repositories.Inventory.Load(ctx, "19")
if err != nil || !found {
t.Fatalf("load inventory found=%t err=%v", found, err)
}
if source := inventory.Slots["0:95"]; source.ItemID != 520 || source.Count != 1 {
t.Fatalf("source=%+v", source)
}
for _, itemID := range []int64{43, 30, 10000391, 2660703} {
for _, stack := range inventory.Slots {
if stack.ItemID == itemID {
t.Fatalf("contract item %d leaked into backpack: %+v", itemID, stack)
}
}
}
account, found, err := repositories.Account.Load(ctx, "account-1")
if err != nil || !found {
t.Fatalf("load account found=%t err=%v", found, err)
}
for _, premiumType := range []int64{22, 27, 84, premium.TypeCrystal} {
if !premium.Active(account, premiumType, time.Now().UTC()) {
t.Fatalf("premium type %d was not activated: %+v", premiumType, account.Metadata)
}
}
}
func TestRealPVFAttendanceContractAndReviveGiftRouting(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify attendance gift routing")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
items, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
reviveGift, err := resolveCurrentBoosterDefinition(items, 490006575, currentBoosterRequestRandom)
if err != nil {
t.Fatal(err)
}
if reviveGift.MaterialItemID != 0 || reviveGift.MaterialCount != 0 ||
reviveGift.Random.MaterialItemID != 0 || reviveGift.Random.MaterialCountPerUse != 0 {
t.Fatalf("gift acquisition metadata leaked into opening cost: %+v", reviveGift)
}
reviveAmounts, _, _, err := resolveCurrentBoosterRewardAmounts(
reviveGift,
currentBoosterOpenRequest{Kind: currentBoosterRequestRandom},
func(int64) (int64, error) { return 0, nil },
)
if err != nil || reviveAmounts[uint32(dnfrevivecoin.WalletItemID)] != 2 {
t.Fatalf("revive reward amounts=%v err=%v", reviveAmounts, err)
}
contractGift, err := resolveCurrentBoosterDefinition(items, 490006574, currentBoosterRequestRandom)
if err != nil {
t.Fatal(err)
}
contractAmounts, _, _, err := resolveCurrentBoosterRewardAmounts(
contractGift,
currentBoosterOpenRequest{Kind: currentBoosterRequestRandom},
func(int64) (int64, error) { return 0, nil },
)
if err != nil {
t.Fatal(err)
}
premiumCatalog, err := buildCurrentPremiumCatalog(archive)
if err != nil {
t.Fatal(err)
}
for _, itemID := range []int64{43, 30, 10000391, 2660703} {
if contractAmounts[uint32(itemID)] != 1 {
t.Fatalf("contract gift item=%d amount=%d", itemID, contractAmounts[uint32(itemID)])
}
contract, exists := premiumCatalog.contractsByItem[itemID]
if !exists || contract.PremiumType <= 0 || contract.DurationSeconds != 86400 {
t.Fatalf("premium contract item=%d mapping=%+v exists=%t", itemID, contract, exists)
}
}
if crystal := premiumCatalog.contractsByItem[10000391]; crystal.PremiumType != premium.TypeCrystal {
t.Fatalf("crystal contract mapping=%+v", crystal)
}
}
func TestCurrentBoosterReviveCoinGiftIgnoresAcquisitionMaterialAndRefreshesWallet(t *testing.T) {
catalog := mustCurrentBoosterSpecialRewardCatalog(t)
definition, err := resolveCurrentBoosterDefinition(catalog, 521, currentBoosterRequestRandom)
if err != nil {
t.Fatal(err)
}
if definition.MaterialItemID != 0 || definition.MaterialCount != 0 ||
definition.Random.MaterialItemID != 0 || definition.Random.MaterialCountPerUse != 0 {
t.Fatalf("gift opening cost=%+v, want unconditional open", definition)
}
repositories := dnfrepomemory.NewMemoryGroup()
ctx := context.Background()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "19", AccountID: "account-1"}); err != nil {
t.Fatal(err)
}
if err := repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{
CharacterID: "19",
Slots: map[string]dnfrepo.ItemStack{
"0:104": {ItemID: 521, Count: 1},
"0:1": {ItemID: dnfrevivecoin.WalletItemID, Count: 5},
"0:121": {ItemID: 999, Count: 2},
},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := currentBoosterTestService(catalog, repositories)
session := &gameSession{conn: connection, connID: "booster-revive-test", selectedCharacterID: 19}
if err := service.handleGameCommand(
session,
byte(dnfenum.GameCmdCommand),
uint16(dnfenum.CmdPacketUseBoosterItem),
[]byte{104, 0},
); err != nil {
t.Fatal(err)
}
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(ack.Body) == 0 || ack.Body[0] != 1 {
t.Fatalf("op160 failure body=%x", ack.Body)
}
rest = assertCurrentBoosterSourceRefresh(t, rest, 104, ^uint32(0), 0)
walletRefresh, _ := splitGameServerUpperPacket(t, rest)
if walletRefresh.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
len(walletRefresh.Body) < 3+currentItemListEntryWireSize ||
walletRefresh.Body[0] != dnfrepo.MainInventoryListType ||
!bytes.Equal(walletRefresh.Body[3:5], []byte{1, 0}) {
t.Fatalf("wallet refresh header=%+v body=%x", walletRefresh.Header, walletRefresh.Body)
}
inventory, found, err := repositories.Inventory.Load(ctx, "19")
if err != nil || !found {
t.Fatalf("load inventory found=%t err=%v", found, err)
}
if _, exists := inventory.Slots["0:104"]; exists {
t.Fatalf("source box was not consumed: %+v", inventory.Slots["0:104"])
}
if material := inventory.Slots["0:121"]; material.ItemID != 999 || material.Count != 2 {
t.Fatalf("gift acquisition material changed during open: %+v", material)
}
if wallet := inventory.Slots[dnfrevivecoin.WalletKey()]; wallet.ItemID != dnfrevivecoin.WalletItemID || wallet.Count != 7 {
t.Fatalf("wallet=%+v", wallet)
}
}
func mustCurrentBoosterSpecialRewardCatalog(t *testing.T) *pvfDungeonDropCatalog {
t.Helper()
source := dungeonDropCatalogTestSource{
"monster/monster.lst": "",
"equipment/equipment.lst": "",
"stackable/stackable.lst": "1 `coin.stk`\n30 `contract/30.stk`\n43 `contract/43.stk`\n520 `box/contracts.stk`\n521 `box/revive.stk`\n700 `material/filler.stk`\n999 `material/challenge_coin.stk`\n2660703 `contract/2660703.stk`\n10000391 `contract/10000391.stk`\n",
"stackable/coin.stk": "[stackable type]\n`[etc]` 0\n",
"stackable/contract/30.stk": "[stackable type]\n`[etc]` 0\n[stack limit]\n1000\n",
"stackable/contract/43.stk": "[stackable type]\n`[etc]` 0\n[stack limit]\n1000\n",
"stackable/contract/2660703.stk": "[stackable type]\n`[etc]` 0\n[stack limit]\n1000\n",
"stackable/contract/10000391.stk": "[stackable type]\n`[etc]` 0\n[stack limit]\n1000\n",
"stackable/material/filler.stk": "[stackable type]\n`[waste]` 0\n[stack limit]\n1000\n",
"stackable/material/challenge_coin.stk": "[stackable type]\n`[material]` 0\n[stack limit]\n1000\n",
"stackable/box/contracts.stk": "[stackable type]\n`[booster]` 0\n[stack limit]\n1000\n[booster info]\n[cera]\n1 43 1000 1\n[/cera]\n[cera]\n1 30 1000 1\n[/cera]\n[cera]\n1 10000391 1000 1\n[/cera]\n[cera]\n1 2660703 1000 1\n[/cera]\n[/booster info]\n",
"stackable/box/revive.stk": "[stackable type]\n`[booster]` 0\n[stack limit]\n1000\n[booster info]\n[cera]\n1 1 1000 2\n[/cera]\n[/booster info]\n[impossible contents]\ngift\n[/impossible contents]\n[need material]\n999 2\n[/need material]\n",
}
catalog, err := newPVFDungeonDropCatalog(source)
if err != nil {
t.Fatal(err)
}
return catalog
}