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,208 @@
package raid
import (
"encoding/binary"
"encoding/hex"
"errors"
"strings"
"testing"
)
// lukeCardDisplayTestSanctums is the shipped phase-one shape: three sanctums,
// each with its own pattern index and seven card slots.
func lukeCardDisplayTestSanctums() []LukeCardDisplaySanctum {
return []LukeCardDisplaySanctum{
{
DungeonID: 5054,
Pattern: 1,
Cards: []uint32{59171, 59098, 59873, 59096, 56065, 59874, 56699},
},
{
DungeonID: 5055,
Pattern: 2,
Cards: []uint32{59278, 56166, 56673, 59875, 56066, 56674, 56067},
},
{
DungeonID: 5056,
Pattern: 0,
Cards: []uint32{56068, 56063, 56700, 57156, 59872, 59099, 59095},
},
}
}
// TestBuildLukeCardDisplayBodyMatchesTheReverseEngineeredWireBytes is the
// byte-exact pin for opcode 1181. Every expected byte below is written out by
// hand from the spec's field table (tools\_rev_卢克塔罗牌_1181规格_20260929.md
// §一) rather than re-encoded by the builder:
//
// u8 cardSetIndex = 3
// u8 N = 3
// per record: u16 A (little endian), u8 B, u8 K, then K x (u32 P little
// endian, u8 Q = 0)
// u8 openWindowFlag = 1
//
// 5054 = 0x13BE, 5055 = 0x13BF, 5056 = 0x13C0; the card ids are the 22
// `[CARD]` monster ids, 59171 = 0x0000E723 and so on.
func TestBuildLukeCardDisplayBodyMatchesTheReverseEngineeredWireBytes(t *testing.T) {
body, err := BuildLukeCardDisplayBody(LukeCardDisplay{
CardSetIndex: 3,
CardSetCount: 5,
Sanctums: lukeCardDisplayTestSanctums(),
OpenWindow: true,
})
if err != nil {
t.Fatalf("BuildLukeCardDisplayBody error = %v", err)
}
const wantHex = "" +
// cardSetIndex, N
"0303" +
// 5054, pattern 1, K=7, then the seven cards each followed by Q=0
"be13" + "01" + "07" +
"23e70000" + "00" +
"dae60000" + "00" +
"e1e90000" + "00" +
"d8e60000" + "00" +
"01db0000" + "00" +
"e2e90000" + "00" +
"7bdd0000" + "00" +
// 5055, pattern 2, K=7
"bf13" + "02" + "07" +
"8ee70000" + "00" +
"66db0000" + "00" +
"61dd0000" + "00" +
"e3e90000" + "00" +
"02db0000" + "00" +
"62dd0000" + "00" +
"03db0000" + "00" +
// 5056, pattern 0, K=7
"c013" + "00" + "07" +
"04db0000" + "00" +
"ffda0000" + "00" +
"7cdd0000" + "00" +
"44df0000" + "00" +
"e0e90000" + "00" +
"dbe60000" + "00" +
"d7e60000" + "00" +
// openWindowFlag
"01"
want, err := hex.DecodeString(wantHex)
if err != nil {
t.Fatalf("decode want hex: %v", err)
}
if len(body) != len(want) {
t.Fatalf("body_len=%d want=%d", len(body), len(want))
}
if got := hex.EncodeToString(body); got != wantHex {
t.Fatalf("body=%s\nwant=%s", got, wantHex)
}
if size := LukeCardDisplayBodySize(lukeCardDisplayTestSanctums()); size != len(want) {
t.Fatalf("LukeCardDisplayBodySize=%d want=%d", size, len(want))
}
// Re-read the body field by field, in the client's own order, so a change in
// any field width fails here with the offset rather than only as a hex diff.
if body[0] != 3 {
t.Fatalf("cardSetIndex=%d want 3", body[0])
}
if body[1] != 3 {
t.Fatalf("N=%d want 3", body[1])
}
offset := 2
for index, want := range lukeCardDisplayTestSanctums() {
if got := binary.LittleEndian.Uint16(body[offset : offset+2]); uint32(got) != want.DungeonID {
t.Fatalf("record %d A@%d=%d want %d", index, offset, got, want.DungeonID)
}
offset += 2
if int(body[offset]) != want.Pattern {
t.Fatalf("record %d B@%d=%d want %d", index, offset, body[offset], want.Pattern)
}
offset++
if int(body[offset]) != 7 {
t.Fatalf("record %d K@%d=%d want 7", index, offset, body[offset])
}
offset++
for slot, cardID := range want.Cards {
if got := binary.LittleEndian.Uint32(body[offset : offset+4]); got != cardID {
t.Fatalf("record %d slot %d P@%d=%d want %d", index, slot, offset, got, cardID)
}
offset += 4
if body[offset] != 0 {
t.Fatalf("record %d slot %d Q@%d=%d want 0", index, slot, offset, body[offset])
}
offset++
}
}
if offset != len(body)-1 {
t.Fatalf("openWindowFlag offset=%d want=%d", offset, len(body)-1)
}
if body[len(body)-1] != 1 {
t.Fatalf("openWindowFlag=%d want 1", body[len(body)-1])
}
}
// TestBuildLukeCardDisplayBodyKeepsOpenWindowOffWhenNotAsked pins the trailing
// byte's other value: only the waiting-area push sets it, later re-reads do not.
func TestBuildLukeCardDisplayBodyKeepsOpenWindowOffWhenNotAsked(t *testing.T) {
body, err := BuildLukeCardDisplayBody(LukeCardDisplay{
CardSetIndex: 0,
CardSetCount: 5,
Sanctums: []LukeCardDisplaySanctum{{DungeonID: 5054, Pattern: 0, Cards: []uint32{59171}}},
})
if err != nil {
t.Fatalf("BuildLukeCardDisplayBody error = %v", err)
}
want := []byte{0x00, 0x01, 0xbe, 0x13, 0x00, 0x01, 0x23, 0xe7, 0x00, 0x00, 0x00, 0x00}
if got := hex.EncodeToString(body); got != hex.EncodeToString(want) {
t.Fatalf("body=%s want=%s", got, hex.EncodeToString(want))
}
}
// TestBuildLukeCardDisplayBodyRefusesBodiesTheClientWouldDrop covers the
// out-of-range card set index (the handler throws the WHOLE packet away) and the
// shapes that would make the client read past the end of a record.
func TestBuildLukeCardDisplayBodyRefusesBodiesTheClientWouldDrop(t *testing.T) {
base := func() LukeCardDisplay {
return LukeCardDisplay{
CardSetIndex: 4,
CardSetCount: 5,
Sanctums: lukeCardDisplayTestSanctums(),
OpenWindow: true,
}
}
cases := []struct {
name string
mutate func(*LukeCardDisplay)
}{
{"card set index at the bound", func(d *LukeCardDisplay) { d.CardSetIndex = 5 }},
{"unknown card set count", func(d *LukeCardDisplay) { d.CardSetCount = 0 }},
{"no records", func(d *LukeCardDisplay) { d.Sanctums = nil }},
{"zero dungeon id", func(d *LukeCardDisplay) { d.Sanctums[0].DungeonID = 0 }},
{"negative pattern", func(d *LukeCardDisplay) { d.Sanctums[0].Pattern = -1 }},
{"pattern above the u8", func(d *LukeCardDisplay) { d.Sanctums[0].Pattern = 256 }},
{"no slots", func(d *LukeCardDisplay) { d.Sanctums[0].Cards = nil }},
{"zero card id", func(d *LukeCardDisplay) { d.Sanctums[0].Cards[3] = 0 }},
{"too many records", func(d *LukeCardDisplay) {
records := make([]LukeCardDisplaySanctum, 256)
for index := range records {
records[index] = LukeCardDisplaySanctum{DungeonID: 5054, Cards: []uint32{59171}}
}
d.Sanctums = records
}},
}
for _, testCase := range cases {
display := base()
testCase.mutate(&display)
if _, err := BuildLukeCardDisplayBody(display); !errors.Is(err, ErrLukeCardDisplayShapeInvalid) {
t.Fatalf("%s: err=%v want %v", testCase.name, err, ErrLukeCardDisplayShapeInvalid)
}
}
if _, err := BuildLukeCardDisplayBody(base()); err != nil {
t.Fatalf("the unmutated shape must still encode: %v", err)
}
if LukeCardDisplayMsgID != 1181 {
t.Fatalf("LukeCardDisplayMsgID=%d want 1181", LukeCardDisplayMsgID)
}
if strings.TrimSpace(LukeCardSetPVFPath) != "etc/raid/luke/LukeCardSet.etc" {
t.Fatalf("LukeCardSetPVFPath=%q", LukeCardSetPVFPath)
}
}