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,227 @@
|
||||
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
|
||||
}
|
||||
Reference in new issue
Block a user