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,410 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"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"
|
||||
)
|
||||
|
||||
func questTriggerBridgeCatalog(t *testing.T) *dnfquest.Catalog {
|
||||
t.Helper()
|
||||
source := questListTestSource{
|
||||
dnfquest.DefaultList: "4728 `meet.qst`\n3193 `normal.qst`\n3622 `seek_meet.qst`\n2002 `hell_hunt.qst`\n",
|
||||
"n_quest/meet.qst": "[grade]\n`[epic]`\n[type]\n`[meet npc]`\n",
|
||||
"n_quest/normal.qst": "[grade]\n`[epic]`\n[type]\n`[normal clear]`\n",
|
||||
"n_quest/seek_meet.qst": "[grade]\n`[epic]`\n[type]\n`[seek n meet npc]`\n[int data]\n3037 500 126\n",
|
||||
"n_quest/hell_hunt.qst": "[grade]\n`[side]`\n[type]\n`[hunt enemy]`\n[int data]\n192 1 70034 1 1 193 1 70300 1 1 194 1 70307 1 1\n[reward type]\n`[hell challenge]`\n[reward int data]\n1\n",
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerPersistsBeforeAckAndSuppressesNoopReplay(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
||||
CharacterID: "19", AccountID: defaultAccountPrefix + "1", Job: "11", Level: 90,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "19",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
4728: {Status: "active", ProgressValue: 1},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{
|
||||
conn: connection, connID: "set-quest-trigger-test",
|
||||
channel: channelcatalog.Channel{ID: 19}, selectedCharacterID: 19,
|
||||
}
|
||||
body := []byte{0x21, 0x00, 0x78, 0x12, 0x00, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, ok, err := repositories.Quest.Load(context.Background(), "19")
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("load persisted quest ok=%t err=%v", ok, err)
|
||||
}
|
||||
if got := stored.States[4728].ProgressValue; got != 0 {
|
||||
t.Fatalf("persisted trigger=%d want=0", got)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if ack.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) ||
|
||||
ack.Header.Classification != dnfproto.DefaultChannelClassification {
|
||||
t.Fatalf("ack header=%+v", ack.Header)
|
||||
}
|
||||
if want := []byte{0x01, 0x78, 0x12, 0x00, 0x00, 0x00, 0x00}; !bytes.Equal(ack.Body, want) {
|
||||
t.Fatalf("ack body=%x want=%x", ack.Body, want)
|
||||
}
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("terminal op33 must not be followed by op574: %x", rest)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := connection.write.Bytes(); len(got) != 0 {
|
||||
t.Fatalf("same-session replay wrote packets=%x", got)
|
||||
}
|
||||
|
||||
// A fresh session may be the first op33 after clear-map persistence already
|
||||
// reached active zero. It receives the one transition ACK, but no op574.
|
||||
newConnection := &bufferConn{}
|
||||
newSession := &gameSession{
|
||||
conn: newConnection, connID: "set-quest-trigger-reload-test",
|
||||
channel: channelcatalog.Channel{ID: 19}, selectedCharacterID: 19,
|
||||
}
|
||||
if err := service.handleCurrentSetQuestTrigger(newSession, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
replayedACK, replayedTrailing := splitGameServerUpperPacket(t, newConnection.write.Bytes())
|
||||
if len(replayedTrailing) != 0 || replayedACK.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) ||
|
||||
!bytes.Equal(replayedACK.Body, []byte{0x01, 0x78, 0x12, 0x00, 0x00, 0x00, 0x00}) {
|
||||
t.Fatalf("persisted active-zero transition ACK=%+v trailing=%x", replayedACK, replayedTrailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerScalarRemoveClearsPackedObjectiveWithoutCounterLoop(t *testing.T) {
|
||||
// Live 2026-09-12T13:32:33Z (packet_log conn game-002646): quest 2900 held
|
||||
// the PVF initial trigger 262657 = packTriggerChannels(1, 1, 1) and the
|
||||
// scalar six-byte op33 shape used to subtract one from the whole packed u32.
|
||||
// Each request therefore changed the durable value and the client fed the
|
||||
// changed active-quest snapshot straight back through op33: 3767 identical
|
||||
// requests at ~30 Hz across seven logins (262656 -> 258892) while every town
|
||||
// entry stayed frozen. The scalar shape must answer the satisfied objective
|
||||
// once (terminal zero, no op574) and then stay silent.
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
||||
CharacterID: "20", AccountID: defaultAccountPrefix + "1", Job: "11", Level: 90,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "20",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
2002: {Status: "active", ProgressValue: 1 | 1<<9 | 1<<18},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{
|
||||
conn: connection, connID: "scalar-packed-objective-trigger",
|
||||
channel: channelcatalog.Channel{ID: 20}, selectedCharacterID: 20,
|
||||
}
|
||||
body := []byte{0x21, 0x00, 0xd2, 0x07, 0x00, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, found, err := repositories.Quest.Load(context.Background(), "20")
|
||||
if err != nil || !found || stored.States[2002].ProgressValue != 0 {
|
||||
t.Fatalf("persisted scalar-remove state found=%t err=%v state=%+v", found, err, stored.States[2002])
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if ack.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) ||
|
||||
binary.LittleEndian.Uint32(ack.Body[3:7]) != 0 || len(rest) != 0 {
|
||||
t.Fatalf("scalar-remove terminal ACK=%+v body=%x trailing=%x", ack.Header, ack.Body, rest)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := connection.write.Bytes(); len(got) != 0 {
|
||||
t.Fatalf("replayed scalar op33 wrote packets=%x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerPublishesPackedChannelsWithoutFeedbackPollution(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
||||
CharacterID: "20", AccountID: defaultAccountPrefix + "1", Job: "11", Level: 90,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "20",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3193: {Status: "active", ProgressValue: 513},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{
|
||||
conn: connection, connID: "set-packed-quest-trigger-test",
|
||||
channel: channelcatalog.Channel{ID: 20}, selectedCharacterID: 20,
|
||||
}
|
||||
|
||||
for _, step := range []struct {
|
||||
triggerType byte
|
||||
want uint32
|
||||
}{
|
||||
{triggerType: 0x10, want: 512},
|
||||
{triggerType: 0x20, want: 0},
|
||||
} {
|
||||
before := connection.write.Len()
|
||||
body := []byte{0x21, 0x00, 0x79, 0x0c, step.triggerType, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes()[before:])
|
||||
if ack.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) || binary.LittleEndian.Uint32(ack.Body[3:7]) != step.want {
|
||||
t.Fatalf("type=%#x ACK header=%+v body=%x want trigger=%d", step.triggerType, ack.Header, ack.Body, step.want)
|
||||
}
|
||||
if step.want == 0 {
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("type=%#x terminal op33 trailing=%x", step.triggerType, rest)
|
||||
}
|
||||
continue
|
||||
}
|
||||
active, trailing := splitGameServerUpperPacket(t, rest)
|
||||
if len(trailing) != 0 || active.Header.MsgID != currentActiveQuestSnapshotMsgID ||
|
||||
binary.LittleEndian.Uint32(active.Body[6:10]) != step.want {
|
||||
t.Fatalf("type=%#x active header=%+v body=%x trailing=%x want trigger=%d", step.triggerType, active.Header, active.Body, trailing, step.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerAcceptsNativeMaskedHellPartyProgress(t *testing.T) {
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(context.Background(), dnfrepo.CharacterRecord{
|
||||
CharacterID: "20", AccountID: defaultAccountPrefix + "1", Job: "11", Level: 90,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(context.Background(), dnfrepo.QuestRecord{
|
||||
CharacterID: "20",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
2002: {Status: "active", ProgressValue: 1 | 1<<9 | 1<<18},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{conn: connection, connID: "native-masked-hell-party-trigger", selectedCharacterID: 20}
|
||||
for _, step := range []struct {
|
||||
mask byte
|
||||
want uint32
|
||||
}{
|
||||
{mask: 0x10, want: 1<<9 | 1<<18},
|
||||
{mask: 0x20, want: 1 << 18},
|
||||
{mask: 0x40, want: 0},
|
||||
} {
|
||||
connection.write.Reset()
|
||||
body := []byte{0x21, 0x00, 0xd2, 0x07, step.mask, 0x00, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if ack.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) ||
|
||||
binary.LittleEndian.Uint32(ack.Body[3:7]) != step.want {
|
||||
t.Fatalf("mask=%#x ACK header=%+v body=%x want trigger=%d", step.mask, ack.Header, ack.Body, step.want)
|
||||
}
|
||||
if step.want == 0 {
|
||||
if len(rest) != 0 {
|
||||
t.Fatalf("terminal masked op33 trailing=%x", rest)
|
||||
}
|
||||
continue
|
||||
}
|
||||
active, trailing := splitGameServerUpperPacket(t, rest)
|
||||
if len(trailing) != 0 || active.Header.MsgID != currentActiveQuestSnapshotMsgID ||
|
||||
binary.LittleEndian.Uint32(active.Body[6:10]) != step.want {
|
||||
t.Fatalf("mask=%#x active header=%+v body=%x trailing=%x want trigger=%d", step.mask, active.Header, active.Body, trailing, step.want)
|
||||
}
|
||||
}
|
||||
stored, found, err := repositories.Quest.Load(context.Background(), "20")
|
||||
if err != nil || !found || stored.States[2002].ProgressValue != 0 {
|
||||
t.Fatalf("native masked Hell Party progress found=%t err=%v state=%+v", found, err, stored.States[2002])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerSeekAndMeetNPCUsesAccountCrystalCountWithoutFeedbackLoop(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{
|
||||
CharacterID: "4", AccountID: "dnf:1", Job: "11", Level: 90,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{
|
||||
CharacterID: "4",
|
||||
States: map[int64]dnfrepo.QuestState{
|
||||
3622: {Status: "active", ProgressValue: 816},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{
|
||||
AccountID: "dnf:1",
|
||||
Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 120019},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{
|
||||
conn: connection, connID: "set-seek-meet-trigger-test",
|
||||
channel: channelcatalog.Channel{ID: 19}, selectedCharacterID: 4,
|
||||
}
|
||||
|
||||
itemBody := []byte{0x21, 0x00, 0x26, 0x0e, 0x10, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, itemBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, _, _ := repositories.Quest.Load(ctx, "4")
|
||||
if got := stored.States[3622].ProgressValue; got != 512 {
|
||||
t.Fatalf("item-channel progress=%d want=512", got)
|
||||
}
|
||||
ack, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if ack.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) || binary.LittleEndian.Uint32(ack.Body[3:7]) != 512 {
|
||||
t.Fatalf("item-channel ACK=%+v body=%x", ack.Header, ack.Body)
|
||||
}
|
||||
active, trailing := splitGameServerUpperPacket(t, rest)
|
||||
if len(trailing) != 0 || active.Header.MsgID != currentActiveQuestSnapshotMsgID || binary.LittleEndian.Uint32(active.Body[6:10]) != 512 {
|
||||
t.Fatalf("item-channel snapshot=%+v body=%x trailing=%x", active.Header, active.Body, trailing)
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.handleCurrentSetQuestTrigger(session, itemBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if connection.write.Len() != 0 {
|
||||
t.Fatalf("resolved item-channel replay wrote packets=%x", connection.write.Bytes())
|
||||
}
|
||||
|
||||
npcBody := []byte{0x21, 0x00, 0x26, 0x0e, 0x20, 0x00}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, npcBody); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, _, _ = repositories.Quest.Load(ctx, "4")
|
||||
if got := stored.States[3622].ProgressValue; got != 0 {
|
||||
t.Fatalf("terminal progress=%d want=0", got)
|
||||
}
|
||||
terminalACK, terminalTrailing := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if terminalACK.Header.MsgID != uint16(dnfenum.CmdPacketSetQuestTrigger) || binary.LittleEndian.Uint32(terminalACK.Body[3:7]) != 0 || len(terminalTrailing) != 0 {
|
||||
t.Fatalf("terminal ACK=%+v body=%x trailing=%x", terminalACK.Header, terminalACK.Body, terminalTrailing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentSetQuestTriggerSeekAndMeetNPCInsufficientMaterialsIsSilent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repositories := dnfrepomemory.NewMemoryGroup()
|
||||
if err := repositories.Character.Save(ctx, dnfrepo.CharacterRecord{CharacterID: "4", AccountID: "dnf:1"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.Quest.Save(ctx, dnfrepo.QuestRecord{CharacterID: "4", States: map[int64]dnfrepo.QuestState{
|
||||
3622: {Status: "active", ProgressValue: 1012},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := repositories.AccountInventory.Save(ctx, dnfrepo.AccountInventoryRecord{AccountID: "dnf:1", Slots: map[string]dnfrepo.ItemStack{
|
||||
"0:358": {ItemID: 3037, Count: 499},
|
||||
}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
connection := &bufferConn{}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
repositoryProvider: func() (dnfrepo.Group, bool) { return repositories, true },
|
||||
questCatalog: questTriggerBridgeCatalog(t),
|
||||
}
|
||||
session := &gameSession{conn: connection, selectedCharacterID: 4}
|
||||
if err := service.handleCurrentSetQuestTrigger(session, []byte{0x21, 0x00, 0x26, 0x0e, 0x10, 0x00}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, _, _ := repositories.Quest.Load(ctx, "4")
|
||||
if got := stored.States[3622].ProgressValue; got != 1012 || connection.write.Len() != 0 {
|
||||
t.Fatalf("insufficient material progress=%d packets=%x", got, connection.write.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLegacySetQuestTriggerStripsOnlyExactCloneTrailer(t *testing.T) {
|
||||
semantic := []byte{0x21, 0x00, 0x78, 0x12, 0x00, 0x00}
|
||||
wrapped := append(append([]byte(nil), semantic...), 0xaa, 0xbb, 0xcc, 0xdd)
|
||||
if got := normalizeLegacyGameBody(uint16(dnfenum.CmdPacketSetQuestTrigger), wrapped); !bytes.Equal(got, semantic) {
|
||||
t.Fatalf("normalized=%x want=%x", got, semantic)
|
||||
}
|
||||
opaque := append(append([]byte(nil), semantic...), 1, 2, 3, 4, 5)
|
||||
if got := normalizeLegacyGameBody(uint16(dnfenum.CmdPacketSetQuestTrigger), opaque); !bytes.Equal(got, opaque) {
|
||||
t.Fatalf("non-exact body was changed: %x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeLegacySetQuestTriggerPreservesNativeMaskedSevenByteForm(t *testing.T) {
|
||||
// Current-client op33 uses the seventh byte as the masked-form reserved
|
||||
// field. Removing it destroys the native 0x10/0x20/0x40 lane layout.
|
||||
wire := []byte{0x21, 0x00, 0x7b, 0x0d, 0x10, 0x00, 0x00}
|
||||
if got := normalizeLegacyGameBody(uint16(dnfenum.CmdPacketSetQuestTrigger), wire); !bytes.Equal(got, wire) {
|
||||
t.Fatalf("normalized=%x want=%x", got, wire)
|
||||
}
|
||||
opaque := []byte{0x20, 0x00, 0x7b, 0x0d, 0x10, 0x00, 0x00}
|
||||
if got := normalizeLegacyGameBody(uint16(dnfenum.CmdPacketSetQuestTrigger), opaque); !bytes.Equal(got, opaque) {
|
||||
t.Fatalf("non-matching wrapped body was changed: %x", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user