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,174 @@
|
||||
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 = ¤tPremiumCatalog{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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user