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,531 @@
|
||||
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{},
|
||||
¤tFinishLoadingExperiencePresentation{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{},
|
||||
¤tFinishLoadingExperiencePresentation{},
|
||||
)
|
||||
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{},
|
||||
¤tFinishLoadingExperiencePresentation{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{},
|
||||
¤tFinishLoadingExperiencePresentation{},
|
||||
)
|
||||
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{},
|
||||
¤tFinishLoadingExperiencePresentation{},
|
||||
)
|
||||
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{},
|
||||
¤tFinishLoadingExperiencePresentation{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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user