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

175 lines
8.2 KiB
Go

package dnfbridge
import (
"context"
"errors"
"testing"
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
dnfquest "longheng.io/server/internal/modules/dnf/quest"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
appkit "longheng.io/server/internal/platform/app"
)
func TestGMExtendedAccountSkillAndInventoryFlow(t *testing.T) {
t.Parallel()
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
must := func(err error) {
if err != nil {
t.Fatal(err)
}
}
must(repositories.Account.Save(ctx, dnfrepo.AccountRecord{
AccountID: "dnf:extended", HonorExp: 123,
Metadata: map[string]string{"account_cera": "50", "account_cargo_gold": "70", "account_cargo_level": "8", "account_cargo_created": "true"},
}))
must(repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
CharacterID: "11", AccountID: "dnf:extended", Name: "Extended", Job: "0", Level: 20,
Stats: map[string]int64{"gold": 100, "bonus_sp": 10, "bonus_tp": 2},
}))
must(repositories.Skill.Save(ctx, dnfrepo.SkillRecord{CharacterID: "11", Points: dnfrepo.SkillPointState{
TotalSP: 100, RemainingSP: 75, TotalTP: 10, RemainingTP: 7, SyncedLevel: 20,
}}))
must(repositories.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "11",
Slots: map[string]dnfrepo.ItemStack{"0:4": {ItemID: 1001, Count: 9}},
Warehouse: map[string]dnfrepo.ItemStack{"2:3": {ItemID: 2001, Count: 1}},
}))
must(repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{AccountID: "dnf:extended",
Slots: map[string]dnfrepo.ItemStack{"12:0": {ItemID: 3001, Count: 2}},
}))
service := NewWithOptions(&appkit.Env{}, ServiceOptions{RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }})
service.premiumCatalog = &currentPremiumCatalog{crystalCubeIDs: [6]int64{3033, 3034, 3035, 3036, 3037, 3262}}
itemCatalog, err := newPVFDungeonDropCatalog(dungeonDropCatalogTestSource{
"monster/monster.lst": "",
"equipment/equipment.lst": "",
"stackable/stackable.lst": "3033 `cube/black.stk`\n3034 `cube/white.stk`\n3035 `cube/red.stk`\n3036 `cube/blue.stk`\n3037 `cube/clear.stk`\n3262 `cube/gold.stk`\n",
"stackable/cube/black.stk": "[name] `黑色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
"stackable/cube/white.stk": "[name] `白色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
"stackable/cube/red.stk": "[name] `红色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
"stackable/cube/blue.stk": "[name] `蓝色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
"stackable/cube/clear.stk": "[name] `无色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
"stackable/cube/gold.stk": "[name] `金色小晶体`\n[stackable type] `[material]`\n[stack limit] 4294967295\n",
})
if err != nil {
t.Fatal(err)
}
service.pvfItemCatalog = itemCatalog
account, err := service.GMAccount(ctx, "dnf:extended")
if err != nil || account.Cera != 50 || account.CargoGold != 70 || account.CargoLevel != 8 || !account.CargoCreated {
t.Fatalf("GMAccount = %+v, %v", account, err)
}
account, err = service.GMSetAccountValue(ctx, "dnf:extended", "cera", 999)
if err != nil || account.Cera != 999 {
t.Fatalf("GMSetAccountValue = %+v, %v", account, err)
}
detail, err := service.GMSetCharacterSkillBonus(ctx, "11", 30, 5)
if err != nil || detail.BonusSP != 30 || detail.BonusTP != 5 || detail.Skill.TotalSP != 120 || detail.Skill.RemainingSP != 95 || detail.Skill.TotalTP != 13 || detail.Skill.RemainingTP != 10 {
t.Fatalf("GMSetCharacterSkillBonus = %+v, %v", detail, err)
}
snapshot, err := service.GMInventory(ctx, "11")
if err != nil || len(snapshot.Items) != 3 {
t.Fatalf("GMInventory = %+v, %v", snapshot, err)
}
snapshot, err = service.GMDeleteInventoryItem(ctx, "11", "inventory", "0:4", 4)
if err != nil {
t.Fatal(err)
}
var remaining int64
for _, item := range snapshot.Items {
if item.Container == "inventory" && item.Key == "0:4" {
remaining = item.Count
}
}
if remaining != 5 {
t.Fatalf("remaining inventory count = %d, want 5", remaining)
}
cubes, err := service.GMSetAccountCube(ctx, "11", 4, 123456)
if err != nil || len(cubes.Cubes) != 6 || cubes.Cubes[4].ItemID != 3037 || cubes.Cubes[4].Count != 123456 {
t.Fatalf("GMSetAccountCube = %+v, %v", cubes, err)
}
accountInventory, found, err := repositories.AccountInventory.Load(ctx, "dnf:extended")
if err != nil || !found {
t.Fatalf("load repaired account cubes: found=%v err=%v", found, err)
}
repaired := accountInventory.Slots[dnfrepo.AccountSharedInventorySlotKey(dnfrepo.CrystalWarehouseFirstSlot+4)]
if repaired.ItemID != 3037 || repaired.Count != 123456 || len(repaired.RawEntry) != currentItemListEntryWireSize {
t.Fatalf("repaired cube stack = %+v", repaired)
}
if _, err := service.GMClearInventory(ctx, "11", "account"); err != nil {
t.Fatal(err)
}
accountInventory, found, err = repositories.AccountInventory.Load(ctx, "dnf:extended")
if err != nil || !found || len(accountInventory.Slots) != 0 {
t.Fatalf("account inventory = %+v found=%v err=%v", accountInventory, found, err)
}
service.gameSessions[11] = &gameSession{selectedCharacterID: 11}
if _, err := service.GMSetAccountValue(ctx, "dnf:extended", "cera", 1); !errors.Is(err, ErrGMCharacterOnline) {
t.Fatalf("online account write error = %v", err)
}
// 2026-09-14 行为变更:在线角色现在允许 GM 删除库存。
// 旧实现一律返回 ErrGMCharacterOnline,等于"删个东西先把人踢下线";
// 现在仓储改动照常提交,客户端那份显示靠提交后的 op14 增量刷新补。
//
// 这里挂的是没有 conn 的假会话,所以刷新会以
// "dnf game connection is unavailable" 失败并被记为 deferred —— 但**不能**
// 因此让删除本身失败,这条断言就是在钉这个边界。
if _, err := service.GMDeleteInventoryItem(ctx, "11", "warehouse", "2:3", 0); err != nil {
t.Fatalf("online warehouse delete should succeed, got %v", err)
}
inventory, found, err := repositories.Inventory.Load(ctx, "11")
if err != nil || !found {
t.Fatalf("load inventory after online delete: found=%v err=%v", found, err)
}
if _, still := inventory.Warehouse["2:3"]; still {
t.Fatalf("online warehouse delete did not commit: %+v", inventory.Warehouse)
}
if _, err := service.GMClearInventory(ctx, "11", "inventory"); err != nil {
t.Fatalf("online inventory clear should succeed, got %v", err)
}
inventory, found, err = repositories.Inventory.Load(ctx, "11")
if err != nil || !found {
t.Fatalf("load inventory after online clear: found=%v err=%v", found, err)
}
if len(inventory.Slots) != 0 {
t.Fatalf("online inventory clear did not commit: %+v", inventory.Slots)
}
}
func TestGMBatchQuestCompletionIsAtomicAndRewardFree(t *testing.T) {
t.Parallel()
ctx := context.Background()
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "12", AccountID: "dnf:q", Job: "0", Level: 20}); err != nil {
t.Fatal(err)
}
source := gmQuestPVFSource{
dnfquest.DefaultList: "10 `a.qst`\n11 `b.qst`\n",
"n_quest/a.qst": "[name]\n`A`\n[grade]\n`[normal]`\n[type]\n`[clear map]`\n[level]\n1 99\n[job]\n`[all]`\n",
"n_quest/b.qst": "[name]\n`B`\n[grade]\n`[normal]`\n[type]\n`[clear map]`\n[level]\n1 99\n[job]\n`[all]`\n[pre required quest]\n10\n",
}
index, err := dnfpvf.Build(ctx, source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
if err != nil {
t.Fatal(err)
}
catalog, err := dnfquest.Load(ctx, index)
if err != nil {
t.Fatal(err)
}
service := NewWithOptions(&appkit.Env{}, ServiceOptions{RepositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true }})
service.questCatalog = catalog
overview, err := service.GMCompleteQuestBatch(ctx, "12", []int64{10, 11})
if err != nil || len(overview.Completed) != 2 {
t.Fatalf("batch overview = %+v, %v", overview, err)
}
record, found, err := repositories.Quest.Load(ctx, "12")
if err != nil || !found || record.States[10].Extra["gm_rewards_granted"] != "false" || record.States[11].Status != "completed" {
t.Fatalf("quest record = %+v found=%v err=%v", record, found, err)
}
}