DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
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)
}
}