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,448 @@
|
||||
package cargo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
func TestOwnerPlanAccountReadsCargoMetadata(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-1",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_gold": "1200",
|
||||
"account_cargo_level": "3",
|
||||
"account_cargo_created": "1",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.PlanAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: " acc-1 "})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanAccount() error = %v", err)
|
||||
}
|
||||
if got.AccountID != "acc-1" || got.CargoGold != 1200 || got.CargoLevel != 3 || !got.CargoCreated {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerPlanMoneyDepositChecksCharacterGold(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-2",
|
||||
Metadata: map[string]string{"account_cargo_gold": "50"},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "77",
|
||||
AccountID: "acc-2",
|
||||
Stats: map[string]int64{"gold": 500},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.PlanMoney(ctx, MoneyCommand{
|
||||
AccountID: "acc-2",
|
||||
SelectedCharacterID: 77,
|
||||
Direction: MoneyDeposit,
|
||||
Amount: 300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanMoney() error = %v", err)
|
||||
}
|
||||
if got.CharacterGold != 500 || got.CargoGold != 50 || got.Amount != 300 || got.Direction != MoneyDeposit {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerPlanMoneyWithdrawChecksCargoGold(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-3",
|
||||
Metadata: map[string]string{"account_cargo_gold": "900"},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "78",
|
||||
AccountID: "acc-3",
|
||||
Stats: map[string]int64{"gold": 20},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.PlanMoney(ctx, MoneyCommand{
|
||||
AccountID: "acc-3",
|
||||
SelectedCharacterID: 78,
|
||||
Direction: MoneyWithdraw,
|
||||
Amount: 700,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PlanMoney() error = %v", err)
|
||||
}
|
||||
if got.CharacterGold != 20 || got.CargoGold != 900 || got.Amount != 700 || got.Direction != MoneyWithdraw {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerPlanMoneyRejectsInsufficientGold(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-4",
|
||||
Metadata: map[string]string{"account_cargo_gold": "5"},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "79",
|
||||
AccountID: "acc-4",
|
||||
Stats: map[string]int64{"gold": 10},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
_, err = owner.PlanMoney(ctx, MoneyCommand{
|
||||
AccountID: "acc-4",
|
||||
SelectedCharacterID: 79,
|
||||
Direction: MoneyWithdraw,
|
||||
Amount: 6,
|
||||
})
|
||||
if !errors.Is(err, ErrGoldInsufficient) {
|
||||
t.Fatalf("PlanMoney() error = %v, want ErrGoldInsufficient", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyCreateCargoChargesGold(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{AccountID: "acc-5"}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "80",
|
||||
AccountID: "acc-5",
|
||||
Stats: map[string]int64{"gold": 500000},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.ApplyAccount(ctx, "create_account_cargo", AccountCommand{
|
||||
AccountID: "acc-5",
|
||||
SelectedCharacterID: 80,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyAccount() error = %v", err)
|
||||
}
|
||||
if !got.Changed || got.CharacterGold != 400000 || got.CargoLevel != 1 || !got.CargoCreated {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
|
||||
account, ok, err := repos.Account.Load(ctx, "acc-5")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||||
}
|
||||
if account.Metadata["account_cargo_level"] != "1" || account.Metadata["account_cargo_created"] != "true" {
|
||||
t.Fatalf("account metadata = %+v", account.Metadata)
|
||||
}
|
||||
character, ok, err := repos.Character.Load(ctx, "80")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load character ok=%t err=%v", ok, err)
|
||||
}
|
||||
if got := character.Stats["gold"]; got != 400000 {
|
||||
t.Fatalf("character gold = %d, want 400000", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAccountUpgradeDebitsAccountSharedCera(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-cera",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_created": "true",
|
||||
"account_cargo_level": "8",
|
||||
"account_cargo_gold": "0",
|
||||
"account_cera": "3000",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "87", AccountID: "acc-cera", Stats: map[string]int64{"gold": 1}}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-cera", SelectedCharacterID: 87})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyAccount() error = %v", err)
|
||||
}
|
||||
if !got.Changed || got.Cost.Kind != CostCera || got.Cost.Amount != 2000 || got.CharacterCera != 1000 || got.CargoLevel != 16 {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
account, ok, err := repos.Account.Load(ctx, "acc-cera")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||||
}
|
||||
if account.Metadata["account_cera"] != "1000" || account.Metadata["account_cargo_level"] != "16" {
|
||||
t.Fatalf("account metadata = %+v", account.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAccountUpgradeRejectsInsufficientAccountCera(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-cera-poor",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_created": "true",
|
||||
"account_cargo_level": "8",
|
||||
"account_cargo_gold": "0",
|
||||
"account_cera": "100",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "88", AccountID: "acc-cera-poor", Stats: map[string]int64{"gold": 1}}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
if _, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-cera-poor", SelectedCharacterID: 88}); !errors.Is(err, ErrGoldInsufficient) {
|
||||
t.Fatalf("ApplyAccount() error = %v, want ErrGoldInsufficient", err)
|
||||
}
|
||||
account, _, _ := repos.Account.Load(ctx, "acc-cera-poor")
|
||||
if account.Metadata["account_cera"] != "100" || account.Metadata["account_cargo_level"] != "8" {
|
||||
t.Fatalf("insufficient-cera attempt mutated account: %+v", account.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyMoneyDepositPersistsBothSides(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-6",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_gold": "50",
|
||||
"account_cargo_level": "1",
|
||||
"account_cargo_created": "true",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "81",
|
||||
AccountID: "acc-6",
|
||||
Stats: map[string]int64{"gold": 500},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.ApplyMoney(ctx, MoneyCommand{
|
||||
AccountID: "acc-6",
|
||||
SelectedCharacterID: 81,
|
||||
Direction: MoneyDeposit,
|
||||
Amount: 300,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyMoney() error = %v", err)
|
||||
}
|
||||
if !got.Changed || got.CharacterGold != 200 || got.CargoGold != 350 {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
|
||||
account, ok, err := repos.Account.Load(ctx, "acc-6")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||||
}
|
||||
if got := account.Metadata["account_cargo_gold"]; got != "350" {
|
||||
t.Fatalf("cargo gold = %q, want 350", got)
|
||||
}
|
||||
character, ok, err := repos.Character.Load(ctx, "81")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load character ok=%t err=%v", ok, err)
|
||||
}
|
||||
if got := character.Stats["gold"]; got != 200 {
|
||||
t.Fatalf("character gold = %d, want 200", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAccountUpgradeConsumesVoidMagicStoneForMiddleTiers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-void",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_created": "true",
|
||||
"account_cargo_level": "16",
|
||||
"account_cargo_gold": "0",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "86", AccountID: "acc-void", Stats: map[string]int64{"gold": 1, "cera": 1}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "86", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:10": {ItemID: accountCargoVoidMagicStoneItemID, Count: 200},
|
||||
"0:11": {ItemID: accountCargoVoidMagicStoneItemID, Count: 100},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-void", SelectedCharacterID: 86})
|
||||
if err != nil {
|
||||
t.Fatalf("upgrade account cargo: %v", err)
|
||||
}
|
||||
if !got.Changed || got.CargoLevel != 24 || got.Cost.Kind != CostMaterial || got.Cost.Amount != 250 {
|
||||
t.Fatalf("upgrade result=%+v", got)
|
||||
}
|
||||
items, found, err := repos.Inventory.Load(ctx, "86")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := items.Slots["0:10"]; exists {
|
||||
t.Fatalf("first material stack not consumed: %+v", items.Slots)
|
||||
}
|
||||
if got := items.Slots["0:11"].Count; got != 50 {
|
||||
t.Fatalf("remaining void magic stones=%d want=50", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyMoneyWithdrawPersistsBothSides(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-7",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_gold": "900",
|
||||
"account_cargo_level": "1",
|
||||
"account_cargo_created": "true",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "82",
|
||||
AccountID: "acc-7",
|
||||
Stats: map[string]int64{"gold": 20},
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.ApplyMoney(ctx, MoneyCommand{
|
||||
AccountID: "acc-7",
|
||||
SelectedCharacterID: 82,
|
||||
Direction: MoneyWithdraw,
|
||||
Amount: 700,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyMoney() error = %v", err)
|
||||
}
|
||||
if !got.Changed || got.CharacterGold != 720 || got.CargoGold != 200 {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNextAccountCargoTierCoversEveryCeraShopTool 锁住完整阶梯。2026-08-25 现网
|
||||
// 抓包证明客户端按顺序消耗 12 个点券工具,容量 64 时请求 tool6(64->72);阶梯
|
||||
// 只写到 64 时那次购买被拒,客户端显示"物品栏空间不足"。
|
||||
func TestNextAccountCargoTierCoversEveryCeraShopTool(t *testing.T) {
|
||||
want := []int64{1, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120}
|
||||
tier := want[0]
|
||||
for _, expected := range want[1:] {
|
||||
next, ok := NextAccountCargoTier(tier)
|
||||
if !ok || next != expected {
|
||||
t.Fatalf("NextAccountCargoTier(%d) = %d, %t; want %d, true", tier, next, ok, expected)
|
||||
}
|
||||
tier = next
|
||||
}
|
||||
if next, ok := NextAccountCargoTier(tier); ok {
|
||||
t.Fatalf("NextAccountCargoTier(%d) = %d, true; want max level", tier, next)
|
||||
}
|
||||
if next, ok := NextAccountCargoTier(5); ok {
|
||||
t.Fatalf("NextAccountCargoTier(5) = %d, true; want an unknown tier to fail closed", next)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOwnerApplyAccountUpgradeRequiresToolAboveMaterialTiers 保证新接上的高档位
|
||||
// 不会被金库窗口按钮(op306)白送——那些档只有商城工具能付费。
|
||||
func TestOwnerApplyAccountUpgradeRequiresToolAboveMaterialTiers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Account.Save(ctx, dnfrepo.AccountRecord{
|
||||
AccountID: "acc-tool",
|
||||
Metadata: map[string]string{
|
||||
"account_cargo_created": "true",
|
||||
"account_cargo_level": "64",
|
||||
"account_cargo_gold": "0",
|
||||
"account_cera": "9000000",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save account: %v", err)
|
||||
}
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "89", AccountID: "acc-tool", Stats: map[string]int64{"gold": 900000000}}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
if _, err := owner.ApplyAccount(ctx, "upgrade_account_cargo", AccountCommand{AccountID: "acc-tool", SelectedCharacterID: 89}); !errors.Is(err, ErrCargoUpgradeToolRequired) {
|
||||
t.Fatalf("ApplyAccount() error = %v, want ErrCargoUpgradeToolRequired", err)
|
||||
}
|
||||
account, ok, err := repos.Account.Load(ctx, "acc-tool")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load account ok=%t err=%v", ok, err)
|
||||
}
|
||||
if account.Metadata["account_cargo_level"] != "64" || account.Metadata["account_cera"] != "9000000" {
|
||||
t.Fatalf("blocked op306 upgrade mutated account: %+v", account.Metadata)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user