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,483 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/dnfenum"
|
||||
"longheng.io/server/internal/modules/dnf/premium"
|
||||
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
|
||||
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
|
||||
)
|
||||
|
||||
func newCrystalContractTestRuntime(
|
||||
t *testing.T,
|
||||
slots map[string]dnfrepo.ItemStack,
|
||||
) (*Service, *gameSession, dnfrepo.Group, *bufferConn) {
|
||||
t.Helper()
|
||||
service, session, repositories, connection := newPremiumServiceTestRuntime(t)
|
||||
account, found, err := repositories.Account.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load account found=%t err=%v", found, err)
|
||||
}
|
||||
premium.Upsert(&account, premium.TypeCrystal, int64(time.Hour/time.Second), 1, time.Now().UTC())
|
||||
if err := repositories.Account.Save(context.Background(), account); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(context.Background(), dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "account-1",
|
||||
Slots: slots,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
service.premiumCatalog = ¤tPremiumCatalog{
|
||||
contractsByItem: make(map[int64]currentPremiumContractInfo),
|
||||
devilSlots: make(map[uint32]currentPremiumDevilSlotInfo),
|
||||
crystalCubeIDs: [6]int64{3033, 3034, 3035, 3036, 3037, 3262},
|
||||
}
|
||||
return service, session, repositories, connection
|
||||
}
|
||||
|
||||
func nextCrystalPacket(t *testing.T, data []byte) (dnfproto.ChannelPacket, []byte) {
|
||||
t.Helper()
|
||||
return splitGameServerUpperPacket(t, data)
|
||||
}
|
||||
|
||||
func assertCurrentCrystalStatePacket(t *testing.T, packet dnfproto.ChannelPacket, wantBody []byte) {
|
||||
t.Helper()
|
||||
if packet.Header.Classification != 0 ||
|
||||
packet.Header.MsgID != currentCrystalContractStateMsgID ||
|
||||
!bytes.Equal(packet.Body, wantBody) {
|
||||
t.Fatalf("crystal state header=%+v body=%x, want class0/op898 body=%x", packet.Header, packet.Body, wantBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractSelectionSendsOp535AckThenOp898State(t *testing.T) {
|
||||
service, session, _, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 12},
|
||||
})
|
||||
if err := service.handleCurrentCrystalContractUpdate(session, []byte{0, 4}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ack, rest := nextCrystalPacket(t, connection.write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketUpdateContractOfCubeInfo) ||
|
||||
!bytes.Equal(ack.Body, []byte{1, 0, 4}) {
|
||||
t.Fatalf("op535 packet header=%+v body=%x", ack.Header, ack.Body)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, rest)
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 4})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("op898 packet header=%+v body=%x trailing=%x", state.Header, state.Body, trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractExplicitDeselectPersistsAndPushesNoneState(t *testing.T) {
|
||||
service, session, repositories, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:354": {ItemID: 3033, Count: 12},
|
||||
})
|
||||
if err := service.handleCurrentCrystalContractUpdate(session, []byte{0, 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection.write.Reset()
|
||||
if err := service.handleCurrentCrystalContractUpdate(session, []byte{0, 0xff}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ack, rest := nextCrystalPacket(t, connection.write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketUpdateContractOfCubeInfo) ||
|
||||
!bytes.Equal(ack.Body, []byte{1, 0, 0xff}) {
|
||||
t.Fatalf("op535 deselect ACK header=%+v body=%x", ack.Header, ack.Body)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, rest)
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 0xff})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("op898 deselected state trailing=%x", trailing)
|
||||
}
|
||||
character, found, err := repositories.Character.Load(context.Background(), "19")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character found=%t err=%v", found, err)
|
||||
}
|
||||
if got := character.Stats["premium_crystal_selection"]; got != -1 {
|
||||
t.Fatalf("persisted selection=%d, want -1", got)
|
||||
}
|
||||
inventory, found, err := repositories.AccountInventory.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load account inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if got := inventory.Slots["0:354"].Count; got != 12 {
|
||||
t.Fatalf("cube count=%d, want unchanged 12", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyCrystalContractSelectionPersistsAndRestoresAfterRelog(t *testing.T) {
|
||||
service, session, _, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:357": {ItemID: 3036, Count: 12},
|
||||
})
|
||||
if err := service.handleGameCommand(
|
||||
session,
|
||||
byte(dnfenum.GameCmdCommand),
|
||||
uint16(dnfenum.CmdPacketUpdateContractOfCubeInfo),
|
||||
[]byte{0, 3},
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ack, rest := nextCrystalPacket(t, connection.write.Bytes())
|
||||
if ack.Header.Classification != dnfproto.DefaultChannelClassification ||
|
||||
ack.Header.MsgID != uint16(dnfenum.CmdPacketUpdateContractOfCubeInfo) ||
|
||||
!bytes.Equal(ack.Body, []byte{1, 0, 3}) {
|
||||
t.Fatalf("legacy op535 packet header=%+v body=%x", ack.Header, ack.Body)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, rest)
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 3})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("legacy op898 packet header=%+v body=%x trailing=%x", state.Header, state.Body, trailing)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
relogged := &gameSession{
|
||||
conn: connection,
|
||||
connID: "crystal-contract-relogged",
|
||||
selectedCharacterID: session.selectedCharacterID,
|
||||
accountID: session.accountID,
|
||||
}
|
||||
if err := service.sendCurrentCrystalContractState(relogged, "test_relog"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
restored, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
assertCurrentCrystalStatePacket(t, restored, []byte{0, 3})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("restored op898 packet header=%+v body=%x trailing=%x", restored.Header, restored.Body, trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractInitialBootstrapSendsOnceAndOp36IsFallbackPerTownRoute(t *testing.T) {
|
||||
service, session, _, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:357": {ItemID: 3036, Count: 12},
|
||||
})
|
||||
if err := service.sendCurrentCrystalContractStateOnce(
|
||||
session,
|
||||
"test_after_inventory_bootstrap_before_adventure_mode1",
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.sendCurrentCrystalContractTownUIReadyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 0xff})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("town UI-ready state header=%+v body=%x trailing=%x", state.Header, state.Body, trailing)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
service.armCurrentInitialTownRoute(session, session.selectedCharacterID)
|
||||
if err := service.sendCurrentCrystalContractTownUIReadyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rearmed, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
assertCurrentCrystalStatePacket(t, rearmed, []byte{0, 0xff})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("rearmed town UI-ready state header=%+v body=%x trailing=%x", rearmed.Header, rearmed.Body, trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeferredSelectSceneTailReplaysPremiumAndLeavesCrystalForTownUIReadyBoundary(t *testing.T) {
|
||||
service, session, _, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 12},
|
||||
})
|
||||
session.sceneBootstrapTailDeferred = true
|
||||
session.initialTownRouteCharacterID = session.selectedCharacterID
|
||||
session.initialTownRouteStage = currentInitialTownRoutePlayerStateSent
|
||||
session.initialTownLegacySceneReadyAccepted = true
|
||||
|
||||
if err := service.sendDeferredSelectSceneTail(session, "test_scene_ready"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !session.sceneBootstrapTailSent || session.sceneBootstrapTailDeferred {
|
||||
t.Fatalf("scene tail flags sent=%t deferred=%t", session.sceneBootstrapTailSent, session.sceneBootstrapTailDeferred)
|
||||
}
|
||||
|
||||
premiumCount := 0
|
||||
crystalCount := 0
|
||||
for stream := connection.write.Bytes(); len(stream) > 0; {
|
||||
packet, rest := splitCurrentGameServerUpperPacketAuto(t, stream)
|
||||
stream = rest
|
||||
switch {
|
||||
case packet.Header.Classification == dnfproto.DefaultChannelClassification &&
|
||||
packet.Header.MsgID == uint16(dnfenum.CmdPacketPremiumService):
|
||||
premiumCount++
|
||||
case packet.Header.Classification == 0 &&
|
||||
packet.Header.MsgID == currentCrystalContractStateMsgID:
|
||||
crystalCount++
|
||||
}
|
||||
}
|
||||
if premiumCount != 1 || crystalCount != 0 {
|
||||
t.Fatalf("scene tail premium/crystal packets=%d/%d, want 1/0", premiumCount, crystalCount)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.sendCurrentCrystalContractTownUIReadyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 0xff})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("town UI-ready state header=%+v body=%x trailing=%x", state.Header, state.Body, trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func newCurrentCrystalContractDungeonTimerTest(
|
||||
t *testing.T,
|
||||
cubeCount int64,
|
||||
) (*Service, *gameSession, *runtimeDungeonState, dnfrepo.Group, *bufferConn, *fakeCurrentDungeonDeathTimerQueue) {
|
||||
t.Helper()
|
||||
service, session, runtime := newBackToVillageRuntime(t)
|
||||
repositories, ok := service.repositoryGroup()
|
||||
if !ok {
|
||||
t.Fatal("repository group unavailable")
|
||||
}
|
||||
account, found, err := repositories.Account.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load account found=%t err=%v", found, err)
|
||||
}
|
||||
premium.Upsert(&account, premium.TypeCrystal, int64(time.Hour/time.Second), 1, time.Now().UTC())
|
||||
if err := repositories.Account.Save(context.Background(), account); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(context.Background(), dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "account-1",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
dnfrepo.AccountSharedInventorySlotKey(354): {ItemID: 3033, Count: cubeCount},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := service.handleCurrentCrystalContractUpdate(session, []byte{0, 0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := session.conn.(*bufferConn)
|
||||
connection.write.Reset()
|
||||
queue := newFakeCurrentDungeonDeathTimerQueue()
|
||||
service.gameplayTimers = queue
|
||||
return service, session, runtime, repositories, connection, queue
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractDungeonTimerConsumesAndRefreshesAccountCube(t *testing.T) {
|
||||
service, session, runtime, repositories, connection, queue := newCurrentCrystalContractDungeonTimerTest(t, 2)
|
||||
if err := service.armCurrentCrystalContractTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
task := queue.task(t, 0)
|
||||
if task.delay != currentCrystalContractConsumeInterval {
|
||||
t.Fatalf("timer delay=%s", task.delay)
|
||||
}
|
||||
queue.fire(task, false)
|
||||
|
||||
inventory, found, err := repositories.AccountInventory.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load account inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if got := inventory.Slots[dnfrepo.AccountSharedInventorySlotKey(354)].Count; got != 1 {
|
||||
t.Fatalf("cube count=%d, want 1", got)
|
||||
}
|
||||
packet, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
||||
packet.Body[0] != dnfrepo.MainInventoryListType || binary.LittleEndian.Uint16(packet.Body[1:3]) != 1 ||
|
||||
binary.LittleEndian.Uint16(packet.Body[3:5]) != 354 || binary.LittleEndian.Uint32(packet.Body[5:9]) != 3033 ||
|
||||
binary.LittleEndian.Uint32(packet.Body[9:13]) != 1 || len(trailing) != 0 {
|
||||
t.Fatalf("cube op14 header=%+v body=%x trailing=%x", packet.Header, packet.Body, trailing)
|
||||
}
|
||||
scheduled, _, active := queue.counts()
|
||||
if scheduled != 2 || active != 1 || !runtime.crystalContractTimerActive {
|
||||
t.Fatalf("timer scheduled=%d active=%d runtime_active=%t", scheduled, active, runtime.crystalContractTimerActive)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractDungeonTimerLastCubeDeselectsAndStops(t *testing.T) {
|
||||
service, session, runtime, repositories, connection, queue := newCurrentCrystalContractDungeonTimerTest(t, 1)
|
||||
if err := service.armCurrentCrystalContractTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
queue.fire(queue.task(t, 0), false)
|
||||
first, rest := nextCrystalPacket(t, connection.write.Bytes())
|
||||
if first.Header.Classification != 0 || first.Header.MsgID != uint16(dnfenum.CmdPacketWalkoutPartyMember) ||
|
||||
binary.LittleEndian.Uint32(first.Body[5:9]) != 0 {
|
||||
t.Fatalf("last-cube clear op14 header=%+v body=%x", first.Header, first.Body)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, rest)
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 0xff})
|
||||
if len(trailing) != 0 || runtime.crystalContractTimerActive {
|
||||
t.Fatalf("last-cube trailing=%x timer_active=%t", trailing, runtime.crystalContractTimerActive)
|
||||
}
|
||||
inventory, found, err := repositories.AccountInventory.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load account inventory found=%t err=%v", found, err)
|
||||
}
|
||||
if _, exists := inventory.Slots[dnfrepo.AccountSharedInventorySlotKey(354)]; exists {
|
||||
t.Fatal("last cube remained in account inventory")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentCrystalContractPracticeDungeonDoesNotArmOrConsume(t *testing.T) {
|
||||
service, session, runtime, repositories, connection, queue := newCurrentCrystalContractDungeonTimerTest(t, 2)
|
||||
runtime.ProtocolMode = currentDungeonProtocolModePractice
|
||||
if err := service.armCurrentCrystalContractTimer(session, runtime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if scheduled, _, active := queue.counts(); scheduled != 0 || active != 0 {
|
||||
t.Fatalf("practice timer scheduled=%d active=%d", scheduled, active)
|
||||
}
|
||||
inventory, found, err := repositories.AccountInventory.Load(context.Background(), "account-1")
|
||||
if err != nil || !found || inventory.Slots[dnfrepo.AccountSharedInventorySlotKey(354)].Count != 2 {
|
||||
t.Fatalf("practice inventory found=%t count=%d err=%v", found, inventory.Slots[dnfrepo.AccountSharedInventorySlotKey(354)].Count, err)
|
||||
}
|
||||
if connection.write.Len() != 0 {
|
||||
t.Fatalf("practice timer wrote=%x", connection.write.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTownAreaReadyReplaysPersistedCrystalSelectionWithoutReplacingMainInventory(t *testing.T) {
|
||||
service, session, repositories := newTownMoveTest(t)
|
||||
session.accountID = "dnf:1"
|
||||
service.options.accountID = "dnf:1"
|
||||
service.premiumCatalog = ¤tPremiumCatalog{
|
||||
contractsByItem: make(map[int64]currentPremiumContractInfo),
|
||||
devilSlots: make(map[uint32]currentPremiumDevilSlotInfo),
|
||||
crystalCubeIDs: [6]int64{3033, 3034, 3035, 3036, 3037, 3262},
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
account := dnfrepo.AccountRecord{
|
||||
AccountID: "dnf:1",
|
||||
Metadata: make(map[string]string),
|
||||
}
|
||||
premium.Upsert(&account, premium.TypeCrystal, int64(time.Hour/time.Second), 1, now)
|
||||
if err := repositories.Account.Save(context.Background(), account); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(context.Background(), dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "dnf:1",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 257},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{
|
||||
CharacterID: "29",
|
||||
Slots: map[string]dnfrepo.ItemStack{},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
character, found, err := repositories.Character.Load(context.Background(), "29")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load character found=%t err=%v", found, err)
|
||||
}
|
||||
character.Stats["premium_crystal_selection"] = 4
|
||||
if err := repositories.Character.Save(context.Background(), character); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := service.handleTownSetUserArea(session, buildTownMoveRequest(38, 0, 900, 250, 5)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
townPacket, rest := splitTownTransitionAndPostState(
|
||||
t,
|
||||
session.conn.(*bufferConn).write.Bytes(),
|
||||
session,
|
||||
29,
|
||||
38,
|
||||
0,
|
||||
900,
|
||||
250,
|
||||
5,
|
||||
3,
|
||||
townMoveSkillProjectionBody(t, repositories, "29"),
|
||||
false,
|
||||
)
|
||||
if townPacket.Header.MsgID != currentSceneTransitionMsgID {
|
||||
t.Fatalf("first packet msg=%d, want town transition %d", townPacket.Header.MsgID, currentSceneTransitionMsgID)
|
||||
}
|
||||
statePacket, trailing := nextCrystalPacket(t, rest)
|
||||
assertCurrentCrystalStatePacket(t, statePacket, []byte{0, 4})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("late crystal state header=%+v body=%x trailing=%x", statePacket.Header, statePacket.Body, trailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPremiumGameplayModuleRegistersCrystalContractRoutes(t *testing.T) {
|
||||
module := premiumGameplayModule()
|
||||
for _, opcode := range []uint16{
|
||||
uint16(dnfenum.CmdPacketUpdateContractOfCubeInfo),
|
||||
uint16(dnfenum.CmdPacketUseLimitCube),
|
||||
} {
|
||||
if module.LegacyHandlers[opcode] == nil {
|
||||
t.Fatalf("premium module missing legacy opcode %d", opcode)
|
||||
}
|
||||
if module.UpperHandlers[opcode] == nil {
|
||||
t.Fatalf("premium module missing upper opcode %d", opcode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeferredSelectSceneTailRestoresPremiumStateWithoutCrystalState(t *testing.T) {
|
||||
service, session, repositories, connection := newCrystalContractTestRuntime(t, map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 12},
|
||||
})
|
||||
account, found, err := repositories.Account.Load(context.Background(), "account-1")
|
||||
if err != nil || !found {
|
||||
t.Fatalf("load premium account found=%t err=%v", found, err)
|
||||
}
|
||||
premium.Upsert(&account, premium.DevilSlotType(premium.DevilSlotGoldCard), int64(7*24*time.Hour/time.Second), 1, time.Now().UTC())
|
||||
if err := repositories.Account.Save(context.Background(), account); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
session.sceneBootstrapTailDeferred = true
|
||||
session.initialTownRouteCharacterID = session.selectedCharacterID
|
||||
session.initialTownRouteStage = currentInitialTownRoutePlayerStateSent
|
||||
session.initialTownLegacySceneReadyAccepted = true
|
||||
|
||||
if err := service.sendDeferredSelectSceneTail(session, "test_scene_ready"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !session.sceneBootstrapTailSent || session.sceneBootstrapTailDeferred {
|
||||
t.Fatalf("scene tail flags sent=%t deferred=%t", session.sceneBootstrapTailSent, session.sceneBootstrapTailDeferred)
|
||||
}
|
||||
|
||||
premiumCount := 0
|
||||
var premiumBody []byte
|
||||
crystalCount := 0
|
||||
for stream := connection.write.Bytes(); len(stream) > 0; {
|
||||
packet, rest := splitCurrentGameServerUpperPacketAuto(t, stream)
|
||||
stream = rest
|
||||
switch {
|
||||
case packet.Header.Classification == dnfproto.DefaultChannelClassification &&
|
||||
packet.Header.MsgID == uint16(dnfenum.CmdPacketPremiumService):
|
||||
premiumCount++
|
||||
premiumBody = append([]byte(nil), packet.Body...)
|
||||
case packet.Header.Classification == 0 &&
|
||||
packet.Header.MsgID == currentCrystalContractStateMsgID:
|
||||
crystalCount++
|
||||
}
|
||||
}
|
||||
if premiumCount != 1 || crystalCount != 0 {
|
||||
t.Fatalf("scene tail premium/crystal packets=%d/%d, want 1/0", premiumCount, crystalCount)
|
||||
}
|
||||
const goldCardExpiryOffset = 3 + 6 + premium.DevilSlotGoldCard*9
|
||||
if len(premiumBody) != 77 || binary.LittleEndian.Uint32(premiumBody[goldCardExpiryOffset:goldCardExpiryOffset+4]) == 0 {
|
||||
t.Fatalf("scene tail premium body=%x, want active gold-card deadline", premiumBody)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.sendCurrentCrystalContractTownUIReadyState(session); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, trailing := nextCrystalPacket(t, connection.write.Bytes())
|
||||
assertCurrentCrystalStatePacket(t, state, []byte{0, 0xff})
|
||||
if len(trailing) != 0 {
|
||||
t.Fatalf("town UI-ready state header=%+v body=%x trailing=%x", state.Header, state.Body, trailing)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user