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,869 @@
|
||||
package quest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/alignedcmd"
|
||||
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"
|
||||
)
|
||||
|
||||
func TestOwnerPlanReadsQuestState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "46",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "46",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
0x1234: {Status: "active", ProgressValue: 2},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save quest: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.Plan(ctx, NewFinishCommand(alignedRequest("acc-quest", 46), FinishQuestRequest{
|
||||
QuestID: 0x1234,
|
||||
RewardSelectIndex: 1,
|
||||
HasRewardSelect: true,
|
||||
Multiplier: 2,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("Plan() error = %v", err)
|
||||
}
|
||||
if got.AccountID != "acc-quest" || got.CharacterID != "46" || !got.Known || got.Status != "active" || got.ProgressValue != 2 || got.RewardSelectIndex != 1 || got.Multiplier != 2 {
|
||||
t.Fatalf("result = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplySetTriggerType1RecomputesSeekingItems(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "48",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "48",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
0x2222: {
|
||||
Status: "active",
|
||||
ProgressValue: 1,
|
||||
Extra: map[string]string{
|
||||
"seeking_item_ids": "9001",
|
||||
},
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save quest: %v", err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{
|
||||
CharacterID: "48",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:9": {ItemID: 9001, Count: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save inventory: %v", err)
|
||||
}
|
||||
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
got, err := owner.ApplySetTrigger(ctx, NewTriggerCommand(alignedRequest("acc-quest", 48), SetTriggerRequest{
|
||||
QuestID: 0x2222,
|
||||
TriggerType: 1,
|
||||
IsIncrement: true,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("ApplySetTrigger() error = %v", err)
|
||||
}
|
||||
if got.ProgressValue != 0 {
|
||||
t.Fatalf("progress = %d, want 0 after held seeking item", got.ProgressValue)
|
||||
}
|
||||
if !got.StateChanged {
|
||||
t.Fatalf("first trigger result = %+v, want durable state change", got)
|
||||
}
|
||||
loaded, ok, err := repos.Quest.Load(ctx, "48")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load quest ok=%v err=%v", ok, err)
|
||||
}
|
||||
if got := loaded.States[0x2222].ProgressValue; got != 0 {
|
||||
t.Fatalf("stored progress = %d, want 0", got)
|
||||
}
|
||||
replayed, err := owner.ApplySetTrigger(ctx, NewTriggerCommand(alignedRequest("acc-quest", 48), SetTriggerRequest{
|
||||
QuestID: 0x2222,
|
||||
TriggerType: 1,
|
||||
IsIncrement: true,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("ApplySetTrigger replay error = %v", err)
|
||||
}
|
||||
if replayed.StateChanged {
|
||||
t.Fatalf("replay result = %+v, want no durable state change", replayed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplySetTriggerSeekAndMeetNPCRecomputesAccountSharedItems(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "50",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "50",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active", ProgressValue: int64(packTriggerChannels(304, 1))},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "acc-quest",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 499},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[type]\n`[seek n meet npc]`\n[int data]\n3037 500 126\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := NewTriggerCommand(alignedRequest("acc-quest", 50), SetTriggerRequest{
|
||||
QuestID: 3145, TriggerType: 0x10,
|
||||
})
|
||||
insufficient, err := owner.ApplySetTriggerWithCatalog(ctx, catalog, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if insufficient.StateChanged || insufficient.ProgressValue != int64(packTriggerChannels(304, 1)) {
|
||||
t.Fatalf("insufficient result=%+v", insufficient)
|
||||
}
|
||||
|
||||
if err := repos.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "acc-quest",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 120019},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
satisfied, err := owner.ApplySetTriggerWithCatalog(ctx, catalog, cmd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !satisfied.StateChanged || satisfied.ProgressValue != int64(packTriggerChannels(0, 1)) {
|
||||
t.Fatalf("satisfied result=%+v", satisfied)
|
||||
}
|
||||
stored, ok, err := repos.Quest.Load(ctx, "50")
|
||||
if err != nil || !ok || stored.States[3145].ProgressValue != int64(packTriggerChannels(0, 1)) {
|
||||
t.Fatalf("stored quest ok=%t err=%v state=%+v", ok, err, stored.States[3145])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplySetTriggerUpdatesOnlyRequestedPackedChannel(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "49",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "49",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3193: {Status: "active", ProgressValue: int64(packTriggerChannels(1, 1))},
|
||||
3203: {Status: "active", ProgressValue: int64(packTriggerChannels(4, 6, 7))},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
apply := func(questID uint16, triggerType byte, increment bool, want int64) {
|
||||
t.Helper()
|
||||
result, err := owner.ApplySetTrigger(ctx, NewTriggerCommand(alignedRequest("acc-quest", 49), SetTriggerRequest{
|
||||
QuestID: questID, TriggerType: triggerType, IsIncrement: increment,
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("ApplySetTrigger quest=%d type=%#x: %v", questID, triggerType, err)
|
||||
}
|
||||
if !result.StateChanged || result.ProgressValue != want {
|
||||
t.Fatalf("ApplySetTrigger quest=%d type=%#x result=%+v want progress=%d", questID, triggerType, result, want)
|
||||
}
|
||||
}
|
||||
|
||||
apply(3193, 0x10, false, int64(packTriggerChannels(0, 1)))
|
||||
apply(3193, 0x20, false, 0)
|
||||
apply(3203, 0x40, false, int64(packTriggerChannels(4, 6, 6)))
|
||||
apply(3203, 0x20, true, int64(packTriggerChannels(4, 7, 6)))
|
||||
}
|
||||
|
||||
// Live incident 2026-09-12T13:32:33Z: quest 2900 held the PVF initial trigger
|
||||
// 262657 = packTriggerChannels(1, 1, 1) and the client reported its objective
|
||||
// with the scalar (no lane mask) op33 shape. Subtracting one from the packed
|
||||
// u32 changed the durable value on every identical request, so the client kept
|
||||
// re-triggering it: 3767 requests at ~30 Hz across seven logins while every
|
||||
// town entry stayed frozen. The scalar shape must answer the satisfied
|
||||
// objective once and then stay idempotent.
|
||||
func TestOwnerApplySetTriggerScalarRemoveClearsPackedObjectiveWithoutCounterLoop(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "52",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "52",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
2900: {Status: "active", ProgressValue: int64(packTriggerChannels(1, 1, 1))},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
apply := func() PlanResult {
|
||||
t.Helper()
|
||||
result, applyErr := owner.ApplySetTrigger(ctx, NewTriggerCommand(alignedRequest("acc-quest", 52), SetTriggerRequest{QuestID: 2900}))
|
||||
if applyErr != nil {
|
||||
t.Fatalf("ApplySetTrigger quest=2900: %v", applyErr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if first := apply(); !first.StateChanged || first.ProgressValue != 0 {
|
||||
t.Fatalf("scalar remove result=%+v want state changed to 0", first)
|
||||
}
|
||||
if replay := apply(); replay.StateChanged || replay.ProgressValue != 0 {
|
||||
t.Fatalf("replayed scalar remove result=%+v want unchanged zero", replay)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "52")
|
||||
if err != nil || !found || stored.States[2900].ProgressValue != 0 {
|
||||
t.Fatalf("persisted scalar-remove state found=%t err=%v state=%+v", found, err, stored.States[2900])
|
||||
}
|
||||
}
|
||||
|
||||
// 婚约任务 20737 的真实 PVF 形状:[type] `[condition under clear]` + [sub type] 22 +
|
||||
// [int data] `-1 -1 5`,即"跟心上人组 2 人队通关普通地下城 5 次"。
|
||||
//
|
||||
// 这种计数任务的进度由**服务端**在通关结算里权威地减 1(clear_map.go 的
|
||||
// decrementClearTriggerChannel),而客户端每通关一次还会发一条标量 op33
|
||||
// ("目标已满足")。标量通知必须原样回权威值、不再动进度,否则一次通关会被减两次
|
||||
// 甚至直接清零 —— 实测就是这样"通关 1 次就完成",客户端还显示 5/5。
|
||||
//
|
||||
// 对照:任务 2900 是 [hunt enemy],仍然保持"标量即满足 → 清零"的旧语义
|
||||
// (见 TestOwnerApplySetTriggerScalarRemoveClearsPackedObjectiveWithoutCounterLoop)。
|
||||
func TestOwnerApplySetTriggerScalarKeepsConditionUnderClearCounter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "61",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 接取后的权威进度:5 次待通关。
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "61",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
20737: {Status: "active", ProgressValue: int64(packTriggerChannels(5))},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := catalogTestSource{
|
||||
DefaultList: "20737 `wedding.qst`\n",
|
||||
"n_quest/wedding.qst": questCatalogTestDefinition("[wedding]", 30, 99, "[all]",
|
||||
"[type]\n`[condition under clear]`\n[sub type]\n22\n[int data]\n-1 -1 5\n"),
|
||||
}
|
||||
index, err := dnfpvf.Build(ctx, source, dnfpvf.BuildOptions{Lists: []string{DefaultList}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := Load(ctx, index)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplySetTriggerWithCatalog(ctx, catalog,
|
||||
NewTriggerCommand(alignedRequest("acc-quest", 61), SetTriggerRequest{QuestID: 20737}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ProgressValue != 5 || result.StateChanged {
|
||||
t.Fatalf("标量 op33 动了 [condition under clear] 的计数器: %+v", result)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "61")
|
||||
if err != nil || !found || stored.States[20737].ProgressValue != 5 {
|
||||
t.Fatalf("持久化的计数器被改写: found=%t err=%v state=%+v", found, err, stored.States[20737])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplySetTriggerRaidPhaseClearLimitsEachPhaseToOncePerChinaDay(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "51",
|
||||
AccountID: "acc-quest",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "51",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
12830: {Status: "active", ProgressValue: int64(packTriggerChannels(5, 5))},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := &Catalog{byID: map[int64]Definition{
|
||||
12830: {ID: 12830, Type: "[raid phase clear]", IntData: []int64{0, 0, 5, -1, 1, 0, 5, -1}},
|
||||
}}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
apply := func(triggerType byte) PlanResult {
|
||||
t.Helper()
|
||||
result, applyErr := owner.ApplySetTriggerWithCatalog(ctx, catalog, NewTriggerCommand(alignedRequest("acc-quest", 51), SetTriggerRequest{
|
||||
QuestID: 12830, TriggerType: triggerType,
|
||||
}))
|
||||
if applyErr != nil {
|
||||
t.Fatal(applyErr)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if first := apply(0x10); !first.StateChanged || first.ProgressValue != int64(packTriggerChannels(4, 5)) {
|
||||
t.Fatalf("first phase-one clear=%+v", first)
|
||||
}
|
||||
if duplicate := apply(0x10); duplicate.StateChanged || duplicate.ProgressValue != int64(packTriggerChannels(4, 5)) {
|
||||
t.Fatalf("same-day phase-one replay=%+v", duplicate)
|
||||
}
|
||||
if secondPhase := apply(0x20); !secondPhase.StateChanged || secondPhase.ProgressValue != int64(packTriggerChannels(4, 4)) {
|
||||
t.Fatalf("same-day phase-two clear=%+v", secondPhase)
|
||||
}
|
||||
if unknown := apply(0x40); unknown.StateChanged || unknown.ProgressValue != int64(packTriggerChannels(4, 4)) {
|
||||
t.Fatalf("undeclared phase channel=%+v", unknown)
|
||||
}
|
||||
|
||||
stored, ok, err := repos.Quest.Load(ctx, "51")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load stored quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
state := stored.States[12830]
|
||||
today := raidPhaseClearCalendarDay(time.Now())
|
||||
if state.Extra["raid_phase_clear_day_0"] != today || state.Extra["raid_phase_clear_day_1"] != today {
|
||||
t.Fatalf("daily markers=%+v want day=%s", state.Extra, today)
|
||||
}
|
||||
state.Extra["raid_phase_clear_day_0"] = "2000-01-01"
|
||||
stored.States[12830] = state
|
||||
if err := repos.Quest.Save(ctx, stored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if nextDay := apply(0x10); !nextDay.StateChanged || nextDay.ProgressValue != int64(packTriggerChannels(3, 4)) {
|
||||
t.Fatalf("next-day phase-one clear=%+v", nextDay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRaidPhaseClearCalendarDayRollsAtSixBeijing(t *testing.T) {
|
||||
before := time.Date(2026, 8, 19, 5, 59, 59, 0, raidPhaseClearCalendar)
|
||||
after := time.Date(2026, 8, 19, 6, 0, 0, 0, raidPhaseClearCalendar)
|
||||
if got := raidPhaseClearCalendarDay(before); got != "2026-08-18" {
|
||||
t.Fatalf("05:59 service day=%s want 2026-08-18", got)
|
||||
}
|
||||
if got := raidPhaseClearCalendarDay(after); got != "2026-08-19" {
|
||||
t.Fatalf("06:00 service day=%s want 2026-08-19", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPackedTriggerChannelArithmeticSaturatesAndPreservesReservedBits(t *testing.T) {
|
||||
const reserved = uint32(0xf8000000)
|
||||
start := reserved | packTriggerChannels(511, 6, 7)
|
||||
if got := incrementPackedTriggerChannel(start, 0); got != start {
|
||||
t.Fatalf("saturated packed trigger=%#08x want unchanged %#08x", got, start)
|
||||
}
|
||||
if got := decrementPackedTriggerChannel(reserved|packTriggerChannels(0, 6, 7), 0); got != reserved|packTriggerChannels(0, 6, 7) {
|
||||
t.Fatalf("zero packed trigger=%#08x want unchanged", got)
|
||||
}
|
||||
want := reserved | packTriggerChannels(1, 7, 7)
|
||||
if got := incrementPackedTriggerChannel(reserved|packTriggerChannels(1, 6, 7), 1); got != want {
|
||||
t.Fatalf("packed trigger increment=%#08x want=%#08x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerPlanRejectsMissingCharacter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatalf("NewOwner() error = %v", err)
|
||||
}
|
||||
_, err = owner.Plan(ctx, NewQuestIDCommand(alignedRequest("acc-quest", 47), "accept_quest", QuestIDRequest{QuestID: 1}))
|
||||
if !errors.Is(err, ErrCharacterNotFound) {
|
||||
t.Fatalf("Plan() error = %v, want ErrCharacterNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAcceptPersistsThenReturnsIdempotentResult(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "17",
|
||||
AccountID: "acc-quest",
|
||||
Job: "2",
|
||||
Level: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[type]\n`[clear map]`\n[int data]\n76126\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := NewQuestIDCommand(alignedRequest("acc-quest", 17), "accept_quest", QuestIDRequest{QuestID: 3145})
|
||||
first, err := owner.ApplyAccept(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyAccept first error = %v", err)
|
||||
}
|
||||
if first.QuestID != 3145 || first.InitTrigger != 1 || first.Idempotent {
|
||||
t.Fatalf("first result = %+v", first)
|
||||
}
|
||||
stored, ok, err := repos.Quest.Load(ctx, "17")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load stored quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
state := stored.States[3145]
|
||||
if state.Status != "active" || state.ProgressValue != 1 || state.Extra["pvf_path"] == "" || normalizeQuestTag(state.Extra["quest_type"]) != "clear map" {
|
||||
t.Fatalf("stored state = %+v", state)
|
||||
}
|
||||
second, err := owner.ApplyAccept(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyAccept retry error = %v", err)
|
||||
}
|
||||
if !second.Idempotent || second.InitTrigger != 1 {
|
||||
t.Fatalf("retry result = %+v", second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerSeekingAcceptPersistsPVFRequirementsAndReconcilesExistingState(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "17", AccountID: "acc-quest", Job: "2", Level: 1,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repos.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "17", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:9": {ItemID: 4821, Count: 1},
|
||||
"0:10": {ItemID: 4822, Count: 1},
|
||||
"0:11": {ItemID: 4823, Count: 1},
|
||||
"0:12": {ItemID: 4824, Count: 1},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[grade]\n`[side]`\n[type]\n`[seeking]`\n[int data]\n4821 1 4822 1 4823 1 4824 1\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
command := NewQuestIDCommand(alignedRequest("acc-quest", 17), "accept_quest", QuestIDRequest{QuestID: 3145})
|
||||
if _, err := owner.ApplyAccept(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, command); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, found, err := repos.Quest.Load(ctx, "17")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load accepted seeking quest found=%t err=%v", found, err)
|
||||
}
|
||||
state := stored.States[3145]
|
||||
if state.Extra["seeking_item_ids"] != "4821,4822,4823,4824" ||
|
||||
state.Extra["seeking_item_counts"] != "1,1,1,1" {
|
||||
t.Fatalf("accepted seeking requirements=%+v", state.Extra)
|
||||
}
|
||||
// Simulate a pre-fix accepted record which has no cached keys. The native
|
||||
// type-1 held-item probe must still resolve the PVF requirements and open
|
||||
// the exact terminal op34 path.
|
||||
delete(state.Extra, "seeking_item_ids")
|
||||
delete(state.Extra, "seeking_item_counts")
|
||||
stored.States[3145] = state
|
||||
if err := repos.Quest.Save(ctx, stored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplySetTriggerWithCatalog(ctx, catalog, NewTriggerCommand(
|
||||
alignedRequest("acc-quest", 17),
|
||||
SetTriggerRequest{QuestID: 3145, TriggerType: 1},
|
||||
))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.StateChanged || result.ProgressValue != 0 {
|
||||
t.Fatalf("seeking PVF fallback result=%+v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAcceptActivatesNoRewardMainSubQuests(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "19",
|
||||
AccountID: "acc-quest",
|
||||
Job: "2",
|
||||
Level: 3,
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "19",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "completed"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save quest: %v", err)
|
||||
}
|
||||
source := catalogTestSource{
|
||||
DefaultList: "3145 `pre.qst`\n3146 `parent.qst`\n3157 `hunt.qst`\n3054 `clear.qst`\n",
|
||||
"n_quest/pre.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]",
|
||||
"[type]\n`[clear map]`\n[int data]\n76126\n"),
|
||||
"n_quest/parent.qst": questCatalogTestDefinition("[epic]", 3, 99, "[gunner]",
|
||||
"[type]\n`[quest clear]`\n[pre required quest]\n3145\n[int data]\n3157 3054\n"),
|
||||
"n_quest/hunt.qst": questCatalogTestDefinition("[sub]", 3, 99, "[gunner]",
|
||||
"[type]\n`[hunt enemy]`\n[main quest]\n3146\n[int data]\n3 -1 13099 3 1\n"),
|
||||
"n_quest/clear.qst": questCatalogTestDefinition("[sub]", 1, 99, "[gunner]",
|
||||
"[type]\n`[clear map]`\n[main quest]\n3146\n[int data]\n76136\n"),
|
||||
}
|
||||
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)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplyAccept(ctx, catalog, CharacterEligibility{Level: 3, Job: 2}, NewQuestIDCommand(alignedRequest("acc-quest", 19), "accept_quest", QuestIDRequest{QuestID: 3146}))
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyAccept parent error = %v", err)
|
||||
}
|
||||
if result.QuestID != 3146 || result.InitTrigger != 2 || result.Idempotent {
|
||||
t.Fatalf("parent accept result = %+v", result)
|
||||
}
|
||||
stored, ok, err := repos.Quest.Load(ctx, "19")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load stored quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
for _, check := range []struct {
|
||||
id int64
|
||||
trigger int64
|
||||
linked bool
|
||||
}{
|
||||
{id: 3146, trigger: 2},
|
||||
{id: 3157, trigger: 1, linked: true},
|
||||
{id: 3054, trigger: 1, linked: true},
|
||||
} {
|
||||
state := stored.States[check.id]
|
||||
if state.Status != "active" || state.ProgressValue != check.trigger {
|
||||
t.Fatalf("quest %d state = %+v", check.id, state)
|
||||
}
|
||||
if check.linked && (state.Extra["main_quest_id"] != "3146" || state.Extra["auto_activated_by_main_quest"] != "true") {
|
||||
t.Fatalf("quest %d linked extra = %+v", check.id, state.Extra)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyActiveQuestClearLinkedSubQuestActivationRepairsExistingParent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repos := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repos.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "19",
|
||||
AccountID: "acc-quest",
|
||||
Job: "2",
|
||||
Level: 3,
|
||||
}); err != nil {
|
||||
t.Fatalf("save character: %v", err)
|
||||
}
|
||||
if err := repos.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "19",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3146: {Status: "active", ProgressValue: 2},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("save quest: %v", err)
|
||||
}
|
||||
source := catalogTestSource{
|
||||
DefaultList: "3146 `parent.qst`\n3157 `hunt.qst`\n3054 `clear.qst`\n",
|
||||
"n_quest/parent.qst": questCatalogTestDefinition("[epic]", 3, 99, "[gunner]",
|
||||
"[type]\n`[quest clear]`\n[int data]\n3157 3054\n"),
|
||||
"n_quest/hunt.qst": questCatalogTestDefinition("[sub]", 3, 99, "[gunner]",
|
||||
"[type]\n`[hunt enemy]`\n[main quest]\n3146\n[int data]\n3 -1 13099 3 1\n"),
|
||||
"n_quest/clear.qst": questCatalogTestDefinition("[sub]", 1, 99, "[gunner]",
|
||||
"[type]\n`[clear map]`\n[main quest]\n3146\n[int data]\n76136\n"),
|
||||
}
|
||||
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)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
activatedAt := time.Date(2026, 7, 17, 14, 20, 0, 0, time.UTC)
|
||||
result, err := owner.ApplyActiveQuestClearLinkedSubQuestActivation(ctx, catalog, "19", activatedAt)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyActiveQuestClearLinkedSubQuestActivation error = %v", err)
|
||||
}
|
||||
if result.Idempotent || len(result.Activations) != 2 || len(result.ChangedFields) != 1 || result.ChangedFields[0] != dnfrepo.QuestFieldStates {
|
||||
t.Fatalf("activation result=%+v", result)
|
||||
}
|
||||
stored, ok, err := repos.Quest.Load(ctx, "19")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load stored quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
for _, questID := range []int64{3157, 3054} {
|
||||
state := stored.States[questID]
|
||||
if state.Status != "active" || state.ProgressValue != 1 ||
|
||||
state.Extra["main_quest_id"] != "3146" ||
|
||||
state.Extra["auto_activated_by_main_quest"] != "true" ||
|
||||
state.Extra["auto_activation_reason"] != "active_quest_clear_parent_reconcile" {
|
||||
t.Fatalf("linked quest %d state=%+v", questID, state)
|
||||
}
|
||||
}
|
||||
replay, err := owner.ApplyActiveQuestClearLinkedSubQuestActivation(ctx, catalog, "19", activatedAt.Add(time.Second))
|
||||
if err != nil {
|
||||
t.Fatalf("replay activation error = %v", err)
|
||||
}
|
||||
if !replay.Idempotent || len(replay.Activations) != 0 || len(replay.ChangedFields) != 0 {
|
||||
t.Fatalf("replay result=%+v", replay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAcceptEventItemQuestRequiresAllocatorWithoutMutation(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.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "18", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[type]\n`[clear map]`\n[int data]\n76126\n[depend give item]\n1001 1\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := NewQuestIDCommand(alignedRequest("acc-quest", 18), "accept_quest", QuestIDRequest{QuestID: 3145})
|
||||
if _, err := owner.ApplyAccept(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd); !errors.Is(err, ErrAcceptItemAllocatorRequired) {
|
||||
t.Fatalf("ApplyAccept error = %v", err)
|
||||
}
|
||||
if _, ok, err := repos.Quest.Load(ctx, "18"); err != nil || ok {
|
||||
t.Fatalf("event-item rejection mutated quest record ok=%t err=%v", ok, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAcceptWithItemsAtomicallyCommitsQuestAndInventory(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.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "18", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[type]\n`[clear map]`\n[int data]\n76126\n[depend give item]\n1001 2\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
allocate := func(record *dnfrepo.InventoryRecord, request FinishItemGrantRequest) (FinishCommittedItem, error) {
|
||||
if record.Slots == nil {
|
||||
record.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
const key = "0:65"
|
||||
stack := record.Slots[key]
|
||||
stack.ItemID = request.ItemID
|
||||
stack.Count += request.Count
|
||||
stack.RawEntry = []byte{1}
|
||||
record.Slots[key] = stack
|
||||
return FinishCommittedItem{
|
||||
SlotKey: key, SlotIndex: 65, ItemID: request.ItemID,
|
||||
Delta: request.Count, PostCount: stack.Count, CountOrSeed: uint32(request.Count), RawEntry: []byte{1},
|
||||
}, nil
|
||||
}
|
||||
cmd := NewQuestIDCommand(alignedRequest("acc-quest", 18), "accept_quest", QuestIDRequest{QuestID: 3145})
|
||||
result, err := owner.ApplyAcceptWithItems(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd, allocate)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.Idempotent || result.QuestID != 3145 || result.InitTrigger != 1 || len(result.EventItems) != 1 ||
|
||||
result.EventItems[0].ItemID != 1001 || result.EventItems[0].Delta != 2 {
|
||||
t.Fatalf("accept result=%+v", result)
|
||||
}
|
||||
questRecord, found, err := repos.Quest.Load(ctx, "18")
|
||||
if err != nil || !found || questRecord.States[3145].Status != "active" {
|
||||
t.Fatalf("quest record=%+v found=%t err=%v", questRecord, found, err)
|
||||
}
|
||||
inventory, found, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil || !found || inventory.Slots["0:65"].ItemID != 1001 || inventory.Slots["0:65"].Count != 2 {
|
||||
t.Fatalf("inventory=%+v found=%t err=%v", inventory, found, err)
|
||||
}
|
||||
replay, err := owner.ApplyAcceptWithItems(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd, allocate)
|
||||
if err != nil || !replay.Idempotent || len(replay.EventItems) != 0 {
|
||||
t.Fatalf("idempotent replay=%+v err=%v", replay, err)
|
||||
}
|
||||
inventory, _, _ = repos.Inventory.Load(ctx, "18")
|
||||
if inventory.Slots["0:65"].Count != 2 {
|
||||
t.Fatalf("idempotent replay granted twice: %+v", inventory.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyAcceptWithItemsRollsBackBothAggregates(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.Inventory.Save(ctx, dnfrepo.InventoryRecord{CharacterID: "18", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog := testAcceptCatalog(t, "[type]\n`[clear map]`\n[int data]\n76126\n[depend give item]\n1001 1 1002 1\n")
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
forced := errors.New("forced second event-item allocation failure")
|
||||
allocate := func(record *dnfrepo.InventoryRecord, request FinishItemGrantRequest) (FinishCommittedItem, error) {
|
||||
if request.ItemID == 1002 {
|
||||
return FinishCommittedItem{}, forced
|
||||
}
|
||||
if record.Slots == nil {
|
||||
record.Slots = make(map[string]dnfrepo.ItemStack)
|
||||
}
|
||||
record.Slots["0:65"] = dnfrepo.ItemStack{ItemID: request.ItemID, Count: request.Count}
|
||||
return FinishCommittedItem{SlotKey: "0:65", SlotIndex: 65, ItemID: request.ItemID, Delta: request.Count, PostCount: request.Count, CountOrSeed: uint32(request.Count), RawEntry: []byte{1}}, nil
|
||||
}
|
||||
cmd := NewQuestIDCommand(alignedRequest("acc-quest", 18), "accept_quest", QuestIDRequest{QuestID: 3145})
|
||||
if _, err := owner.ApplyAcceptWithItems(ctx, catalog, CharacterEligibility{Level: 1, Job: 2}, cmd, allocate); !errors.Is(err, forced) {
|
||||
t.Fatalf("ApplyAcceptWithItems error=%v", err)
|
||||
}
|
||||
if _, found, err := repos.Quest.Load(ctx, "18"); err != nil || found {
|
||||
t.Fatalf("rolled-back quest found=%t err=%v", found, err)
|
||||
}
|
||||
inventory, found, err := repos.Inventory.Load(ctx, "18")
|
||||
if err != nil || !found || len(inventory.Slots) != 0 {
|
||||
t.Fatalf("rolled-back inventory=%+v found=%t err=%v", inventory, found, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOwnerApplyGiveUpPersistsActiveQuestRemoval(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{
|
||||
3144: {Status: "completed", ProgressValue: 1},
|
||||
3145: {Status: "active", ProgressValue: 7},
|
||||
3146: {Status: "active", ProgressValue: 2},
|
||||
},
|
||||
Progress: map[int64]dnfrepo.QuestState{
|
||||
3145: {Status: "active", ProgressValue: 7},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner, err := NewOwner(repos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := owner.ApplyGiveUp(
|
||||
ctx,
|
||||
testAcceptCatalog(t, "[type]\n`[disjoint item]`\n[int data]\n1\n"),
|
||||
NewQuestIDCommand(alignedRequest("acc-quest", 18), "giveup_quest", QuestIDRequest{QuestID: 3145}),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyGiveUp() error = %v", err)
|
||||
}
|
||||
if result.AccountID != "acc-quest" || result.CharacterID != "18" || result.QuestID != 3145 {
|
||||
t.Fatalf("ApplyGiveUp() result = %+v", result)
|
||||
}
|
||||
stored, ok, err := repos.Quest.Load(ctx, "18")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load stored quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
if _, exists := stored.States[3145]; exists {
|
||||
t.Fatalf("abandoned quest remains in states: %+v", stored.States[3145])
|
||||
}
|
||||
if _, exists := stored.Progress[3145]; exists {
|
||||
t.Fatalf("abandoned quest remains in progress: %+v", stored.Progress[3145])
|
||||
}
|
||||
if stored.States[3144].Status != "completed" || stored.States[3146].Status != "active" {
|
||||
t.Fatalf("unrelated quest states changed: %+v", stored.States)
|
||||
}
|
||||
if _, err := owner.ApplyGiveUp(
|
||||
ctx,
|
||||
testAcceptCatalog(t, "[type]\n`[disjoint item]`\n[int data]\n1\n"),
|
||||
NewQuestIDCommand(alignedRequest("acc-quest", 18), "giveup_quest", QuestIDRequest{QuestID: 3145}),
|
||||
); !errors.Is(err, ErrQuestNotActive) {
|
||||
t.Fatalf("ApplyGiveUp() replay error = %v, want %v", err, ErrQuestNotActive)
|
||||
}
|
||||
}
|
||||
|
||||
// 放弃任务的判据从这里搬到了 give_up_reclaim_test.go:
|
||||
// `[cant giveup]` 仍然是唯一的拒绝理由,收集类任务改为"允许放弃 + 回收任务专属道具"。
|
||||
func testAcceptCatalog(t *testing.T, extra string) *Catalog {
|
||||
t.Helper()
|
||||
source := catalogTestSource{
|
||||
DefaultList: "3145 `accept.qst`\n",
|
||||
"n_quest/accept.qst": questCatalogTestDefinition("[epic]", 1, 99, "[gunner]", extra),
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func alignedRequest(accountID string, selectedCharacterID uint16) alignedcmd.Request {
|
||||
return alignedcmd.Request{AccountID: accountID, SelectedCharacterID: selectedCharacterID}
|
||||
}
|
||||
Reference in new issue
Block a user