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,362 @@
|
||||
package quest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
dnfpvf "longheng.io/server/internal/modules/dnf/pvf"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
|
||||
)
|
||||
|
||||
// buildGiveUpCatalog 造两个任务:3145(被放弃的收集类任务)+ 3146(另一个进行中任务)。
|
||||
func buildGiveUpCatalog(t *testing.T, abandonedExtra, otherExtra string) *Catalog {
|
||||
t.Helper()
|
||||
source := catalogTestSource{
|
||||
DefaultList: "3145 `giveup.qst`\n3146 `other.qst`\n",
|
||||
"n_quest/giveup.qst": questCatalogTestDefinition("[daily]", 1, 99, "[all]", abandonedExtra),
|
||||
"n_quest/other.qst": questCatalogTestDefinition("[daily]", 1, 99, "[all]", otherExtra),
|
||||
}
|
||||
return buildCatalogFromSource(t, source)
|
||||
}
|
||||
|
||||
func buildCatalogFromSource(t *testing.T, source catalogTestSource) *Catalog {
|
||||
t.Helper()
|
||||
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := Load(context.Background(), index)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
// 普通材料永远不会进回收名单:这正是 2026-09-22 角色 1111 / 任务 8557 的形状
|
||||
// (`[seeking]` + `[int data] 10094602 10`,而 10094602 是 `[material]` 可交易材料)。
|
||||
func TestPlanGiveUpReclaimSkipsOrdinaryMaterials(t *testing.T) {
|
||||
catalog := buildGiveUpCatalog(t, "[type]\n`[seeking]`\n[int data]\n10094602 10\n", "")
|
||||
record := dnfrepo.QuestRecord{CharacterID: "18", States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active"},
|
||||
}}
|
||||
if plan := PlanGiveUpReclaim(catalog, record, 3145, nil); len(plan) != 0 {
|
||||
t.Fatalf("ordinary material reclaim plan = %+v, want empty", plan)
|
||||
}
|
||||
if plan := PlanGiveUpReclaim(catalog, record, 3145, func(int64) bool { return false }); len(plan) != 0 {
|
||||
t.Fatalf("classifier-false reclaim plan = %+v, want empty", plan)
|
||||
}
|
||||
}
|
||||
|
||||
// 任务专属(`[quest]` 家族)道具与 `[depend give item]` 事件道具要进回收名单。
|
||||
func TestPlanGiveUpReclaimIncludesQuestFamilyAndActivationItems(t *testing.T) {
|
||||
catalog := buildGiveUpCatalog(t, "[type]\n`[seeking]`\n[int data]\n1001 10\n", "")
|
||||
record := dnfrepo.QuestRecord{CharacterID: "18", States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active"},
|
||||
}}
|
||||
plan := PlanGiveUpReclaim(catalog, record, 3145, func(itemID int64) bool { return itemID == 1001 })
|
||||
if len(plan) != 1 || plan[0].ItemID != 1001 || plan[0].RetainCount != 0 {
|
||||
t.Fatalf("quest-family reclaim plan = %+v, want item 1001 retain 0", plan)
|
||||
}
|
||||
|
||||
catalogEvent := buildGiveUpCatalog(t, "[type]\n`[clear map]`\n[depend give item]\n2002 1\n", "")
|
||||
planEvent := PlanGiveUpReclaim(catalogEvent, record, 3145, nil)
|
||||
if len(planEvent) != 1 || planEvent[0].ItemID != 2002 {
|
||||
t.Fatalf("activation-item reclaim plan = %+v, want item 2002", planEvent)
|
||||
}
|
||||
}
|
||||
|
||||
// 其它仍在进行中的任务需要的数量必须留下(参考端的 retainCount)。
|
||||
func TestPlanGiveUpReclaimRetainsOtherActiveQuestRequirements(t *testing.T) {
|
||||
catalog := buildGiveUpCatalog(t,
|
||||
"[type]\n`[seeking]`\n[int data]\n1001 10\n",
|
||||
"[type]\n`[seeking]`\n[int data]\n1001 3\n")
|
||||
record := dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active"},
|
||||
3146: {Status: "active"},
|
||||
3147: {Status: "completed"}, // 已完成的任务不占额度
|
||||
},
|
||||
}
|
||||
plan := PlanGiveUpReclaim(catalog, record, 3145, func(int64) bool { return true })
|
||||
if len(plan) != 1 || plan[0].RetainCount != 3 {
|
||||
t.Fatalf("retain plan = %+v, want item 1001 retain 3", plan)
|
||||
}
|
||||
|
||||
record.States[3146] = dnfrepo.QuestState{Status: "completed"}
|
||||
plan = PlanGiveUpReclaim(catalog, record, 3145, func(int64) bool { return true })
|
||||
if len(plan) != 1 || plan[0].RetainCount != 0 {
|
||||
t.Fatalf("no-active-successor plan = %+v, want retain 0", plan)
|
||||
}
|
||||
}
|
||||
|
||||
// `[cant giveup]` 依然是唯一的拒绝理由;收集类任务不再被拦(本次修复的核心)。
|
||||
func TestOwnerApplyGiveUpHonorsPVFBoundaryAndAllowsCollectionQuests(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
extra string
|
||||
wantErr error
|
||||
wantRemoved bool
|
||||
}{
|
||||
{name: "cant giveup", extra: "[type]\n`[disjoint item]`\n[cant giveup]\n", wantErr: ErrQuestCannotGiveUp},
|
||||
{name: "seeking", extra: "[type]\n`[seeking]`\n[int data]\n1001 10\n", wantRemoved: true},
|
||||
{name: "get item", extra: "[type]\n`[get item]`\n[int data]\n1001 10\n", wantRemoved: true},
|
||||
{name: "seek n meet npc", extra: "[type]\n`[seek n meet npc]`\n[int data]\n1001 10\n", wantRemoved: true},
|
||||
{name: "depend give item", extra: "[type]\n`[clear map]`\n[depend give item]\n1001 1\n", wantRemoved: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "18", AccountID: "acc-quest"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{3145: {Status: "active", ProgressValue: 1}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = owner.ApplyGiveUp(
|
||||
ctx,
|
||||
testAcceptCatalog(t, test.extra),
|
||||
NewQuestIDCommand(alignedRequest("acc-quest", 18), "giveup_quest", QuestIDRequest{QuestID: 3145}),
|
||||
)
|
||||
if !errors.Is(err, test.wantErr) {
|
||||
t.Fatalf("ApplyGiveUp() error = %v, want %v", err, test.wantErr)
|
||||
}
|
||||
stored, ok, loadErr := repos.Quest.Load(ctx, "18")
|
||||
if loadErr != nil || !ok {
|
||||
t.Fatalf("load quest ok=%t err=%v", ok, loadErr)
|
||||
}
|
||||
_, exists := stored.States[3145]
|
||||
if !test.wantRemoved {
|
||||
if !exists || stored.States[3145].Status != "active" {
|
||||
t.Fatalf("rejected give-up mutated quest: %+v", stored.States[3145])
|
||||
}
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
t.Fatalf("allowed give-up left quest active: %+v", stored.States[3145])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 收集类任务 + 普通材料:放弃成功,且背包一件都不动(1111/8557 的现场)。
|
||||
func TestOwnerApplyGiveUpKeepsOrdinaryMaterialsInBag(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "18", AccountID: "acc-quest"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{3145: {Status: "active"}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "18",
|
||||
Slots: map[string]dnfrepo.ItemStack{"0:121": {ItemID: 10094602, Count: 10}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplyGiveUpSettlement(ctx, buildGiveUpCatalog(t, "[type]\n`[seeking]`\n[int data]\n10094602 10\n", ""),
|
||||
GiveUpInput{
|
||||
AccountID: "acc-quest", SelectedCharacterID: 18, QuestID: 3145,
|
||||
// 物品目录判定 10094602 是 `[material]`:不是任务专属 ⇒ 不回收。
|
||||
IsQuestItem: func(int64) bool { return false },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyGiveUpSettlement() error = %v", err)
|
||||
}
|
||||
if len(result.Reclaimed) != 0 || result.ReclaimSkipped || result.HasPostCommitInventory {
|
||||
t.Fatalf("ordinary-material reclaim = %+v, want none", result)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load quest found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := stored.States[3145]; exists {
|
||||
t.Fatalf("quest 3145 still active: %+v", stored.States[3145])
|
||||
}
|
||||
inventory, found, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if stack := inventory.Slots["0:121"]; stack.ItemID != 10094602 || stack.Count != 10 {
|
||||
t.Fatalf("ordinary material changed: %+v", stack)
|
||||
}
|
||||
}
|
||||
|
||||
// 任务专属道具:放弃成功 + 同事务扣减 + 给出提交后快照;其它任务需要的量要留下。
|
||||
func TestOwnerApplyGiveUpReclaimsQuestItemsInSameSettlement(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "18", AccountID: "acc-quest"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active"},
|
||||
3146: {Status: "active"}, // 还需要 1001×3
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "18",
|
||||
Slots: map[string]dnfrepo.ItemStack{"0:177": {ItemID: 1001, Count: 6}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := buildGiveUpCatalog(t,
|
||||
"[type]\n`[seeking]`\n[int data]\n1001 10\n",
|
||||
"[type]\n`[seeking]`\n[int data]\n1001 3\n")
|
||||
result, err := owner.ApplyGiveUpSettlement(ctx, catalog, GiveUpInput{
|
||||
AccountID: "acc-quest", SelectedCharacterID: 18, QuestID: 3145,
|
||||
IsQuestItem: func(itemID int64) bool { return itemID == 1001 },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyGiveUpSettlement() error = %v", err)
|
||||
}
|
||||
if len(result.Reclaimed) != 1 {
|
||||
t.Fatalf("reclaimed = %+v, want one row", result.Reclaimed)
|
||||
}
|
||||
receipt := result.Reclaimed[0]
|
||||
if receipt.SlotKey != "0:177" || receipt.ItemID != 1001 || receipt.Delta != 3 || receipt.PostCount != 3 {
|
||||
t.Fatalf("reclaim receipt = %+v, want slot 0:177 item 1001 delta 3 post 3", receipt)
|
||||
}
|
||||
if !result.HasPostCommitInventory {
|
||||
t.Fatal("reclaim without post-commit inventory snapshot")
|
||||
}
|
||||
if stack := result.PostCommitInventory.Slots["0:177"]; stack.ItemID != 1001 || stack.Count != 3 {
|
||||
t.Fatalf("post-commit snapshot = %+v, want item 1001 count 3", stack)
|
||||
}
|
||||
inventory, found, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if stack := inventory.Slots["0:177"]; stack.ItemID != 1001 || stack.Count != 3 {
|
||||
t.Fatalf("persisted inventory = %+v, want item 1001 count 3", stack)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load quest found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := stored.States[3145]; exists {
|
||||
t.Fatalf("quest 3145 still active: %+v", stored.States[3145])
|
||||
}
|
||||
if stored.States[3146].Status != "active" {
|
||||
t.Fatalf("other active quest changed: %+v", stored.States[3146])
|
||||
}
|
||||
}
|
||||
|
||||
// 整行扣完时该行必须从背包里消失。
|
||||
func TestOwnerApplyGiveUpDeletesEmptiedQuestItemRow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "18", AccountID: "acc-quest"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{3145: {Status: "active"}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "18",
|
||||
Slots: map[string]dnfrepo.ItemStack{"0:177": {ItemID: 1001, Count: 2}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplyGiveUpSettlement(ctx, buildGiveUpCatalog(t, "[type]\n`[seeking]`\n[int data]\n1001 2\n", ""),
|
||||
GiveUpInput{
|
||||
AccountID: "acc-quest", SelectedCharacterID: 18, QuestID: 3145,
|
||||
IsQuestItem: func(int64) bool { return true },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyGiveUpSettlement() error = %v", err)
|
||||
}
|
||||
if len(result.Reclaimed) != 1 || result.Reclaimed[0].Delta != 2 || result.Reclaimed[0].PostCount != 0 {
|
||||
t.Fatalf("reclaim receipt = %+v, want delta 2 post 0", result.Reclaimed)
|
||||
}
|
||||
inventory, found, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := inventory.Slots["0:177"]; exists {
|
||||
t.Fatalf("emptied quest-item row survived: %+v", inventory.Slots["0:177"])
|
||||
}
|
||||
}
|
||||
|
||||
// 没有事务单元时:放弃仍然成功(绝不挡住玩家),只是不回收并如实上报。
|
||||
func TestOwnerApplyGiveUpWithoutSettlementStillAbandons(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "18", AccountID: "acc-quest"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "18",
|
||||
States: map[int64]dnfrepo.QuestState{3145: {Status: "active"}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "18",
|
||||
Slots: map[string]dnfrepo.ItemStack{"0:177": {ItemID: 1001, Count: 4}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repos.CharacterSettlement = nil
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplyGiveUpSettlement(ctx, buildGiveUpCatalog(t, "[type]\n`[seeking]`\n[int data]\n1001 10\n", ""),
|
||||
GiveUpInput{
|
||||
AccountID: "acc-quest", SelectedCharacterID: 18, QuestID: 3145,
|
||||
IsQuestItem: func(int64) bool { return true },
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyGiveUpSettlement() error = %v", err)
|
||||
}
|
||||
if !result.ReclaimSkipped || result.ReclaimSkipReason != "character_settlement_transaction_unavailable" {
|
||||
t.Fatalf("reclaim skip = %t/%q", result.ReclaimSkipped, result.ReclaimSkipReason)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "18")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load quest found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := stored.States[3145]; exists {
|
||||
t.Fatalf("degraded give-up left quest active: %+v", stored.States[3145])
|
||||
}
|
||||
inventory, _, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stack := inventory.Slots["0:177"]; stack.Count != 4 {
|
||||
t.Fatalf("degraded give-up mutated inventory: %+v", stack)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user