服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
594 lines
24 KiB
Go
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
|
|
}
|