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

204 lines
8.4 KiB
Go

package dnfbridge
import (
"bytes"
"errors"
"testing"
"time"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
func TestHandleDungeonSetPlayResultSendsFrozenCommittedPlanInCurrentEXEOrder(t *testing.T) {
service, runtime, session, connection, _ := prepareCompletedSettlementRuntime(t)
connection.write.Reset()
const clientRankPoint = byte(73)
plan := currentDungeonSettlementPacketPlanForTest(t, runtime, clientRankPoint)
runtime.settlementResultPlan = &plan
request := make([]byte, currentDungeonPlayResultBaseSize)
request[currentDungeonPlayResultClientRankPointOffset] = clientRankPoint
if err := service.handleDungeonSetPlayResult(session, request); err != nil {
t.Fatal(err)
}
op34, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
op37, rest := splitGameServerUpperPacket(t, rest)
op35, rest := splitGameServerUpperPacket(t, rest)
if len(rest) != 0 {
t.Fatalf("unexpected trailing settlement stream=%x", rest)
}
if op34.Header.Classification != 0 || op34.Header.MsgID != currentDungeonPlayResultNoticeMsgID ||
!bytes.Equal(op34.Body, plan.PlayResultBody) {
t.Fatalf("op34 packet class=%d msg=%d body=%x", op34.Header.Classification, op34.Header.MsgID, op34.Body)
}
if op37.Header.Classification != 0 || op37.Header.MsgID != currentDungeonCharacterStateMsgID ||
!bytes.Equal(op37.Body, plan.CharacterBody) {
t.Fatalf("op37 packet class=%d msg=%d body=%x", op37.Header.Classification, op37.Header.MsgID, op37.Body)
}
if op35.Header.Classification != 0 || op35.Header.MsgID != currentDungeonClearRewardMsgID ||
!bytes.Equal(op35.Body, plan.ClearRewardBody) {
t.Fatalf("op35 packet class=%d msg=%d body_len=%d", op35.Header.Classification, op35.Header.MsgID, len(op35.Body))
}
if !runtime.settlementResultNoticeSent || !runtime.settlementCharacterStateSent || !runtime.settlementClearRewardSent {
t.Fatalf("settlement send state=%+v", runtime)
}
if runtime.settlementCardScrollStateSent || runtime.settlementCardLayoutSent ||
runtime.settlementCardSelectionSent {
t.Fatalf("op46 must stop after rating chain: %+v", runtime)
}
written := connection.write.Len()
if err := service.handleDungeonSetPlayResult(session, append([]byte(nil), request...)); err != nil {
t.Fatal(err)
}
if connection.write.Len() != written {
t.Fatalf("exact op46 replay resent completed plan bytes=%d", connection.write.Len()-written)
}
}
func TestCompletedDungeonDoesNotSynthesizeOp46OrRatingPackets(t *testing.T) {
_, runtime, _, connection, _ := prepareCompletedSettlementRuntime(t)
connection.write.Reset()
// The removed fallback used to forge an op46 about 800ms after op31. Wait
// beyond that boundary and prove the server remains at the clear-entry gate.
time.Sleep(900 * time.Millisecond)
if connection.write.Len() != 0 || runtime.settlementPlayResultReceived ||
runtime.settlementResultNoticeSent || runtime.settlementCharacterStateSent ||
runtime.settlementClearRewardSent || runtime.settlementCardScrollStateSent ||
runtime.settlementCardLayoutSent {
t.Fatalf("missing client op46 advanced settlement bytes=%x state=%+v", connection.write.Bytes(), runtime)
}
}
func TestHandleDungeonSetPlayResultResumesAfterMiddlePacketWriteFailure(t *testing.T) {
service, runtime, session, _, _ := prepareCompletedSettlementRuntime(t)
const clientRankPoint = byte(91)
plan := currentDungeonSettlementPacketPlanForTest(t, runtime, clientRankPoint)
runtime.settlementResultPlan = &plan
request := make([]byte, currentDungeonPlayResultBaseSize)
request[currentDungeonPlayResultClientRankPointOffset] = clientRankPoint
wantErr := errors.New("settlement op37 write failed")
connection := &failNthDungeonWriteConn{failAt: 2, err: wantErr}
session.conn = connection
if err := service.handleDungeonSetPlayResult(session, request); !errors.Is(err, wantErr) {
t.Fatalf("first op46 error=%v want=%v", err, wantErr)
}
if !runtime.settlementResultNoticeSent || runtime.settlementCharacterStateSent || runtime.settlementClearRewardSent {
t.Fatalf("failed middle write committed wrong flags=%+v", runtime)
}
if err := service.handleDungeonSetPlayResult(session, append([]byte(nil), request...)); err != nil {
t.Fatal(err)
}
if !runtime.settlementResultNoticeSent || !runtime.settlementCharacterStateSent || !runtime.settlementClearRewardSent {
t.Fatalf("retry did not complete flags=%+v", runtime)
}
op34, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
op37, rest := splitGameServerUpperPacket(t, rest)
op35, rest := splitGameServerUpperPacket(t, rest)
if len(rest) != 0 || op34.Header.MsgID != currentDungeonPlayResultNoticeMsgID ||
op37.Header.MsgID != currentDungeonCharacterStateMsgID || op35.Header.MsgID != currentDungeonClearRewardMsgID {
t.Fatalf("resumed order op34=%d op37=%d op35=%d rest=%x",
op34.Header.MsgID, op37.Header.MsgID, op35.Header.MsgID, rest)
}
if runtime.settlementCardScrollStateSent || runtime.settlementCardLayoutSent ||
runtime.settlementCardSelectionSent {
t.Fatalf("retried op46 must stop after rating chain: %+v", runtime)
}
}
func TestHandleDungeonSetPlayResultDoesNotSendPlanForDifferentOp46Rank(t *testing.T) {
service, runtime, session, connection, _ := prepareCompletedSettlementRuntime(t)
connection.write.Reset()
plan := currentDungeonSettlementPacketPlanForTest(t, runtime, 44)
runtime.settlementResultPlan = &plan
request := make([]byte, currentDungeonPlayResultBaseSize)
request[currentDungeonPlayResultClientRankPointOffset] = 45
if err := service.handleDungeonSetPlayResult(session, request); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 || runtime.settlementResultNoticeSent ||
runtime.settlementCharacterStateSent || runtime.settlementClearRewardSent {
t.Fatalf("mismatched rank emitted settlement bytes=%x state=%+v", connection.write.Bytes(), runtime)
}
}
func TestCurrentDungeonSettlementPlanRejectsOwnerRankAndZeroRewardMismatch(t *testing.T) {
_, runtime, _, _, _ := prepareCompletedSettlementRuntime(t)
const clientRankPoint = byte(55)
character := runtime.Character
character.Level = 5
character.Stats = map[string]int64{"exp": 1234}
notice := currentDungeonPlayResultNotice{
RankGrade: 3,
ClearTimeMS: 2222,
RankPoint: clientRankPoint,
Participants: []currentDungeonPlayResultParticipant{{ObjectKey: 99, ClearTimeMS: 2222}},
TimeBonusPoint: 0,
}
reward := currentDungeonClearRewardSnapshot{
CharacterID: 99, CompletionKey: "test-run", Source: "committed_test", Committed: true,
Base: [4]uint32{100}, Tail: currentDungeonClearRewardTail{ShowResult: true},
}
wrongOwner := reward
wrongOwner.CharacterID = 98
if _, err := buildCurrentDungeonSettlementPacketPlan(clientRankPoint, notice, wrongOwner, character, dnfrepo.SkillPointState{}); !errors.Is(err, errCurrentDungeonSettlementPlanOwner) {
t.Fatalf("wrong owner error=%v", err)
}
debugNotice := notice
debugNotice.RankPoint = 99
if _, err := buildCurrentDungeonSettlementPacketPlan(clientRankPoint, debugNotice, reward, character, dnfrepo.SkillPointState{}); !errors.Is(err, errCurrentDungeonSettlementPlanShape) {
t.Fatalf("different op34/op46 rank error=%v", err)
}
zeroReward := reward
zeroReward.Base = [4]uint32{}
if _, err := buildCurrentDungeonSettlementPacketPlan(clientRankPoint, notice, zeroReward, character, dnfrepo.SkillPointState{}); !errors.Is(err, errCurrentDungeonClearRewardShape) {
t.Fatalf("zero reward error=%v", err)
}
}
func currentDungeonSettlementPacketPlanForTest(
t *testing.T,
runtime *runtimeDungeonState,
clientRankPoint byte,
) currentDungeonSettlementPacketPlan {
t.Helper()
character := runtime.Character
character.Level = 5
character.Stats = map[string]int64{"exp": 12345, "bonus_sp": 25}
completionKey := runtime.clearMapCompletionKey
if completionKey == "" {
completionKey = "committed-test-run"
}
notice := currentDungeonPlayResultNotice{
RankGrade: 3,
ClearTimeMS: 12345,
RankPoint: clientRankPoint,
AllVisitedClear: true,
Participants: []currentDungeonPlayResultParticipant{{
ObjectKey: 99, ClearTimeMS: 12345, NewBest: true,
}},
}
reward := currentDungeonClearRewardSnapshot{
CharacterID: 99, CompletionKey: completionKey, Source: "committed_test_reward", Committed: true,
Base: [4]uint32{100}, Tail: currentDungeonClearRewardTail{ShowResult: true},
}
plan, err := buildCurrentDungeonSettlementPacketPlan(
clientRankPoint,
notice,
reward,
character,
dnfrepo.SkillPointState{RemainingSP: 30, RemainingTP: 1},
)
if err != nil {
t.Fatal(err)
}
return plan
}