服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
493 lines
21 KiB
Go
493 lines
21 KiB
Go
package dnfbridge
|
|
|
|
import (
|
|
"context"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
|
"longheng.io/server/internal/modules/dnf/dnfenum"
|
|
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
|
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"
|
|
)
|
|
|
|
func TestCurrentAutoCompleteMainQuestLegacyRouteCommitsNoRewardAndSendsExactReaderThenSnapshots(t *testing.T) {
|
|
ctx := context.Background()
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
character := dnfrepo.CharacterRecord{
|
|
CharacterID: "17",
|
|
AccountID: defaultAccountPrefix + "1",
|
|
Job: "2",
|
|
Level: 90,
|
|
Stats: map[string]int64{"grow_type": 0, "sp": 999, "exp": 54321},
|
|
}
|
|
if err := repositories.Character.Save(ctx, character); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "active", ProgressValue: 1},
|
|
102: {Status: "active", ProgressValue: 1},
|
|
103: {Status: "active", ProgressValue: 1},
|
|
106: {Status: "active", ProgressValue: 1},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteMainBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{
|
|
conn: connection,
|
|
connID: "auto-complete-main-legacy",
|
|
channel: channelcatalog.Channel{ID: 19},
|
|
selectedCharacterID: 17,
|
|
}
|
|
if err := service.handleGameCommand(
|
|
session,
|
|
byte(dnfenum.GameCmdCommand),
|
|
uint16(dnfenum.CmdPacketClearQuestTicket),
|
|
[]byte{100, 0, 0, 0},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found {
|
|
t.Fatalf("load quest found=%t err=%v", found, err)
|
|
}
|
|
for _, questID := range []int64{100} {
|
|
if state := stored.States[questID]; state.Status != "completed" || state.Extra["reward_state"] != "granted" {
|
|
t.Fatalf("epic quest %d state=%+v", questID, state)
|
|
}
|
|
}
|
|
if _, exists := stored.States[101]; exists {
|
|
t.Fatalf("absent successor quest 101 was manufactured: %+v", stored.States[101])
|
|
}
|
|
for _, questID := range []int64{102, 103, 106} {
|
|
if state := stored.States[questID]; state.Status != "active" || state.ProgressValue != 1 {
|
|
t.Fatalf("non-target quest %d state=%+v", questID, state)
|
|
}
|
|
}
|
|
postCharacter, found, err := repositories.Character.Load(ctx, "17")
|
|
if err != nil || !found || !reflect.DeepEqual(postCharacter, character) {
|
|
t.Fatalf("no-reward operation changed character found=%t err=%v got=%+v want=%+v", found, err, postCharacter, character)
|
|
}
|
|
|
|
response, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if response.Header.MsgID != uint16(dnfenum.CmdPacketClearQuestTicket) ||
|
|
response.Header.Classification != dnfproto.DefaultChannelClassification ||
|
|
len(response.Body) != currentAutoCompleteMainQuestAckSuccessSize ||
|
|
response.Body[0] != 1 {
|
|
t.Fatalf("op1429 response header=%+v body=%x", response.Header, response.Body)
|
|
}
|
|
|
|
if len(rest) < 16 || rest[0] != 0 || binary.LittleEndian.Uint16(rest[1:3]) != currentClearQuestListMsgID {
|
|
t.Fatalf("completed-quest snapshot header=%x", rest[:minInt(len(rest), 16)])
|
|
}
|
|
clearPacketLength := int(binary.LittleEndian.Uint32(rest[3:7]))
|
|
if clearPacketLength < 16 || clearPacketLength > len(rest) || rest[15] != 1 {
|
|
t.Fatalf("completed-quest snapshot length=%d wire=%x", clearPacketLength, rest[:minInt(len(rest), 16)])
|
|
}
|
|
clearBody, err := zlibDecompress(rest[16:clearPacketLength])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if clearBody[4+100] != 1 || clearBody[4+101] != 0 || clearBody[4+102] != 0 || clearBody[4+103] != 0 {
|
|
t.Fatalf("completed-quest flags 100=%d 101=%d 102=%d 103=%d", clearBody[4+100], clearBody[4+101], clearBody[4+102], clearBody[4+103])
|
|
}
|
|
active, acceptableWire := splitGameServerUpperPacket(t, rest[clearPacketLength:])
|
|
if active.Header.MsgID != currentActiveQuestSnapshotMsgID || active.Header.Classification != 0 {
|
|
t.Fatalf("active snapshot header=%+v body=%x", active.Header, active.Body)
|
|
}
|
|
acceptable, trailing := splitGameServerUpperPacket(t, acceptableWire)
|
|
if acceptable.Header.MsgID != currentAcceptableQuestListMsgID || acceptable.Header.Classification != 0 || len(trailing) != 0 {
|
|
t.Fatalf("acceptable snapshot header=%+v trailing=%x", acceptable.Header, trailing)
|
|
}
|
|
if len(acceptable.Body) < 4 || int(binary.LittleEndian.Uint32(acceptable.Body[:4])) != len(acceptable.Body)-4 {
|
|
t.Fatalf("acceptable snapshot protobuf length mismatch: %x", acceptable.Body)
|
|
}
|
|
_, messages := consumeCurrentSkillInfoFields(t, acceptable.Body[4:])
|
|
if len(messages[4]) != 1 {
|
|
t.Fatalf("acceptable snapshot field4 rows=%d body=%x", len(messages[4]), acceptable.Body)
|
|
}
|
|
questIDs := consumePackedQuestIDs(t, messages[4][0])
|
|
foundCompleted := false
|
|
foundSuccessor := false
|
|
for _, questID := range questIDs {
|
|
if questID == 100 {
|
|
foundCompleted = true
|
|
}
|
|
if questID == 101 {
|
|
foundSuccessor = true
|
|
}
|
|
}
|
|
if foundCompleted {
|
|
t.Fatalf("completed task-manual definition 100 leaked into op21: %v", questIDs)
|
|
}
|
|
if !foundSuccessor {
|
|
t.Fatalf("successor main quest 101 missing from task-manual snapshot: %v", questIDs)
|
|
}
|
|
activeCount := int(binary.LittleEndian.Uint32(active.Body[:4]))
|
|
activeIDs := make([]uint16, 0, activeCount)
|
|
for offset := 4; offset+6 <= len(active.Body); offset += 6 {
|
|
activeIDs = append(activeIDs, binary.LittleEndian.Uint16(active.Body[offset:offset+2]))
|
|
}
|
|
if !reflect.DeepEqual(activeIDs, []uint16{102, 103, 106}) {
|
|
t.Fatalf("active quest ids=%v body=%x", activeIDs, active.Body)
|
|
}
|
|
}
|
|
|
|
func TestCurrentAutoCompleteMainQuestNonzeroSelectorCompletesOnlyExactQuest(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "active"},
|
|
106: {Status: "active"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteMainBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{100, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found || stored.States[100].Status != "completed" || stored.States[106].Status != "active" {
|
|
t.Fatalf("exact selector result found=%t err=%v record=%+v", found, err, stored)
|
|
}
|
|
response, _ := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(response.Body) != currentAutoCompleteMainQuestAckSuccessSize || response.Body[0] != 1 {
|
|
t.Fatalf("exact selector response=%x", response.Body)
|
|
}
|
|
}
|
|
|
|
func TestCurrentAutoCompleteMainQuestZeroSelectorCompletesAllEligibleMainQuests(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
// A bulk request past level 50 needs the class change taken; see
|
|
// TestCurrentAutoCompleteMainQuestUntransferredJobIsRejectedWithNotice.
|
|
Stats: map[string]int64{"grow_type": 2},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "active"},
|
|
106: {Status: "active"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteMainBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found || stored.States[100].Status != "completed" || stored.States[106].Status != "completed" {
|
|
t.Fatalf("zero selector result found=%t err=%v record=%+v", found, err, stored)
|
|
}
|
|
if stored.States[100].Extra["reward_state"] != "granted" || stored.States[106].Extra["reward_state"] != "granted" {
|
|
t.Fatalf("zero selector must remain no-reward completion record=%+v", stored)
|
|
}
|
|
response, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
// The bulk request carries selector 0, so the old packed body opened with a
|
|
// zero byte and the class-1 envelope read the whole success as a failure.
|
|
if len(response.Body) != currentAutoCompleteMainQuestAckSuccessSize || response.Body[0] != 1 {
|
|
t.Fatalf("zero selector response=%x trailing=%x", response.Body, trailing)
|
|
}
|
|
if len(trailing) == 0 {
|
|
t.Fatal("zero selector success missing post-commit quest snapshots")
|
|
}
|
|
}
|
|
|
|
func buildAutoCompleteMainBridgeCatalog(t *testing.T) *dnfquest.Catalog {
|
|
t.Helper()
|
|
definition := func(grade string, level int, job, extra string) string {
|
|
return "[grade]\n`" + grade + "`\n[level]\n" + strconv.Itoa(level) + " 99\n[job]\n`" + job + "`\n[exposed by npc]\n1\n" + extra
|
|
}
|
|
source := questListTestSource{
|
|
dnfquest.DefaultList: "100 `epic_start.qst`\n101 `epic_next.qst`\n102 `epic_level_90.qst`\n103 `side.qst`\n106 `other_active_epic.qst`\n",
|
|
"n_quest/epic_start.qst": definition("[epic]", 20, "[gunner]", "[type]\n`[clear map]`\n"),
|
|
"n_quest/epic_next.qst": definition("[epic]", 30, "[gunner]", "[type]\n`[meet npc]`\n[pre required quest]\n100\n"),
|
|
"n_quest/epic_level_90.qst": definition("[epic]", 90, "[gunner]", "[type]\n`[meet npc]`\n[pre required quest]\n101\n"),
|
|
"n_quest/side.qst": definition("[side]", 10, "[gunner]", "[type]\n`[meet npc]`\n"),
|
|
"n_quest/other_active_epic.qst": definition("[epic]", 35, "[gunner]", "[type]\n`[meet npc]`\n"),
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := dnfquest.Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return catalog
|
|
}
|
|
|
|
// A bulk request that finds nothing it may complete must still answer with the
|
|
// class-1 failure envelope - one zero status byte plus the error byte the
|
|
// envelope reader consumes for this opcode - and then say why. The old packed
|
|
// body opened with the selector instead, so success and failure were
|
|
// indistinguishable on the wire and the client drew nothing either way.
|
|
func TestCurrentAutoCompleteMainQuestWithoutEligibleTargetSendsFailureEnvelopeAndNotice(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
Stats: map[string]int64{"grow_type": 2},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
// 102 stays open on purpose: it sits at level 90, above the level-89
|
|
// cutoff, so the bulk pass has nothing left it may touch.
|
|
States: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "completed"},
|
|
101: {Status: "completed"},
|
|
102: {Status: "active"},
|
|
106: {Status: "completed"},
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteMainBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if response.Header.MsgID != uint16(dnfenum.CmdPacketClearQuestTicket) ||
|
|
response.Header.Classification != dnfproto.DefaultChannelClassification ||
|
|
len(response.Body) != currentAutoCompleteMainQuestAckFailureSize ||
|
|
response.Body[0] != 0 {
|
|
t.Fatalf("no-target response header=%+v body=%x", response.Header, response.Body)
|
|
}
|
|
assertAutoCompleteRejectionNotice(t, trailing, currentAutoCompleteNoTargetNoticeText)
|
|
}
|
|
|
|
// The bulk pass past level 50 can only end in an empty candidate set when the
|
|
// character never took its class change: the story chain is gated on the first
|
|
// awakening quest group, and that group is the one thing a bulk pass must not
|
|
// complete for the player. Reject it up front with a notice that names the
|
|
// missing step instead of a silent empty result.
|
|
func TestCurrentAutoCompleteMainQuestUntransferredJobIsRejectedWithNotice(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
Stats: map[string]int64{"grow_type": 0},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Same exhausted set as the no-target case above; only grow_type differs, so
|
|
// the rejection reason is the missing class change instead of "nothing left".
|
|
wantQuest := dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{
|
|
100: {Status: "completed"},
|
|
101: {Status: "completed"},
|
|
102: {Status: "active"},
|
|
106: {Status: "completed"},
|
|
},
|
|
}
|
|
if err := repositories.Quest.Save(ctx, wantQuest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteMainBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found || !reflect.DeepEqual(stored, wantQuest) {
|
|
t.Fatalf("rejected bulk request mutated quest found=%t err=%v got=%+v want=%+v", found, err, stored, wantQuest)
|
|
}
|
|
response, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if response.Header.MsgID != uint16(dnfenum.CmdPacketClearQuestTicket) ||
|
|
len(response.Body) != currentAutoCompleteMainQuestAckFailureSize ||
|
|
response.Body[0] != 0 {
|
|
t.Fatalf("class-change rejection header=%+v body=%x", response.Header, response.Body)
|
|
}
|
|
assertAutoCompleteRejectionNotice(t, trailing, currentAutoCompleteClassChangeNoticeText)
|
|
}
|
|
|
|
func assertAutoCompleteRejectionNotice(t *testing.T, wire []byte, wantText string) {
|
|
t.Helper()
|
|
notice, rest := splitGameServerUpperPacketWithHeader(t, wire, dnfproto.GameServerUpperHeaderSize16)
|
|
if notice.Header.MsgID != currentNativeNoticeMsgID || notice.Header.Classification != 0 || len(rest) != 0 {
|
|
t.Fatalf("rejection notice header=%+v rest=%x", notice.Header, rest)
|
|
}
|
|
wantBody, err := buildCurrentNativeNoticeBody(
|
|
currentAutoCompleteMainQuestNoticeSurface.Type,
|
|
currentAutoCompleteMainQuestNoticeSurface.Channel,
|
|
0,
|
|
wantText,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(notice.Body) != string(wantBody) {
|
|
t.Fatalf("rejection notice body=%x want=%x", notice.Body, wantBody)
|
|
}
|
|
}
|
|
|
|
// The blocking awakening quest is still sitting in the player's list, so
|
|
// "当前没有可以一键完成的主线任务" reads as a lie. Name the quest instead.
|
|
func TestCurrentAutoCompleteMainQuestBlockedByAwakeningNamesTheQuest(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
Stats: map[string]int64{"grow_type": 0x13},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantQuest := dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{200: {Status: "active", ProgressValue: 1}},
|
|
}
|
|
if err := repositories.Quest.Save(ctx, wantQuest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: buildAutoCompleteAwakeningBridgeCatalog(t),
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found || !reflect.DeepEqual(stored, wantQuest) {
|
|
t.Fatalf("awakening rejection mutated quest found=%t err=%v got=%+v want=%+v", found, err, stored, wantQuest)
|
|
}
|
|
response, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(response.Body) != currentAutoCompleteMainQuestAckFailureSize || response.Body[0] != 0 {
|
|
t.Fatalf("awakening rejection body=%x", response.Body)
|
|
}
|
|
assertAutoCompleteRejectionNotice(t, trailing,
|
|
fmt.Sprintf(currentAutoCompleteAwakeningNoticeFormat, "依然存在的不祥之感"))
|
|
}
|
|
|
|
func buildAutoCompleteAwakeningBridgeCatalog(t *testing.T) *dnfquest.Catalog {
|
|
t.Helper()
|
|
source := questListTestSource{
|
|
dnfquest.DefaultList: "200 `awakening.qst`\n201 `after_awakening.qst`\n",
|
|
"n_quest/awakening.qst": "[grade]\n`[epic]`\n[level]\n50 99\n[job]\n`[gunner]`\n" +
|
|
"[exposed by npc]\n1\n[type]\n`[clear map]`\n[grow type]\n3\n[job change quest]\n2\n" +
|
|
"[reward type]\n`[awakening type]`\n[name]\n`依然存在的不祥之感`\n",
|
|
"n_quest/after_awakening.qst": "[grade]\n`[epic]`\n[level]\n50 99\n[job]\n`[all]`\n" +
|
|
"[exposed by npc]\n1\n[type]\n`[meet npc]`\n[pre required quest]\n200\n",
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := dnfquest.Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return catalog
|
|
}
|
|
|
|
// The Lv87 branch quest stays in the player's list until they answer it, so the
|
|
// rejection has to name it rather than claim the list is empty.
|
|
func TestCurrentAutoCompleteMainQuestBlockedByChoiceNamesTheQuest(t *testing.T) {
|
|
repositories := dnfrepomemory.NewMemoryGroup()
|
|
ctx := context.Background()
|
|
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
|
CharacterID: "17", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 90,
|
|
Stats: map[string]int64{"grow_type": 0x13},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wantQuest := dnfrepo.QuestRecord{
|
|
CharacterID: "17",
|
|
States: map[int64]dnfrepo.QuestState{300: {Status: "active", ProgressValue: 1}},
|
|
}
|
|
if err := repositories.Quest.Save(ctx, wantQuest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
source := questListTestSource{
|
|
dnfquest.DefaultList: "300 `branch.qst`\n301 `branch_a.qst`\n",
|
|
"n_quest/branch.qst": "[grade]\n`[epic]`\n[level]\n87 99\n[job]\n`[gunner]`\n" +
|
|
"[exposed by npc]\n1\n[type]\n`[question]`\n[name]\n`冒险家联盟的劝说和选择`\n",
|
|
"n_quest/branch_a.qst": "[grade]\n`[epic]`\n[level]\n87 99\n[job]\n`[gunner]`\n" +
|
|
"[exposed by npc]\n1\n[type]\n`[meet npc]`\n[pre required quest answer]\n300 1\n",
|
|
}
|
|
index, err := dnfpvf.Build(context.Background(), source, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
catalog, err := dnfquest.Load(context.Background(), index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection := &bufferConn{}
|
|
service := &Service{
|
|
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
|
questCatalog: catalog,
|
|
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
|
}
|
|
session := &gameSession{conn: connection, selectedCharacterID: 17}
|
|
if err := service.handleCurrentAutoCompleteMainQuest(session, []byte{0, 0, 0, 0}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
stored, found, err := repositories.Quest.Load(ctx, "17")
|
|
if err != nil || !found || !reflect.DeepEqual(stored, wantQuest) {
|
|
t.Fatalf("choice rejection mutated quest found=%t err=%v got=%+v", found, err, stored)
|
|
}
|
|
response, trailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
|
if len(response.Body) != currentAutoCompleteMainQuestAckFailureSize || response.Body[0] != 0 {
|
|
t.Fatalf("choice rejection body=%x", response.Body)
|
|
}
|
|
assertAutoCompleteRejectionNotice(t, trailing,
|
|
fmt.Sprintf(currentAutoCompleteChoiceNoticeFormat, "冒险家联盟的劝说和选择"))
|
|
}
|