Files
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

594 lines
24 KiB
Go

package dnfbridge
import (
"bytes"
"context"
"encoding/binary"
"os"
"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"
platformpvf "longheng.io/server/internal/platform/pvf"
)
func TestRealScriptPVFQuest1680AcceptanceItemsResolveInCurrentCatalog(t *testing.T) {
pvfPath := os.Getenv("DNFBRIDGE_REAL_PVF_SMOKE")
if pvfPath == "" {
t.Skip("set DNFBRIDGE_REAL_PVF_SMOKE to verify quest 1680 item definitions")
}
archive, err := platformpvf.LoadArchive(platformpvf.Options{Path: pvfPath})
if err != nil {
t.Fatal(err)
}
index, err := dnfpvf.Build(context.Background(), archive, dnfpvf.BuildOptions{Lists: []string{dnfquest.DefaultList}})
if err != nil {
t.Fatal(err)
}
quests, err := dnfquest.Load(context.Background(), index)
if err != nil {
t.Fatal(err)
}
definition, found := quests.Find(1680)
if !found || !definition.HasDependGiveItem || len(definition.DependGiveItemData) == 0 || len(definition.DependGiveItemData)%2 != 0 {
t.Fatalf("quest 1680 definition=%+v found=%t", definition, found)
}
items, err := newPVFDungeonDropCatalog(archive)
if err != nil {
t.Fatal(err)
}
for index := 0; index < len(definition.DependGiveItemData); index += 2 {
itemID, count := definition.DependGiveItemData[index], definition.DependGiveItemData[index+1]
resolved, err := items.ResolveItem(uint32(itemID))
if err != nil || resolved.ItemID != uint32(itemID) || count <= 0 {
t.Fatalf("quest 1680 event item=(%d,%d) resolved=%+v err=%v", itemID, count, resolved, err)
}
}
}
func TestCurrentAcceptQuestPersistsBeforeExactCurrentEXEAck(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "17",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
connID: "accept-quest-test",
channel: channelcatalog.Channel{ID: 38},
selectedCharacterID: 17,
initialTownRouteCharacterID: 17,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
staleTriggerKey := newCurrentSetQuestTriggerReplayKey(17, dnfquest.SetTriggerRequest{
QuestID: 3145,
TriggerType: 0x10,
})
session.suppressCurrentSetQuestTriggerReplay(staleTriggerKey)
request, err := dnfproto.BuildChannelPacket(uint16(dnfenum.CmdPacketAcceptQuest), []byte{0x1f, 0x00, 0x49, 0x0c}, 9, dnfproto.DefaultChannelClassification)
if err != nil {
t.Fatal(err)
}
if err := service.handleGameUpper(session, request); err != nil {
t.Fatal(err)
}
stored, ok, err := repositories.Quest.Load(context.Background(), "17")
if err != nil || !ok {
t.Fatalf("load persisted quest ok=%t err=%v", ok, err)
}
if state := stored.States[3145]; state.Status != "active" || state.ProgressValue != 1 {
t.Fatalf("persisted quest state = %+v", state)
}
if session.currentSetQuestTriggerReplaySuppressed(staleTriggerKey) {
t.Fatal("newly activated quest retained the preceding generation's terminal op33 replay key")
}
packet, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if packet.Header.Classification != dnfproto.DefaultChannelClassification || packet.Header.MsgID != uint16(dnfenum.CmdPacketAcceptQuest) {
t.Fatalf("ack header = %+v", packet.Header)
}
wantBody := []byte{0x01, 0x49, 0x0c, 0x01, 0x00, 0x00, 0x00, 0x00}
if !bytes.Equal(packet.Body, wantBody) {
t.Fatalf("ack body = % x, want % x", packet.Body, wantBody)
}
// A newly active quest needs the all-task op21 before op574 so the current
// client rebuilds its route-guide owner; no completed quest exists here, so
// the optional op356 completed bitmap is not appended.
acceptable, rest := splitGameServerUpperPacket(t, rest)
if acceptable.Header.Classification != 0 || acceptable.Header.MsgID != currentAcceptableQuestListMsgID {
t.Fatalf("acceptable list header=%+v", acceptable.Header)
}
active, trailing := splitGameServerUpperPacket(t, rest)
if len(trailing) != 0 || active.Header.Classification != 0 || active.Header.MsgID != currentActiveQuestSnapshotMsgID {
t.Fatalf("active snapshot header=%+v trailing=%x", active.Header, trailing)
}
wantActive := []byte{0x01, 0x00, 0x00, 0x00, 0x49, 0x0c, 0x01, 0x00, 0x00, 0x00}
if !bytes.Equal(active.Body, wantActive) {
t.Fatalf("active snapshot body=%x want=%x", active.Body, wantActive)
}
}
func TestCurrentAcceptQuestAfterBulkCompletionPublishesAuthoritativeGuideOrder(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "17",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 2,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
// Task-manual bulk completion leaves the predecessor completed and the
// recovered successor acceptable, but no active op574 generation exists
// until the player accepts it.
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
CharacterID: "17",
States: map[int64]dnfrepo.QuestState{
3145: {Status: "completed"},
},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: finishBridgeCatalog(t),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
connID: "accept-after-bulk-completion-test",
channel: channelcatalog.Channel{ID: 38},
selectedCharacterID: 17,
initialTownRouteCharacterID: 17,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
request, err := dnfproto.BuildChannelPacket(
uint16(dnfenum.CmdPacketAcceptQuest),
[]byte{0x1f, 0x00, 0x4a, 0x0c},
9,
dnfproto.DefaultChannelClassification,
)
if err != nil {
t.Fatal(err)
}
if err := service.handleGameUpper(session, request); err != nil {
t.Fatal(err)
}
stored, ok, err := repositories.Quest.Load(context.Background(), "17")
if err != nil || !ok || stored.States[3146].Status != "active" {
t.Fatalf("persisted successor state=%+v ok=%t err=%v", stored.States[3146], ok, err)
}
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
acceptable, rest := splitGameServerUpperPacket(t, rest)
active, rest := splitGameServerUpperPacket(t, rest)
completed, trailing := splitGameServerUpperPacket(t, rest)
if len(trailing) != 0 ||
ack.Header.MsgID != uint16(dnfenum.CmdPacketAcceptQuest) ||
ack.Header.Classification != dnfproto.DefaultChannelClassification ||
acceptable.Header.MsgID != currentAcceptableQuestListMsgID ||
acceptable.Header.Classification != 0 ||
active.Header.MsgID != currentActiveQuestSnapshotMsgID ||
active.Header.Classification != 0 ||
completed.Header.MsgID != currentClearQuestListMsgID ||
completed.Header.Classification != 0 {
t.Fatalf("recovered successor accept order ack=%+v acceptable=%+v active=%+v completed=%+v trailing=%x",
ack.Header, acceptable.Header, active.Header, completed.Header, trailing)
}
if len(active.Body) != 10 || binary.LittleEndian.Uint32(active.Body) != 1 ||
binary.LittleEndian.Uint16(active.Body[4:]) != 3146 {
t.Fatalf("recovered successor active snapshot=%x", active.Body)
}
}
func TestSuppressSceneInitializationAcceptQuestAllowsFinalizedTownScene(t *testing.T) {
session := &gameSession{
selectedCharacterID: 19,
townSceneReadyCharacterID: 19,
initialTownRouteStage: currentInitialTownRouteIdle,
}
suppressed, readiness, stage := (&Service{}).suppressSceneInitializationAcceptQuestRequest(session)
if suppressed || readiness != "town_scene_player_state_finalized" || stage != currentInitialTownRouteIdle {
t.Fatalf("town-ready accept suppression=%t readiness=%q stage=%d", suppressed, readiness, stage)
}
}
func TestCurrentAcceptQuestAlreadyActiveDoesNotReply(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "20",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
connID: "accept-quest-idempotent-test",
channel: channelcatalog.Channel{ID: 38},
selectedCharacterID: 20,
initialTownRouteCharacterID: 20,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
body := []byte{0x1f, 0x00, 0x49, 0x0c}
if err := service.handleCurrentAcceptQuest(session, body); err != nil {
t.Fatal(err)
}
connection.write.Reset()
if err := service.handleCurrentAcceptQuest(session, body); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("idempotent accept emitted response=%x", connection.write.Bytes())
}
}
func TestCurrentAcceptQuestEventItemAtomicallyPersistsAndUsesNativeAckRows(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "18", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
CharacterID: "18", Slots: map[string]dnfrepo.ItemStack{},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, true),
pvfItemCatalog: acceptQuestTestItemCatalog(),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 18,
initialTownRouteCharacterID: 18,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
questRecord, ok, err := repositories.Quest.Load(context.Background(), "18")
if err != nil || !ok || questRecord.States[3145].Status != "active" {
t.Fatalf("event-item quest record=%+v ok=%t err=%v", questRecord, ok, err)
}
inventory, ok, err := repositories.Inventory.Load(context.Background(), "18")
if err != nil || !ok || inventory.Slots["0:177"].ItemID != 1001 || inventory.Slots["0:177"].Count != 1 {
t.Fatalf("event-item inventory=%+v ok=%t err=%v", inventory, ok, err)
}
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
wantACK := []byte{
0x01, 0x49, 0x0c, 0x01, 0x00, 0x00, 0x00, 0x01,
0xb1, 0x00, 0xe9, 0x03, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
}
if ack.Header.MsgID != uint16(dnfenum.CmdPacketAcceptQuest) ||
ack.Header.Classification != dnfproto.DefaultChannelClassification || !bytes.Equal(ack.Body, wantACK) {
t.Fatalf("event-item ACK header=%+v body=%x want=%x", ack.Header, ack.Body, wantACK)
}
acceptable, rest := splitGameServerUpperPacket(t, rest)
if acceptable.Header.MsgID != currentAcceptableQuestListMsgID {
t.Fatalf("event-item acceptable list header=%+v", acceptable.Header)
}
active, trailing := splitGameServerUpperPacket(t, rest)
if len(trailing) != 0 || active.Header.MsgID != currentActiveQuestSnapshotMsgID {
t.Fatalf("event-item active snapshot header=%+v trailing=%x", active.Header, trailing)
}
}
func acceptQuestTestItemCatalog() *pvfDungeonDropCatalog {
return &pvfDungeonDropCatalog{
source: dungeonDropCatalogTestSource{
"stackable/quest/accept_event.stk": "[stackable type]\n`[quest]`\n[stack limit]\n999\n",
},
itemRefs: map[uint32]dungeonDropItemReference{
1001: {kind: dungeonDropItemStackable, path: "stackable/quest/accept_event.stk"},
},
itemCache: map[uint32]dungeonDropItemDefinition{
1001: {
ItemID: 1001, Kind: dungeonDropItemStackable, PVFPath: "stackable/quest/accept_event.stk",
StackableType: "[quest]", StackLimit: 999, SlotStart: 177, SlotEnd: 232,
},
},
}
}
func TestCurrentAcceptQuestPersistedActiveAcknowledgesOnceWithoutSnapshot(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "20",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
CharacterID: "20",
States: map[int64]dnfrepo.QuestState{
3145: {Status: "active", ProgressValue: 1},
},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 20,
initialTownRouteCharacterID: 20,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(rest) != 0 || ack.Header.MsgID != uint16(dnfenum.CmdPacketAcceptQuest) ||
ack.Header.Classification != dnfproto.DefaultChannelClassification ||
!bytes.Equal(ack.Body, []byte{1, 0x49, 0x0c, 0x01, 0, 0, 0, 0}) {
t.Fatalf("persisted-active accept ACK header=%+v body=%x trailing=%x", ack.Header, ack.Body, rest)
}
connection.write.Reset()
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
if got := connection.write.Bytes(); len(got) != 0 {
t.Fatalf("same-session persisted-active replay emitted response=%x", got)
}
}
func TestKnownTerminalQuestReplaySkipsPreDispatchLoggingOnlyForExactRequest(t *testing.T) {
session := &gameSession{selectedCharacterID: 20}
acceptRequest := dnfquest.QuestIDRequest{QuestID: 3145}
session.suppressCurrentAcceptQuestReplay(newCurrentAcceptQuestReplayKey(session.selectedCharacterID, acceptRequest))
acceptBody := []byte{0x1f, 0x00, 0x49, 0x0c}
if !shouldSuppressKnownQuestReplayBeforeGameUpperLog(session, uint16(dnfenum.CmdPacketAcceptQuest), dnfproto.DefaultChannelClassification, acceptBody) {
t.Fatal("known active op31 replay was not suppressed before logging")
}
if shouldSuppressKnownQuestReplayBeforeGameUpperLog(session, uint16(dnfenum.CmdPacketAcceptQuest), dnfproto.DefaultChannelClassification, []byte{0x1f, 0x00, 0x4a, 0x0c}) {
t.Fatal("different op31 quest was incorrectly suppressed")
}
if shouldSuppressKnownQuestReplayBeforeGameUpperLog(session, uint16(dnfenum.CmdPacketAcceptQuest), dnfproto.DefaultChannelClassification, []byte{0x20, 0x00, 0x49, 0x0c}) {
t.Fatal("malformed op31 echo was incorrectly suppressed")
}
triggerRequest := dnfquest.SetTriggerRequest{QuestID: 4728, TriggerType: 16}
session.suppressCurrentSetQuestTriggerReplay(newCurrentSetQuestTriggerReplayKey(session.selectedCharacterID, triggerRequest))
triggerBody := []byte{0x21, 0x00, 0x78, 0x12, 0x10, 0x00}
if !shouldSuppressKnownQuestReplayBeforeGameUpperLog(session, uint16(dnfenum.CmdPacketSetQuestTrigger), dnfproto.DefaultChannelClassification, triggerBody) {
t.Fatal("known zero op33 replay was not suppressed before logging")
}
if shouldSuppressKnownQuestReplayBeforeGameUpperLog(session, uint16(dnfenum.CmdPacketSetQuestTrigger), dnfproto.DefaultChannelClassification, []byte{0x21, 0x00, 0x78, 0x12, 0x11, 0x00}) {
t.Fatal("different op33 trigger channel was incorrectly suppressed")
}
}
func TestCurrentAcceptQuestKnownIneligiblePassiveRepeatIsSilent(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "19", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 1,
}); err != nil {
t.Fatal(err)
}
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
CharacterID: "19",
States: map[int64]dnfrepo.QuestState{
3145: {Status: "completed"},
},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 19,
initialTownRouteCharacterID: 19,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("known ineligible passive repeat emitted failure popup packet=%x", connection.write.Bytes())
}
stored, ok, err := repositories.Quest.Load(context.Background(), "19")
if err != nil || !ok || stored.States[3145].Status != "completed" {
t.Fatalf("ineligible quest state changed ok=%t err=%v state=%+v", ok, err, stored.States[3145])
}
}
func TestCurrentAcceptQuestUnknownDefinitionKeepsCurrentFailure23(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "19", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 1,
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 19,
initialTownRouteCharacterID: 19,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x0f, 0x27}); err != nil {
t.Fatal(err)
}
packet, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if len(rest) != 0 || packet.Header.MsgID != uint16(dnfenum.CmdPacketAcceptQuest) ||
!bytes.Equal(packet.Body, []byte{0x00, 0x17}) {
t.Fatalf("unknown-definition failure header=%+v body=%x rest=%x", packet.Header, packet.Body, rest)
}
if _, ok, err := repositories.Quest.Load(context.Background(), "19"); err != nil || ok {
t.Fatalf("unknown definition mutated quest record ok=%t err=%v", ok, err)
}
}
func TestCurrentAcceptQuestKnownIneligibleBeforeInitialTownTransitionSendsNoFailure(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "19", AccountID: defaultAccountPrefix + "1", Job: "2", Level: 1,
}); err != nil {
t.Fatal(err)
}
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
CharacterID: "19",
States: map[int64]dnfrepo.QuestState{
3145: {Status: "completed"},
},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 19,
initialTownRouteCharacterID: 19,
initialTownRouteStage: currentInitialTownRouteArmed,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("initial-town automatic ineligible accept emitted packet=%x", connection.write.Bytes())
}
stored, ok, err := repositories.Quest.Load(context.Background(), "19")
if err != nil || !ok || stored.States[3145].Status != "completed" {
t.Fatalf("initial-town ineligible quest state changed ok=%t err=%v state=%+v", ok, err, stored.States[3145])
}
}
func TestCurrentAcceptQuestEligibleBeforeInitialTownTransitionDoesNotMutateOrReply(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "19",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
session := &gameSession{
conn: connection,
selectedCharacterID: 19,
initialTownRouteCharacterID: 19,
initialTownRouteStage: currentInitialTownRouteArmed,
}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("initial-town automatic eligible accept emitted packet=%x", connection.write.Bytes())
}
if _, ok, err := repositories.Quest.Load(context.Background(), "19"); err != nil || ok {
t.Fatalf("initial-town automatic eligible accept mutated quest record ok=%t err=%v", ok, err)
}
}
func TestCurrentAcceptQuestEligibleOnColdFirstLoginBeforeSceneRouteDoesNotMutateOrReply(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "21",
AccountID: defaultAccountPrefix + "1",
Job: "2",
Level: 1,
Stats: map[string]int64{"grow_type": 0},
}); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
questCatalog: buildAcceptQuestTestCatalog(t, false),
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
// A cold first-login can emit the passive op31 before either a town or a
// tutorial/dungeon scene route has reached actor-ready state.
session := &gameSession{conn: connection, selectedCharacterID: 21}
if err := service.handleCurrentAcceptQuest(session, []byte{0x1f, 0x00, 0x49, 0x0c}); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("cold first-login passive accept emitted packet=%x", connection.write.Bytes())
}
if _, ok, err := repositories.Quest.Load(context.Background(), "21"); err != nil || ok {
t.Fatalf("cold first-login passive accept mutated quest record ok=%t err=%v", ok, err)
}
}
func buildAcceptQuestTestCatalog(t *testing.T, eventItem bool) *dnfquest.Catalog {
t.Helper()
extra := "[type]\n`[clear map]`\n[int data]\n76126\n"
if eventItem {
extra += "[depend give item]\n1001 1\n"
}
source := questListTestSource{
dnfquest.DefaultList: "3145 `accept.qst`\n",
"n_quest/accept.qst": "[grade]\n`[epic]`\n[level]\n1 99\n[job]\n`[gunner]`\n[exposed by npc]\n1\n" + extra,
}
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
}