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

532 lines
23 KiB
Go

package dnfbridge
import (
"bytes"
"context"
"encoding/binary"
"testing"
"time"
darkcombo "longheng.io/server/internal/modules/dnf/darkknightcombo"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
dnfrepomemory "longheng.io/server/internal/modules/dnf/repository/memory"
dnfskill "longheng.io/server/internal/modules/dnf/skill"
)
func TestFinishLoadingStatusThenMainCharacterStateUsesRealRecord(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
character := dnfrepo.CharacterRecord{
CharacterID: "77",
AccountID: "account-1",
Job: "15",
Level: 7,
Stats: map[string]int64{
"exp": 1234,
"stat_hp_max": 1700,
"stat_mp_max": 1500,
"stat_inventory_limit": 450000,
"stat_weight": 500000,
"sp_style_0": 11,
"sp_style_1": 12,
"tp_style_0": 13,
"tp_style_1": 14,
"finish_loading_currency_slot2_total": 99,
"finish_loading_exp_category_1": 21,
"finish_loading_independent_scalar": 22,
// These obsolete generic keys deliberately conflict with the
// valid neutral HonorExpert state. They must not reach op37.
"finish_loading_auxiliary_state_id": 23,
"finish_loading_auxiliary_state_value_0": 24,
"finish_loading_auxiliary_state_value_1": 25,
},
}
if err := repositories.Character.Save(context.Background(), character); err != nil {
t.Fatal(err)
}
if err := repositories.Inventory.Save(context.Background(), dnfrepo.InventoryRecord{CharacterID: "77", Slots: map[string]dnfrepo.ItemStack{}}); err != nil {
t.Fatal(err)
}
if err := repositories.Equipment.Save(context.Background(), dnfrepo.EquipmentRecord{
CharacterID: "77",
Entries: map[string]dnfrepo.EquipmentEntry{
"11": {
SlotIndex: 11,
ItemID: 116000011,
RawEntry: buildInitialEquipmentRawEntry(11, 116000011, 45),
Extra: map[string]string{"source": "pvf_create_equipment_list"},
},
},
}); err != nil {
t.Fatal(err)
}
if err := repositories.Skill.Save(context.Background(), dnfrepo.SkillRecord{
CharacterID: "77",
Points: dnfrepo.SkillPointState{
TotalSP: 11, RemainingSP: 11,
TotalTP: 14, RemainingTP: 14,
SyncedLevel: 7,
},
Skills: map[int64]dnfrepo.SkillState{
1: {Level: 1, Enabled: true},
},
}); err != nil {
t.Fatal(err)
}
if err := repositories.Settings.Save(context.Background(), dnfrepo.SettingsRecord{
Scope: dnfrepo.CharacterContainerStateScope("77"),
Values: map[string]string{
"source": "current_exe_86jp_op13_container_state",
"main_list_param16": "24",
"avatar_list_param16": "0",
"personal_cargo_list_param16": "8",
},
}); err != nil {
t.Fatal(err)
}
skillCatalog, err := buildSkillCatalogFromSource(context.Background(), initialEquipmentMemSource{
"skill/skilllist.lst": "15 `job15.lst`\n",
"skill/job15.lst": "1 `job15/one.skl`\n",
"skill/job15/one.skl": "[skill type]\n`active`\n[skill class]\n1\n[required level]\n1\n",
"character/character.lst": "15 `job15.chr`\n",
"character/job15.chr": "[job]\n`[gunblader]`\n",
})
if err != nil {
t.Fatal(err)
}
service := &Service{
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
initialSkillsByJob: map[byte][]initialSkillEntry{15: {{SkillID: 1, Level: 1}}},
initialSPTable: map[int]int{1: 11},
initialTPTable: map[int]int{1: 14},
skillCatalog: skillCatalog,
}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
selectedCharacterID: 77,
postStartMapPlayerStateSent: true,
townActorOwnerChannel: 16,
deferredDungeonUserStateObjectKey: 77,
}
if err := service.sendFinishLoadingStatus(session, make([]byte, 16)); err != nil {
t.Fatal(err)
}
status, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if status.Header.Classification != dnfproto.DefaultChannelClassification || status.Header.MsgID != uint16(dnfenum.CmdPacketFinishLoading) || !bytes.Equal(status.Body, []byte{1}) {
t.Fatalf("finish-loading status=%+v body=%x", status.Header, status.Body)
}
state, rest := splitGameServerUpperPacket(t, rest)
if state.Header.Classification != 0 || state.Header.MsgID != uint16(dnfenum.CmdPacketFinishLoading) || len(state.Body) != currentFinishLoadingCharacterStateBodySize {
t.Fatalf("finish-loading state=%+v body_len=%d", state.Header, len(state.Body))
}
if state.Body[0] != 7 || binary.LittleEndian.Uint32(state.Body[1:5]) != 1234 || binary.LittleEndian.Uint16(state.Body[9:11]) != 11 || binary.LittleEndian.Uint16(state.Body[15:17]) != 14 {
t.Fatalf("finish-loading core fields=%x", state.Body[:17])
}
if binary.LittleEndian.Uint32(state.Body[17:21]) != 99 || binary.LittleEndian.Uint32(state.Body[21:25]) != 21 || binary.LittleEndian.Uint32(state.Body[34:38]) != 22 || state.Body[0x2e] != 0 {
t.Fatalf("finish-loading scalar fields=%x", state.Body)
}
if !bytes.Equal(state.Body[55:67], make([]byte, 12)) {
t.Fatalf("finish-loading HonorExpert state=%x, want neutral 0/0", state.Body[55:67])
}
commit, rest := splitGameServerUpperPacket(t, rest)
if commit.Header.Classification != 0 || commit.Header.MsgID != currentIncreaseStatusResultMsgID || !bytes.Equal(commit.Body, make([]byte, currentIncreaseStatusResultBodySize)) {
t.Fatalf("increase-status result=%+v body=%x", commit.Header, commit.Body)
}
skills, rest := splitGameServerUpperPacket(t, rest)
if skills.Header.Classification != 0 || skills.Header.MsgID != currentSkillInfoMsgID || len(skills.Body) < 4 || int(binary.LittleEndian.Uint32(skills.Body[:4])) != len(skills.Body)-4 {
t.Fatalf("post-finish-loading skills=%+v body=%x", skills.Header, skills.Body)
}
outerVarints, outerMessages := consumeCurrentSkillInfoFields(t, skills.Body[4:])
if got := outerVarints[1]; len(got) != 1 || got[0] != currentSkillInfoMessageType || len(outerMessages[2]) != currentSkillInfoTreeCount {
t.Fatalf("post-finish-loading skill fields=%v trees=%d", outerVarints, len(outerMessages[2]))
}
expertJob, rest := splitGameServerUpperPacket(t, rest)
if expertJob.Header.Classification != 0 || expertJob.Header.MsgID != currentExpertJobInfoNotification ||
!bytes.Equal(expertJob.Body, currentExpertJobClearedBody(0)) {
t.Fatalf("post-finish-loading expert-job state=%+v body=%x", expertJob.Header, expertJob.Body)
}
placement, rest := splitGameServerUpperPacket(t, rest)
if placement.Header.Classification != 0 || placement.Header.MsgID != uint16(dnfenum.CmdPacketRequestBlacklist) || !bytes.Equal(placement.Body, []byte{0, 0}) {
t.Fatalf("post-finish-loading placement (without a second mode3)=%+v body=%x", placement.Header, placement.Body)
}
userState, rest := splitGameServerUpperPacket(t, rest)
wantUserState, err := buildCurrentDungeonUserStateBody(77)
if err != nil {
t.Fatal(err)
}
if userState.Header.Classification != 0 ||
userState.Header.MsgID != uint16(dnfenum.CmdPacketNotifyUserState) ||
!bytes.Equal(userState.Body, wantUserState) {
t.Fatalf("post-finish-loading deferred user state=%+v body=%x want=%x", userState.Header, userState.Body, wantUserState)
}
if len(rest) != 0 || !session.currentFinishLoadingStateSent || !session.currentFinishLoadingCompletionSent {
t.Fatalf("finish-loading trailing=%x state=%v completion=%v",
rest, session.currentFinishLoadingStateSent, session.currentFinishLoadingCompletionSent)
}
if !session.postFinishLoadingPlayerStateSent {
t.Fatal("postFinishLoadingPlayerStateSent = false")
}
if session.deferredDungeonUserStateObjectKey != 0 {
t.Fatalf("deferredDungeonUserStateObjectKey = %d, want cleared", session.deferredDungeonUserStateObjectKey)
}
townConnection := &bufferConn{}
townSession := &gameSession{
conn: townConnection,
selectedCharacterID: 77,
initialTownRouteCharacterID: 77,
initialTownRouteStage: currentInitialTownRoutePlayerStateSent,
initialTownSkillInfoSent: true,
}
if err := service.sendFinishLoadingStatus(townSession, make([]byte, 16)); err != nil {
t.Fatal(err)
}
townStatus, townRest := splitGameServerUpperPacket(t, townConnection.write.Bytes())
if townStatus.Header.Classification != dnfproto.DefaultChannelClassification ||
townStatus.Header.MsgID != uint16(dnfenum.CmdPacketFinishLoading) ||
!bytes.Equal(townStatus.Body, []byte{1}) {
t.Fatalf("town finish-loading status=%+v body=%x", townStatus.Header, townStatus.Body)
}
townCompletion, townRest := splitGameServerUpperPacket(t, townRest)
if townCompletion.Header.Classification != 0 || townCompletion.Header.MsgID != currentIncreaseStatusResultMsgID ||
!bytes.Equal(townCompletion.Body, make([]byte, currentIncreaseStatusResultBodySize)) {
t.Fatalf("town finish-loading completion=%+v body=%x", townCompletion.Header, townCompletion.Body)
}
townPlacement, townRest := splitGameServerUpperPacket(t, townRest)
if townPlacement.Header.Classification != 0 || townPlacement.Header.MsgID != uint16(dnfenum.CmdPacketRequestBlacklist) ||
!bytes.Equal(townPlacement.Body, buildCurrentSceneActorPlacementBody()) || len(townRest) != 0 {
t.Fatalf("town finish-loading placement=%+v body=%x rest=%x", townPlacement.Header, townPlacement.Body, townRest)
}
if !townSession.currentFinishLoadingStateSent || !townSession.currentFinishLoadingCompletionSent ||
!townSession.initialTownSkillInfoSent || !townSession.postFinishLoadingPlayerStateSent {
t.Fatalf("town finish-loading gates current=%t completion=%t skill=%t post=%t",
townSession.currentFinishLoadingStateSent,
townSession.currentFinishLoadingCompletionSent,
townSession.initialTownSkillInfoSent,
townSession.postFinishLoadingPlayerStateSent)
}
if townSession.initialTownSkillInfoPrepared || len(townSession.initialTownSkillInfo.body) != 0 {
t.Fatal("town finish-loading created a duplicate initial skill projection")
}
for stream := townConnection.write.Bytes(); len(stream) > 0; {
packet, rest := splitGameServerUpperPacket(t, stream)
stream = rest
if packet.Header.Classification == 0 &&
packet.Header.MsgID == uint16(dnfenum.CmdPacketFinishLoading) {
t.Fatalf("town finish-loading replayed forbidden class0/op37 state body_len=%d", len(packet.Body))
}
}
}
func TestFinishLoadingStatusAfterCompletedDungeonReturnAcksWithoutMainState(t *testing.T) {
connection := &bufferConn{}
session := &gameSession{
conn: connection,
selectedCharacterID: 77,
returnTownFinishLoadingAckOnly: true,
}
service := &Service{}
if err := service.sendFinishLoadingStatus(session, make([]byte, currentFinishLoadingLegacyRequestBodySize)); err != nil {
t.Fatalf("return-town finish-loading status: %v", err)
}
status, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if status.Header.Classification != dnfproto.DefaultChannelClassification ||
status.Header.MsgID != uint16(dnfenum.CmdPacketFinishLoading) ||
!bytes.Equal(status.Body, []byte{1}) || len(rest) != 0 {
t.Fatalf("return-town status=%+v body=%x trailing=%x", status.Header, status.Body, rest)
}
if session.currentFinishLoadingStateSent || session.currentFinishLoadingCompletionSent || session.postFinishLoadingPlayerStateSent {
t.Fatalf("return-town op37 emitted a state path: state=%t completion=%t post=%t",
session.currentFinishLoadingStateSent,
session.currentFinishLoadingCompletionSent,
session.postFinishLoadingPlayerStateSent)
}
}
func TestPostFinishLoadingSkipsDungeonSkillReplayAfterSceneInitialization(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
CharacterID: "77",
AccountID: "account-1",
Job: "15",
Level: 7,
Stats: map[string]int64{"exp": 1234},
}); err != nil {
t.Fatal(err)
}
service := &Service{
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
selectedCharacterID: 77,
dungeonSceneSkillInfoSent: true,
}
if err := service.sendCurrentPostFinishLoadingPlayerState(session, "test_dungeon_skill_already_initialized"); err != nil {
t.Fatal(err)
}
expertJob, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if expertJob.Header.Classification != 0 || expertJob.Header.MsgID != currentExpertJobInfoNotification {
t.Fatalf("post-finish dungeon expert-job state=%+v body=%x", expertJob.Header, expertJob.Body)
}
packet, rest := splitGameServerUpperPacket(t, rest)
if packet.Header.Classification != 0 || packet.Header.MsgID != uint16(dnfenum.CmdPacketRequestBlacklist) ||
!bytes.Equal(packet.Body, buildCurrentSceneActorPlacementBody()) || len(rest) != 0 {
t.Fatalf("post-finish dungeon placement=%+v body=%x rest=%x", packet.Header, packet.Body, rest)
}
}
func TestPostFinishLoadingRestoresDarkKnightNativeComboAfterSceneInitialization(t *testing.T) {
repositories := dnfrepomemory.NewMemoryGroup()
character := dnfrepo.CharacterRecord{CharacterID: "77", AccountID: "account-1", Job: "9", Level: 7}
if err := repositories.Character.Save(context.Background(), character); err != nil {
t.Fatal(err)
}
if err := repositories.Skill.Save(context.Background(), currentDarkKnightComboNativeTestRecord("77")); err != nil {
t.Fatal(err)
}
pages := []darkcombo.Page{{Index: 0, Roots: []darkcombo.Root{{SkillID: 118, Children: []uint16{46}}}}}
if err := darkcombo.SavePages(context.Background(), repositories.Settings, "77", pages, time.Unix(1, 0)); err != nil {
t.Fatal(err)
}
connection := &bufferConn{}
service := &Service{
options: options{accountID: "account-1", gameUpperBodyCodec: gameUpperBodyCodecPlain},
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
skillCatalog: &dnfskill.Table{},
}
session := &gameSession{
conn: connection,
accountID: "account-1",
selectedCharacterID: 77,
dungeonSceneSkillInfoSent: true,
}
if err := service.sendCurrentPostFinishLoadingPlayerState(session, "test_dark_knight_final_scene_combo_restore"); err != nil {
t.Fatal(err)
}
expertJob, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if expertJob.Header.Classification != 0 || expertJob.Header.MsgID != currentExpertJobInfoNotification {
t.Fatalf("expert-job header=%+v", expertJob.Header)
}
combo, rest := splitGameServerUpperPacket(t, rest)
if combo.Header.Classification != 0 || combo.Header.MsgID != currentDarkKnightComboInitializeMsgID {
t.Fatalf("combo header=%+v", combo.Header)
}
wantCombo, err := darkcombo.EncodeNotificationBody(pages)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(combo.Body, wantCombo) {
t.Fatalf("combo body=%x want=%x", combo.Body, wantCombo)
}
placement, rest := splitGameServerUpperPacket(t, rest)
if placement.Header.Classification != 0 || placement.Header.MsgID != uint16(dnfenum.CmdPacketRequestBlacklist) ||
!bytes.Equal(placement.Body, buildCurrentSceneActorPlacementBody()) || len(rest) != 0 {
t.Fatalf("placement=%+v body=%x rest=%x", placement.Header, placement.Body, rest)
}
}
func TestBuildCurrentFinishLoadingCharacterStateUsesSkillLedgerForBothTrees(t *testing.T) {
body := buildCurrentFinishLoadingCharacterStateBody(
dnfrepo.CharacterRecord{Level: 90, Stats: map[string]int64{
"sp_style_0": 1,
"sp_style_1": 2,
"tp_style_0": 3,
"tp_style_1": 4,
}},
dnfrepo.SkillPointState{RemainingSP: 11970, RemainingTP: 41},
)
if len(body) != currentFinishLoadingCharacterStateBodySize {
t.Fatalf("body length=%d, want %d", len(body), currentFinishLoadingCharacterStateBodySize)
}
if got := binary.LittleEndian.Uint16(body[9:11]); got != 11970 {
t.Fatalf("tree0 SP=%d, want 11970", got)
}
if got := binary.LittleEndian.Uint16(body[11:13]); got != 11970 {
t.Fatalf("tree1 SP=%d, want 11970", got)
}
if got := binary.LittleEndian.Uint16(body[13:15]); got != 41 {
t.Fatalf("tree0 TP=%d, want 41", got)
}
if got := binary.LittleEndian.Uint16(body[15:17]); got != 41 {
t.Fatalf("tree1 TP=%d, want 41", got)
}
}
func TestBuildCurrentFinishLoadingCharacterStatePresentsGrowthContractWithoutFatigueBurn(t *testing.T) {
body := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 20, Stats: map[string]int64{
"exp": 240,
"finish_loading_exp_category_3": 999,
"finish_loading_exp_category_4": 888,
}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{GrowthContractBonus: 40},
)
if len(body) != currentFinishLoadingCharacterStateBodySize {
t.Fatalf("body length=%d, want %d", len(body), currentFinishLoadingCharacterStateBodySize)
}
if got := binary.LittleEndian.Uint32(body[30:34]); got != 40 {
t.Fatalf("growth-contract bonus at +0x1e=%d, want 40", got)
}
if got := binary.LittleEndian.Uint32(body[38:42]); got != 888 {
t.Fatalf("fatigue-burn bonus at +0x26=%d, want persisted 888", got)
}
zero := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 20, Stats: map[string]int64{
"finish_loading_exp_category_3": 999,
}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{},
)
if got := binary.LittleEndian.Uint32(zero[30:34]); got != 0 {
t.Fatalf("zero presentation retained persisted growth bonus=%d", got)
}
}
func TestBuildCurrentFinishLoadingCharacterStatePresentsFatigueBatteryAndGrowth(t *testing.T) {
for _, bonus := range []uint32{62, 0} {
body := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 7, Stats: map[string]int64{
"exp": 325, "finish_loading_exp_category_1": 999,
}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{FatigueBatteryBonus: bonus, GrowthContractBonus: 54},
)
if len(body) != currentFinishLoadingCharacterStateBodySize {
t.Fatalf("body length=%d", len(body))
}
total := binary.LittleEndian.Uint32(body[1:5])
fatigue := binary.LittleEndian.Uint32(body[21:25])
growth := binary.LittleEndian.Uint32(body[30:34])
if total != 325 || fatigue != bonus || growth != 54 || total-fatigue-growth != 271-bonus {
t.Fatalf("total=%d fatigue=%d growth=%d, want 325/%d/54", total, fatigue, growth, bonus)
}
if binary.LittleEndian.Uint32(body[17:21]) != 0 || body[25] != 0 || binary.LittleEndian.Uint32(body[38:42]) != 0 {
t.Fatalf("fatigue bonus changed adjacent currency, result mode or fatigue burn: %x", body)
}
}
}
func TestBuildCurrentFinishLoadingCharacterStateKeepsBuffParameterOutOfMonsterSettlement(t *testing.T) {
body := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 23, Stats: map[string]int64{
"exp": 1234,
"finish_loading_exp_category_6": 999,
}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{},
)
if len(body) != currentFinishLoadingCharacterStateBodySize {
t.Fatalf("body length=%d, want %d", len(body), currentFinishLoadingCharacterStateBodySize)
}
if got := binary.LittleEndian.Uint32(body[47:51]); got != 999 {
t.Fatalf("Buff parameter at +0x2f=%d, want persisted 999", got)
}
zero := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 23, Stats: map[string]int64{
"finish_loading_exp_category_6": 999,
}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{},
)
if got := binary.LittleEndian.Uint32(zero[47:51]); got != 999 {
t.Fatalf("zero presentation retained persisted Buff parameter=%d", got)
}
}
func TestBuildCurrentFinishLoadingCharacterStateUsesPresentedTotalExperience(t *testing.T) {
presented := uint32(1732668403 + 2020)
body := buildCurrentFinishLoadingCharacterStateBodyWithPresentation(
dnfrepo.CharacterRecord{Level: 90, Stats: map[string]int64{"exp": 1828454604}},
dnfrepo.SkillPointState{},
&currentFinishLoadingExperiencePresentation{TotalExperience: &presented},
)
if got := binary.LittleEndian.Uint32(body[1:5]); got != presented {
t.Fatalf("presented total experience=%d, want %d", got, presented)
}
}
func TestBuildCurrentFinishLoadingCharacterStateMatchesCurrentEXEFixedLayout(t *testing.T) {
body := buildCurrentFinishLoadingCharacterStateBody(
dnfrepo.CharacterRecord{Level: 0x5a, Stats: map[string]int64{
"exp": 0x01020304,
"finish_loading_exp_category_0": 0x11121314,
"finish_loading_currency_slot2_total": 0x21222324,
"finish_loading_exp_category_1": 0x31323334,
"finish_loading_result_flag": 0x45,
"finish_loading_exp_category_2": 0x51525354,
"finish_loading_exp_category_3": 0x61626364,
"finish_loading_independent_scalar": 0x71727374,
"finish_loading_exp_category_4": 0x11121315,
"finish_loading_exp_category_5": 0x21222325,
"finish_loading_exp_category_6": 0x31323335,
"finish_loading_exp_category_7": 0x41424345,
"finish_loading_auxiliary_state_id": 0x51525355,
"finish_loading_auxiliary_state_value_0": 0x61626365,
"finish_loading_auxiliary_state_value_1": 0x71727375,
"finish_loading_exp_category_8": 0x12131415,
"finish_loading_exp_category_9": 0x22232425,
"finish_loading_exp_category_10": 0x32333435,
"finish_loading_exp_category_11": 0x42434445,
}},
dnfrepo.SkillPointState{RemainingSP: 0x1234, RemainingTP: 0x2345},
)
if len(body) != 87 {
t.Fatalf("body length=%d, want 87", len(body))
}
if body[0] != 0x5a || binary.LittleEndian.Uint32(body[1:5]) != 0x01020304 ||
binary.LittleEndian.Uint32(body[5:9]) != 0x11121314 {
t.Fatalf("level/EXP prefix=%x", body[:9])
}
for _, check := range []struct {
offset int
want uint16
}{
{9, 0x1234}, {11, 0x1234}, {13, 0x2345}, {15, 0x2345},
} {
if got := binary.LittleEndian.Uint16(body[check.offset : check.offset+2]); got != check.want {
t.Fatalf("u16 at +0x%02x=%04x, want %04x", check.offset, got, check.want)
}
}
if body[25] != 0x45 || body[46] != 0 {
t.Fatalf("flags result=%02x dynamic_count=%02x", body[25], body[46])
}
for _, check := range []struct {
offset int
want uint32
}{
{17, 0x21222324}, {21, 0x31323334}, {26, 0x51525354},
{30, 0x61626364}, {34, 0x71727374}, {38, 0x11121315},
{42, 0x21222325}, {47, 0x31323335}, {51, 0x41424345},
{67, 0x12131415}, {71, 0}, {75, 0x22232425},
{79, 0x32333435}, {83, 0x42434445},
} {
if got := binary.LittleEndian.Uint32(body[check.offset : check.offset+4]); got != check.want {
t.Fatalf("u32 at +0x%02x=%08x, want %08x", check.offset, got, check.want)
}
}
if !bytes.Equal(body[55:67], make([]byte, 12)) {
t.Fatalf("HonorExpert at +0x37=%x, want neutral 0/0", body[55:67])
}
}
func TestFinishLoadingMainStateIsDeferredWithoutPostMapPlayer(t *testing.T) {
service := &Service{}
connection := &bufferConn{}
session := &gameSession{conn: connection, selectedCharacterID: 77}
if err := service.sendFinishLoadingStatus(session, make([]byte, 16)); err != nil {
t.Fatal(err)
}
status, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
if status.Header.Classification != dnfproto.DefaultChannelClassification || !bytes.Equal(status.Body, []byte{1}) || len(rest) != 0 {
t.Fatalf("deferred finish-loading status=%+v body=%x rest=%x", status.Header, status.Body, rest)
}
}