Files
90CNServer/server/internal/services/dnfbridge/dungeon_settlement_result_protocol_test.go
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

301 lines
11 KiB
Go

package dnfbridge
import (
"encoding/binary"
"errors"
"testing"
)
func TestCurrentDungeonPlayResultNoticeMatchesCurrentOp34Reader(t *testing.T) {
body, err := buildCurrentDungeonPlayResultNoticeBody(currentDungeonPlayResultNotice{
RankGrade: 99,
ClearTimeMS: 12345,
TimeBonusPoint: 7,
RankPoint: 99,
AllVisitedClear: true,
Participants: []currentDungeonPlayResultParticipant{{
ObjectKey: 20,
ClearTimeMS: 12345,
NewBest: true,
}},
})
if err != nil {
t.Fatal(err)
}
if len(body) != 16 {
t.Fatalf("op34 body length = %d, want 16", len(body))
}
if body[0] != 99 || binary.LittleEndian.Uint32(body[1:5]) != 12345 ||
body[5] != 7 || body[6] != 99 || body[7] != 1 || body[8] != 1 ||
binary.LittleEndian.Uint16(body[9:11]) != 20 ||
binary.LittleEndian.Uint32(body[11:15]) != 12345 || body[15] != 1 {
t.Fatalf("unexpected op34 body: %x", body)
}
}
func TestCurrentDungeonPlayResultNoticeRejectsUnownedParticipantShape(t *testing.T) {
if _, err := buildCurrentDungeonPlayResultNoticeBody(currentDungeonPlayResultNotice{}); !errors.Is(err, errCurrentDungeonPlayResultNoticeShape) {
t.Fatalf("empty participants error = %v", err)
}
participants := make([]currentDungeonPlayResultParticipant, currentDungeonPlayResultMaximumParticipants+1)
for index := range participants {
participants[index].ObjectKey = uint16(index + 1)
}
if _, err := buildCurrentDungeonPlayResultNoticeBody(currentDungeonPlayResultNotice{Participants: participants}); !errors.Is(err, errCurrentDungeonPlayResultNoticeShape) {
t.Fatalf("overflow participants error = %v", err)
}
if _, err := buildCurrentDungeonPlayResultNoticeBody(currentDungeonPlayResultNotice{Participants: []currentDungeonPlayResultParticipant{{}}}); !errors.Is(err, errCurrentDungeonPlayResultNoticeShape) {
t.Fatalf("zero participant owner error = %v", err)
}
}
func TestCurrentDungeonClearRewardMatchesCurrentOp35ReaderGrammar(t *testing.T) {
snapshot := currentDungeonClearRewardSnapshot{
CharacterID: 20,
CompletionKey: "run-20-3-1",
Source: "committed_test_reward",
Committed: true,
Base: [4]uint32{11, 12, 13, 14},
BaseFlag: 15,
BonusGroupsA: []currentDungeonClearRewardPair{{Key: 21, Value: 22}},
BonusGroupsB: []currentDungeonClearRewardPair{{Key: 23, Value: 24}},
PostBase: [currentDungeonClearRewardPostBaseFieldCount]uint32{31, 32, 33, 34, 35, 36, 37},
Score: [currentDungeonClearRewardScoreFieldCount]uint32{41, 42, 43, 44},
Quest: []currentDungeonClearRewardPair{{Key: 51, Value: 52}},
Drops: []currentDungeonClearRewardDrop{{
ObjectKey: 61, TemplateID: 62, StackCount: 63, Value16A: 64, Value8: 65, Value16B: 66,
}},
TotalReward: 71,
PreTailValue: 80,
Tail: currentDungeonClearRewardTail{
Value0: 81, ShowResult: true, Flag0: true, MonsterTotalExp: 85, Value2: 86, Value3: 87, Value4: 88,
},
}
snapshot.Bonus[0] = 16
snapshot.CardSlots[0] = []currentDungeonClearRewardPair{{Key: 67, Value: 68}}
snapshot.AuxGroupsA[1] = []currentDungeonClearRewardPair{{Key: 72, Value: 73}}
snapshot.AuxGroupsB[2] = []currentDungeonClearRewardPair{{Key: 74, Value: 75}}
body, err := buildCurrentDungeonClearRewardBody(snapshot)
if err != nil {
t.Fatal(err)
}
wantLength := currentDungeonClearRewardMinimumBodySize +
8*len(snapshot.BonusGroupsA) + 8*len(snapshot.BonusGroupsB) +
8*len(snapshot.Quest) + 15*len(snapshot.Drops) +
8 + 8 + 8
if len(body) != wantLength {
t.Fatalf("op35 body length = %d, want %d", len(body), wantLength)
}
reader := settlementProtocolReader{body: body}
for _, want := range snapshot.Base {
if got := reader.u32(); got != want {
t.Fatalf("base value = %d, want %d", got, want)
}
}
if got := reader.u8(); got != snapshot.BaseFlag {
t.Fatalf("base flag = %d, want %d", got, snapshot.BaseFlag)
}
for _, want := range snapshot.Bonus {
if got := reader.u32(); got != want {
t.Fatalf("bonus value = %d, want %d", got, want)
}
}
reader.u8Pairs(t, snapshot.BonusGroupsA)
reader.u8Pairs(t, snapshot.BonusGroupsB)
for _, want := range snapshot.PostBase {
if got := reader.u32(); got != want {
t.Fatalf("post-base value = %d, want %d", got, want)
}
}
for _, want := range snapshot.Score {
if got := reader.u32(); got != want {
t.Fatalf("score value = %d, want %d", got, want)
}
}
if got := reader.u32(); got != uint32(len(snapshot.Quest)) {
t.Fatalf("quest pair count = %d", got)
}
reader.pairs(t, snapshot.Quest)
if got := reader.u8(); got != byte(len(snapshot.Drops)) {
t.Fatalf("drop count = %d", got)
}
for _, drop := range snapshot.Drops {
if got := reader.u16(); got != drop.ObjectKey || reader.u32() != drop.TemplateID ||
reader.u32() != drop.StackCount || reader.u16() != drop.Value16A || reader.u8() != drop.Value8 || reader.u16() != drop.Value16B {
t.Fatalf("drop row mismatch at offset %d", reader.offset)
}
}
for _, slot := range snapshot.CardSlots {
reader.u8Pairs(t, slot)
}
if got := reader.u32(); got != snapshot.TotalReward {
t.Fatalf("total reward = %d", got)
}
for _, group := range snapshot.AuxGroupsA {
reader.u8Pairs(t, group)
}
for _, group := range snapshot.AuxGroupsB {
reader.u8Pairs(t, group)
}
if got := reader.u32(); got != snapshot.PreTailValue {
t.Fatalf("pre-tail value = %d, want %d", got, snapshot.PreTailValue)
}
tailOffset := reader.offset
if reader.u32() != snapshot.Tail.Value0 ||
reader.u8() != boolByte(snapshot.Tail.ShowResult) || reader.u8() != boolByte(snapshot.Tail.Flag0) ||
reader.u32() != snapshot.Tail.MonsterTotalExp || reader.u32() != snapshot.Tail.Value2 ||
reader.u32() != snapshot.Tail.Value3 || reader.u32() != snapshot.Tail.Value4 ||
reader.offset-tailOffset != currentDungeonClearRewardTailSize || reader.offset != len(body) {
t.Fatalf("tail/read boundary mismatch offset=%d len=%d", reader.offset, len(body))
}
}
func TestCurrentDungeonClearRewardMergesMonsterAndQuestDynamicRows(t *testing.T) {
snapshot := currentDungeonClearRewardSnapshot{
CharacterID: 20,
CompletionKey: "run-20-dynamic",
Source: "committed_test_reward",
Committed: true,
Base: [4]uint32{1},
MonsterExperienceByObjectKey: []currentDungeonClearRewardPair{
{Key: 0x192, Value: 1054},
{Key: 0x195, Value: 1129},
},
Quest: []currentDungeonClearRewardPair{{Key: 3149, Value: 0}},
}
body, err := buildCurrentDungeonClearRewardBody(snapshot)
if err != nil {
t.Fatal(err)
}
reader := settlementProtocolReader{body: body}
for range 4 {
reader.u32()
}
reader.u8()
for range currentDungeonClearRewardBonusFieldCount {
reader.u32()
}
for range 2 {
for range reader.u8() {
reader.u32()
reader.u32()
}
}
for range currentDungeonClearRewardPostBaseFieldCount + currentDungeonClearRewardScoreFieldCount {
reader.u32()
}
if got := reader.u32(); got != 3 {
t.Fatalf("dynamic row count=%d, want 3", got)
}
want := []currentDungeonClearRewardPair{
{Key: 0x192, Value: 1054},
{Key: 0x195, Value: 1129},
{Key: 3149, Value: 0},
}
reader.pairs(t, want)
}
func TestCurrentDungeonClearRewardRejectsUncommittedAndFabricatedZeroSnapshots(t *testing.T) {
base := currentDungeonClearRewardSnapshot{CharacterID: 20, CompletionKey: "run", Source: "test"}
if _, err := buildCurrentDungeonClearRewardBody(base); !errors.Is(err, errCurrentDungeonClearRewardUncommitted) {
t.Fatalf("uncommitted error = %v", err)
}
base.Committed = true
base.Tail.ShowResult = true
if _, err := buildCurrentDungeonClearRewardBody(base); !errors.Is(err, errCurrentDungeonClearRewardShape) {
t.Fatalf("zero reward error = %v", err)
}
base.AllowZeroReward = true
body, err := buildCurrentDungeonClearRewardBody(base)
if err != nil || len(body) != currentDungeonClearRewardMinimumBodySize {
t.Fatalf("atomically-owned zero reward body length=%d err=%v", len(body), err)
}
base.AllowZeroReward = false
base.Base[0] = 1
body, err = buildCurrentDungeonClearRewardBody(base)
if err != nil {
t.Fatal(err)
}
if len(body) != currentDungeonClearRewardMinimumBodySize {
t.Fatalf("minimum body length = %d", len(body))
}
}
func TestCurrentDungeonClearRewardMinimumCurrentEXEOffsets(t *testing.T) {
// All count-bearing sections are empty here. The standalone pre-tail u32
// starts at 184. The final current reader segment starts at 188 and spans
// exactly u32/u8/u8/u32/u32/u32/u32 (22 bytes), ending at 210.
snapshot := currentDungeonClearRewardSnapshot{
CharacterID: 20, CompletionKey: "run-20-min", Source: "committed_test", Committed: true,
Base: [4]uint32{333}, Tail: currentDungeonClearRewardTail{ShowResult: true},
}
body, err := buildCurrentDungeonClearRewardBody(snapshot)
if err != nil {
t.Fatal(err)
}
if currentDungeonClearRewardMinimumBodySize != 210 {
t.Fatalf("declared minimum=%d, want current-reader 210", currentDungeonClearRewardMinimumBodySize)
}
if len(body) != currentDungeonClearRewardMinimumBodySize {
t.Fatalf("minimum body length=%d, want current-reader 210", len(body))
}
if got := binary.LittleEndian.Uint32(body[0:4]); got != 333 {
t.Fatalf("base[0]=%d, want committed clear experience 333", got)
}
const preTailOffset = 184
const tailOffset = preTailOffset + 4
if got := binary.LittleEndian.Uint32(body[preTailOffset:tailOffset]); got != 0 {
t.Fatalf("neutral pre-tail value=%d, want 0", got)
}
if tailOffset+currentDungeonClearRewardTailSize != len(body) {
t.Fatalf("tail offset=%d size=%d body=%d", tailOffset, currentDungeonClearRewardTailSize, len(body))
}
reader := settlementProtocolReader{body: body, offset: tailOffset}
if reader.u32() != 0 || reader.u8() != 1 || reader.u8() != 0 ||
reader.u32() != 0 || reader.u32() != 0 || reader.u32() != 0 || reader.u32() != 0 ||
reader.offset != len(body) {
t.Fatalf("current-exe op35 tail mismatch offset=%d body=%x", reader.offset, body)
}
}
type settlementProtocolReader struct {
body []byte
offset int
}
func (r *settlementProtocolReader) u8() byte {
value := r.body[r.offset]
r.offset++
return value
}
func (r *settlementProtocolReader) u16() uint16 {
value := binary.LittleEndian.Uint16(r.body[r.offset : r.offset+2])
r.offset += 2
return value
}
func (r *settlementProtocolReader) u32() uint32 {
value := binary.LittleEndian.Uint32(r.body[r.offset : r.offset+4])
r.offset += 4
return value
}
func (r *settlementProtocolReader) pairs(t *testing.T, want []currentDungeonClearRewardPair) {
t.Helper()
for _, pair := range want {
if gotKey, gotValue := r.u32(), r.u32(); gotKey != pair.Key || gotValue != pair.Value {
t.Fatalf("pair = (%d,%d), want (%d,%d)", gotKey, gotValue, pair.Key, pair.Value)
}
}
}
func (r *settlementProtocolReader) u8Pairs(t *testing.T, want []currentDungeonClearRewardPair) {
t.Helper()
if got := int(r.u8()); got != len(want) {
t.Fatalf("u8 pair count = %d, want %d", got, len(want))
}
r.pairs(t, want)
}