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,473 @@
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/modules/dnf/channelcatalog"
|
||||
"longheng.io/server/internal/modules/dnf/raid"
|
||||
"longheng.io/server/internal/platform/timequeue"
|
||||
)
|
||||
|
||||
func TestActivateRuntimeRaidPhaseBroadcastFailureAllowsSamePhaseRetry(t *testing.T) {
|
||||
preparedAt := time.Date(2026, 8, 6, 4, 0, 0, 0, time.UTC)
|
||||
wantWriteErr := errors.New("raid phase announcement write failed")
|
||||
connection := &runtimeRaidPhaseTestConn{remainingFailures: 1, writeErr: wantWriteErr}
|
||||
queue := &runtimeRaidPhaseTestQueue{}
|
||||
service, raidKey, activationAt := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
||||
queue.now = activationAt
|
||||
|
||||
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, activationAt, false); !errors.Is(err, wantWriteErr) {
|
||||
t.Fatalf("first activation error=%v want=%v", err, wantWriteErr)
|
||||
}
|
||||
if queue.scheduleCalls != 0 {
|
||||
t.Fatalf("phase expiry scheduled after failed announcement: calls=%d", queue.scheduleCalls)
|
||||
}
|
||||
committed := cloneRuntimeRaidState(*service.raids[raidKey])
|
||||
if committed.Lifecycle.Status != runtimeRaidLifecyclePhaseActive || committed.Lifecycle.Phase != runtimeRaidDungeonPhaseOne {
|
||||
t.Fatalf("committed lifecycle=%+v", committed.Lifecycle)
|
||||
}
|
||||
if !committed.Lifecycle.PhaseStartedAt.Equal(activationAt) {
|
||||
t.Fatalf("committed phase start=%s want=%s", committed.Lifecycle.PhaseStartedAt, activationAt)
|
||||
}
|
||||
|
||||
retryAt := activationAt.Add(17*time.Second + 200*time.Millisecond)
|
||||
queue.now = retryAt
|
||||
retried, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, retryAt, false)
|
||||
if err != nil {
|
||||
t.Fatalf("same-phase retry: %v", err)
|
||||
}
|
||||
if !retried.Lifecycle.PhaseStartedAt.Equal(committed.Lifecycle.PhaseStartedAt) ||
|
||||
!retried.Lifecycle.PhaseDeadline.Equal(committed.Lifecycle.PhaseDeadline) ||
|
||||
retried.Lifecycle.Revision != committed.Lifecycle.Revision {
|
||||
t.Fatalf("retry reset phase lifecycle: before=%+v after=%+v", committed.Lifecycle, retried.Lifecycle)
|
||||
}
|
||||
wantRemaining := runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(retryAt))
|
||||
assertRuntimeRaidPhaseActivationPackets(t, connection.write.Bytes(), runtimeRaidDungeonPhaseOne, wantRemaining)
|
||||
if queue.scheduleCalls != 1 || queue.scheduledName != "raid-phase:13108201:1:1" {
|
||||
t.Fatalf("phase expiry reconstruction calls=%d name=%q", queue.scheduleCalls, queue.scheduledName)
|
||||
}
|
||||
if wantDelay := committed.Lifecycle.PhaseDeadline.Sub(retryAt); queue.scheduledDelay != wantDelay {
|
||||
t.Fatalf("phase expiry delay=%s want=%s", queue.scheduledDelay, wantDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateRuntimeRaidPhaseSchedulerFailureAllowsTimerReconstruction(t *testing.T) {
|
||||
preparedAt := time.Date(2026, 8, 6, 5, 0, 0, 0, time.UTC)
|
||||
wantScheduleErr := errors.New("raid phase schedule failed")
|
||||
connection := &runtimeRaidPhaseTestConn{}
|
||||
queue := &runtimeRaidPhaseTestQueue{remainingScheduleFailures: 1, scheduleErr: wantScheduleErr}
|
||||
service, raidKey, activationAt := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
||||
queue.now = activationAt
|
||||
|
||||
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, activationAt, false); !errors.Is(err, wantScheduleErr) {
|
||||
t.Fatalf("first activation error=%v want=%v", err, wantScheduleErr)
|
||||
}
|
||||
committed := cloneRuntimeRaidState(*service.raids[raidKey])
|
||||
assertRuntimeRaidPhaseActivationPackets(
|
||||
t,
|
||||
connection.write.Bytes(),
|
||||
runtimeRaidDungeonPhaseOne,
|
||||
runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(activationAt)),
|
||||
)
|
||||
connection.write.Reset()
|
||||
|
||||
retryAt := activationAt.Add(9*time.Second + 500*time.Millisecond)
|
||||
queue.now = retryAt
|
||||
retried, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, retryAt, false)
|
||||
if err != nil {
|
||||
t.Fatalf("same-phase timer reconstruction: %v", err)
|
||||
}
|
||||
if !retried.Lifecycle.PhaseStartedAt.Equal(committed.Lifecycle.PhaseStartedAt) ||
|
||||
!retried.Lifecycle.PhaseDeadline.Equal(committed.Lifecycle.PhaseDeadline) ||
|
||||
retried.Lifecycle.Revision != committed.Lifecycle.Revision {
|
||||
t.Fatalf("timer reconstruction reset phase lifecycle: before=%+v after=%+v", committed.Lifecycle, retried.Lifecycle)
|
||||
}
|
||||
wantRemaining := runtimeRaidDurationSeconds(committed.Lifecycle.PhaseDeadline.Sub(retryAt))
|
||||
assertRuntimeRaidPhaseActivationPackets(t, connection.write.Bytes(), runtimeRaidDungeonPhaseOne, wantRemaining)
|
||||
if queue.scheduleCalls != 2 || queue.callback == nil {
|
||||
t.Fatalf("phase expiry reconstruction calls=%d callback_nil=%t", queue.scheduleCalls, queue.callback == nil)
|
||||
}
|
||||
if wantDelay := committed.Lifecycle.PhaseDeadline.Sub(retryAt); queue.scheduledDelay != wantDelay {
|
||||
t.Fatalf("reconstructed phase expiry delay=%s want=%s", queue.scheduledDelay, wantDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishRuntimeRaidReadyOrdersCurrentEXEPacketsBeforeScheduling(t *testing.T) {
|
||||
preparedAt := time.Date(2026, 8, 6, 5, 30, 0, 0, time.UTC)
|
||||
wantWriteErr := errors.New("raid prepare-ui write failed")
|
||||
connection := &runtimeRaidPhaseTestConn{failAt: 4, writeErr: wantWriteErr}
|
||||
queue := &runtimeRaidPhaseTestQueue{now: preparedAt}
|
||||
service, raidKey, _ := newRuntimeRaidPhaseActivationTestService(t, preparedAt, connection, queue)
|
||||
preparing := cloneRuntimeRaidState(*service.raids[raidKey])
|
||||
|
||||
if err := service.publishRuntimeRaidReady(preparing); !errors.Is(err, wantWriteErr) {
|
||||
t.Fatalf("prepare-start write failure=%v want=%v", err, wantWriteErr)
|
||||
}
|
||||
if queue.scheduleCalls != 0 {
|
||||
t.Fatalf("ready timer scheduled before all packets succeeded: calls=%d", queue.scheduleCalls)
|
||||
}
|
||||
prepareState, rest := splitGameServerUpperPacket(t, connection.write.Bytes())
|
||||
if prepareState.Header.MsgID != raid.RaidStateMsgID ||
|
||||
!bytes.Equal(prepareState.Body, raid.BuildRaidStateBody(raid.RaidState{State: runtimeRaidPrepareState, Argument: 0})) {
|
||||
t.Fatalf("partial ready sequence state=%#04x body=%x", prepareState.Header.MsgID, prepareState.Body)
|
||||
}
|
||||
timer, rest := splitGameServerUpperPacket(t, rest)
|
||||
if timer.Header.MsgID != raid.RaidTimerMsgID {
|
||||
t.Fatalf("partial ready sequence timer=%#04x body=%x", timer.Header.MsgID, timer.Body)
|
||||
}
|
||||
remain, rest := splitGameServerUpperPacket(t, rest)
|
||||
if remain.Header.MsgID != raid.RaidRemainTimeMsgID || len(rest) != 0 {
|
||||
t.Fatalf("partial ready sequence remain=%#04x trailing=%d", remain.Header.MsgID, len(rest))
|
||||
}
|
||||
connection.write.Reset()
|
||||
|
||||
if err := service.publishRuntimeRaidReady(preparing); err != nil {
|
||||
t.Fatalf("ready sequence retry: %v", err)
|
||||
}
|
||||
if rest := assertRuntimeRaidReadyTimer(t, connection.write.Bytes(), raidKey); len(rest) != 0 {
|
||||
t.Fatalf("ready sequence trailing bytes=%d", len(rest))
|
||||
}
|
||||
if queue.scheduleCalls != 1 || queue.scheduledDelay != 3*time.Second {
|
||||
t.Fatalf("ready scheduling calls=%d delay=%s", queue.scheduleCalls, queue.scheduledDelay)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivateRuntimeRaidSinglePhaseBootstrapsCurrentClientStartNode(t *testing.T) {
|
||||
preparedAt := time.Date(2026, 8, 8, 4, 0, 0, 0, time.UTC)
|
||||
config := runtimeRaidBridgeTestAntonConfig()
|
||||
config.SingleMode = &raid.AntonSingleModeConfig{
|
||||
Name: "single",
|
||||
TimeLimitSeconds: 2100,
|
||||
Phase: raid.AntonPhase{Dungeons: []raid.AntonDungeonInfo{
|
||||
{Index: 5314, InitState: raid.AntonDungeonOpen, HP: 1, PartyMax: 0},
|
||||
{Index: 5315, InitState: raid.AntonDungeonHide, HP: 1, PartyMax: 0},
|
||||
}},
|
||||
}
|
||||
lifecycle, err := newRuntimeRaidLifecycleStateFromAntonModeAndType(config, preparedAt, true, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("create single lifecycle: %v", err)
|
||||
}
|
||||
member := runtimeRaidMemberState{CharID: 1001, Name: "leader", GroupIndex: 1, SlotOrder: 0}
|
||||
connection := &runtimeRaidPhaseTestConn{}
|
||||
queue := &runtimeRaidPhaseTestQueue{now: lifecycle.ReadyDeadline}
|
||||
const raidKey = uint32(0x00c803e9)
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
gameplayTimers: queue,
|
||||
raids: map[uint32]*runtimeRaidState{
|
||||
raidKey: {
|
||||
RaidKey: raidKey,
|
||||
LeaderID: member.CharID,
|
||||
AntonConfig: config,
|
||||
TailFlag: 1,
|
||||
Members: []runtimeRaidMemberState{member},
|
||||
FrozenMembers: []runtimeRaidMemberState{member},
|
||||
Started: true,
|
||||
EntryCostPaid: true,
|
||||
ChargedMembers: []uint16{member.CharID},
|
||||
StartAttempt: 1,
|
||||
Lifecycle: lifecycle,
|
||||
},
|
||||
},
|
||||
}
|
||||
session := &gameSession{conn: connection, connID: "raid-single-phase-runtime-test", channel: channelcatalog.Channel{ID: 200, Type: 23, Name: "anton", Port: 10038}}
|
||||
service.bindGameSessionCharacter(session, member.CharID)
|
||||
|
||||
if _, err := service.activateRuntimeRaidPhase(raidKey, runtimeRaidDungeonPhaseOne, lifecycle.ReadyDeadline, false); err != nil {
|
||||
t.Fatalf("activate single phase: %v", err)
|
||||
}
|
||||
wantIDs := []uint16{
|
||||
raid.RaidObjectMsgID,
|
||||
raid.RaidBuffMsgID,
|
||||
raid.RaidSymbolMsgID,
|
||||
raid.RaidDungeonStateMsgID,
|
||||
raid.RaidStateMsgID,
|
||||
raid.RaidDungeonChangeMsgID,
|
||||
raid.RaidTimerMsgID,
|
||||
raid.RaidRemainTimeMsgID,
|
||||
}
|
||||
wantDungeonBody, err := raid.BuildRaidDungeonStateBody(raid.RaidDungeonStateSnapshot{Rows: []raid.RaidDungeonStateRow{
|
||||
{DungeonID: 210, State: 0},
|
||||
{DungeonID: 211, State: 2},
|
||||
{DungeonID: 212, State: 2},
|
||||
{DungeonID: 213, State: 2},
|
||||
{DungeonID: 214, State: 2},
|
||||
{DungeonID: 215, State: 2},
|
||||
{DungeonID: 216, State: 2},
|
||||
{DungeonID: 218, State: 2},
|
||||
{DungeonID: 219, State: 2},
|
||||
{DungeonID: 220, State: 2},
|
||||
{DungeonID: 221, State: 2},
|
||||
{DungeonID: 222, State: 2},
|
||||
{DungeonID: 223, State: 2},
|
||||
{DungeonID: 224, State: 2},
|
||||
{DungeonID: 5314, State: 0},
|
||||
{DungeonID: 5315, State: 2},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("build expected single dungeon state: %v", err)
|
||||
}
|
||||
stream := connection.write.Bytes()
|
||||
for index, wantID := range wantIDs {
|
||||
packet, rest := splitGameServerUpperPacket(t, stream)
|
||||
stream = rest
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
||||
t.Fatalf("single phase packet %d class=%d msg=%#04x want=%#04x", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
||||
}
|
||||
switch wantID {
|
||||
case raid.RaidDungeonStateMsgID:
|
||||
if !bytes.Equal(packet.Body, wantDungeonBody) {
|
||||
t.Fatalf("single dungeon state body=%x want=%x", packet.Body, wantDungeonBody)
|
||||
}
|
||||
case raid.RaidDungeonChangeMsgID:
|
||||
want := []byte{0xd2, 0, 0, 0, 0, 0}
|
||||
if !bytes.Equal(packet.Body, want) {
|
||||
t.Fatalf("single start node refresh=%x want=%x", packet.Body, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(stream) != 0 {
|
||||
t.Fatalf("single phase trailing bytes=%d", len(stream))
|
||||
}
|
||||
|
||||
connection.write.Reset()
|
||||
if err := service.sendRuntimeRaidSelectorSnapshot(session, "single_selector_reopen"); err != nil {
|
||||
t.Fatalf("reopen single selector: %v", err)
|
||||
}
|
||||
wantSelectorIDs := []uint16{
|
||||
raid.RaidObjectMsgID,
|
||||
raid.RaidMemberRefreshMsgID,
|
||||
raid.RaidSymbolMsgID,
|
||||
raid.RaidDungeonStateMsgID,
|
||||
raid.RaidTimerMsgID,
|
||||
raid.RaidRemainTimeMsgID,
|
||||
}
|
||||
stream = connection.write.Bytes()
|
||||
for index, wantID := range wantSelectorIDs {
|
||||
packet, rest := splitGameServerUpperPacket(t, stream)
|
||||
stream = rest
|
||||
if packet.Header.Classification != 0 || packet.Header.MsgID != wantID {
|
||||
t.Fatalf("single selector packet %d class=%d msg=%#04x want=%#04x", index, packet.Header.Classification, packet.Header.MsgID, wantID)
|
||||
}
|
||||
if packet.Header.MsgID == raid.RaidStateMsgID {
|
||||
t.Fatalf("single selector replayed one-shot raid state=%x", packet.Body)
|
||||
}
|
||||
if packet.Header.MsgID == raid.RaidDungeonStateMsgID && !bytes.Equal(packet.Body, wantDungeonBody) {
|
||||
t.Fatalf("single selector dungeon state body=%x want=%x", packet.Body, wantDungeonBody)
|
||||
}
|
||||
if packet.Header.MsgID == raid.RaidParticipationMsgID || packet.Header.MsgID == raid.RaidDungeonChangeMsgID {
|
||||
t.Fatalf("single selector replayed one-shot bootstrap packet %#04x", packet.Header.MsgID)
|
||||
}
|
||||
}
|
||||
if len(stream) != 0 {
|
||||
t.Fatalf("single selector trailing bytes=%d", len(stream))
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeRaidObjectMode0PreservesLifecycleForSelectorReplay(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status runtimeRaidLifecycleStatus
|
||||
phase byte
|
||||
wantState byte
|
||||
wantPhase byte
|
||||
}{
|
||||
{name: "created", status: runtimeRaidLifecycleCreated, wantPhase: 0xff},
|
||||
{name: "preparing", status: runtimeRaidLifecyclePreparing, phase: runtimeRaidDungeonPhaseOne, wantPhase: 0xff},
|
||||
{name: "phase_one_active", status: runtimeRaidLifecyclePhaseActive, phase: runtimeRaidDungeonPhaseOne, wantState: 2, wantPhase: 0},
|
||||
{name: "phase_two_active", status: runtimeRaidLifecyclePhaseActive, phase: runtimeRaidDungeonPhaseTwo, wantState: 2, wantPhase: 1},
|
||||
{name: "awaiting_movie", status: runtimeRaidLifecycleAwaitingMovie, phase: runtimeRaidDungeonPhaseOne, wantState: 3, wantPhase: 0},
|
||||
{name: "awaiting_rewards", status: runtimeRaidLifecycleAwaitingRewards, phase: runtimeRaidDungeonPhaseOne, wantState: 5, wantPhase: 0},
|
||||
{name: "standby", status: runtimeRaidLifecycleStandby, phase: runtimeRaidDungeonPhaseOne, wantState: 5, wantPhase: 0},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
object := runtimeRaidObjectMode0(runtimeRaidState{
|
||||
RaidKey: 0x00170001,
|
||||
LeaderID: 1001,
|
||||
TailFlag: 1,
|
||||
Members: []runtimeRaidMemberState{{
|
||||
CharID: 1001,
|
||||
Name: "leader",
|
||||
}},
|
||||
Lifecycle: runtimeRaidLifecycleState{Status: test.status, Phase: test.phase},
|
||||
})
|
||||
if object.ObjectField28 != test.wantState || object.ObjectField30 != test.wantPhase {
|
||||
t.Fatalf("object lifecycle=%d/%d want=%d/%d", object.ObjectField28, object.ObjectField30, test.wantState, test.wantPhase)
|
||||
}
|
||||
if object.ObjectField94 != 1 || len(object.Members) != 1 {
|
||||
t.Fatalf("single/member projection field94=%d members=%d", object.ObjectField94, len(object.Members))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newRuntimeRaidPhaseActivationTestService(
|
||||
t *testing.T,
|
||||
preparedAt time.Time,
|
||||
connection *runtimeRaidPhaseTestConn,
|
||||
queue *runtimeRaidPhaseTestQueue,
|
||||
) (*Service, uint32, time.Time) {
|
||||
t.Helper()
|
||||
const raidKey = uint32(0x00c803e9)
|
||||
config := runtimeRaidBridgeTestAntonConfig()
|
||||
lifecycle, err := newRuntimeRaidLifecycleStateFromAnton(config, preparedAt)
|
||||
if err != nil {
|
||||
t.Fatalf("prepare raid lifecycle: %v", err)
|
||||
}
|
||||
member := runtimeRaidMemberState{CharID: 1001, Name: "leader", GroupIndex: 1, SlotOrder: 0}
|
||||
service := &Service{
|
||||
options: options{gameUpperBodyCodec: gameUpperBodyCodecPlain},
|
||||
gameplayTimers: queue,
|
||||
raids: map[uint32]*runtimeRaidState{
|
||||
raidKey: {
|
||||
RaidKey: raidKey,
|
||||
LeaderID: member.CharID,
|
||||
AntonConfig: config,
|
||||
Members: []runtimeRaidMemberState{member},
|
||||
FrozenMembers: []runtimeRaidMemberState{member},
|
||||
Started: true,
|
||||
EntryCostPaid: true,
|
||||
ChargedMembers: []uint16{member.CharID},
|
||||
StartAttempt: 1,
|
||||
Lifecycle: lifecycle,
|
||||
},
|
||||
},
|
||||
}
|
||||
session := &gameSession{
|
||||
conn: connection,
|
||||
connID: "raid-phase-runtime-test",
|
||||
channel: channelcatalog.Channel{ID: 200, Type: 23, Name: "anton", Port: 10038},
|
||||
}
|
||||
service.bindGameSessionCharacter(session, member.CharID)
|
||||
return service, raidKey, lifecycle.ReadyDeadline
|
||||
}
|
||||
|
||||
// assertRuntimeRaidPhaseActivationPackets pins the raid wire sequence of a phase
|
||||
// activation. The stream is filtered rather than read positionally, because
|
||||
// activation also re-asserts the squads' ordinary-party projection
|
||||
// (op9 -> op153) and installs each squad mate's actor record, and neither belongs
|
||||
// to the raid sequence this helper owns.
|
||||
func assertRuntimeRaidPhaseActivationPackets(t *testing.T, raw []byte, phase byte, wantSeconds uint32) {
|
||||
t.Helper()
|
||||
wantIDs := []uint16{
|
||||
raid.RaidObjectMsgID,
|
||||
raid.RaidBuffMsgID,
|
||||
raid.RaidSymbolMsgID,
|
||||
raid.RaidDungeonStateMsgID,
|
||||
raid.RaidStateMsgID,
|
||||
raid.RaidTimerMsgID,
|
||||
raid.RaidRemainTimeMsgID,
|
||||
}
|
||||
packets := make([]runtimeRaidRejoinExpectedPacket, 0, len(wantIDs))
|
||||
for len(raw) != 0 {
|
||||
packet, rest := splitGameServerUpperPacket(t, raw)
|
||||
raw = rest
|
||||
if !runtimeRaidRejoinIsRaidPacket(packet.Header.MsgID) {
|
||||
continue
|
||||
}
|
||||
packets = append(packets, runtimeRaidRejoinExpectedPacket{
|
||||
msgID: packet.Header.MsgID,
|
||||
classification: packet.Header.Classification,
|
||||
body: append([]byte(nil), packet.Body...),
|
||||
})
|
||||
}
|
||||
if len(packets) != len(wantIDs) {
|
||||
t.Fatalf("phase activation raid packets=%d want=%d", len(packets), len(wantIDs))
|
||||
}
|
||||
for index, wantID := range wantIDs {
|
||||
packet := packets[index]
|
||||
if packet.classification != 0 || packet.msgID != wantID {
|
||||
t.Fatalf("phase packet %d class=%d msg=%#04x want=%#04x", index, packet.classification, packet.msgID, wantID)
|
||||
}
|
||||
if wantID == raid.RaidObjectMsgID && len(packet.body) == 0 {
|
||||
t.Fatalf("phase activation emitted an empty raid object")
|
||||
}
|
||||
if packet.msgID == raid.RaidParticipationMsgID {
|
||||
t.Fatalf("phase activation emitted empty participation packet 0x0248")
|
||||
}
|
||||
switch wantID {
|
||||
case raid.RaidStateMsgID:
|
||||
wantBody := []byte{runtimeRaidPhaseStartState, phase - 1}
|
||||
if !bytes.Equal(packet.body, wantBody) {
|
||||
t.Fatalf("phase state body=%x want=%x", packet.body, wantBody)
|
||||
}
|
||||
case raid.RaidTimerMsgID:
|
||||
if len(packet.body) != 13 || packet.body[0] != 0 ||
|
||||
binary.LittleEndian.Uint32(packet.body[1:5]) != runtimeRaidTopTimerKey ||
|
||||
binary.LittleEndian.Uint32(packet.body[5:9]) == 0 ||
|
||||
binary.LittleEndian.Uint32(packet.body[9:13]) == 0 {
|
||||
t.Fatalf("phase timer body=%x", packet.body)
|
||||
}
|
||||
case raid.RaidRemainTimeMsgID:
|
||||
if len(packet.body) != 5 || packet.body[0] != 0 || binary.LittleEndian.Uint32(packet.body[1:]) != wantSeconds {
|
||||
t.Fatalf("phase remaining body=%x want_seconds=%d", packet.body, wantSeconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type runtimeRaidPhaseTestConn struct {
|
||||
bufferConn
|
||||
remainingFailures int
|
||||
writeCalls int
|
||||
failAt int
|
||||
writeErr error
|
||||
}
|
||||
|
||||
func (connection *runtimeRaidPhaseTestConn) Write(data []byte) (int, error) {
|
||||
connection.writeCalls++
|
||||
if connection.failAt > 0 && connection.writeCalls == connection.failAt {
|
||||
return 0, connection.writeErr
|
||||
}
|
||||
if connection.remainingFailures > 0 {
|
||||
connection.remainingFailures--
|
||||
return 0, connection.writeErr
|
||||
}
|
||||
return connection.bufferConn.Write(data)
|
||||
}
|
||||
|
||||
type runtimeRaidPhaseTestQueue struct {
|
||||
now time.Time
|
||||
remainingScheduleFailures int
|
||||
scheduleErr error
|
||||
scheduleCalls int
|
||||
scheduledName string
|
||||
scheduledDelay time.Duration
|
||||
callback timequeue.Callback
|
||||
}
|
||||
|
||||
func (queue *runtimeRaidPhaseTestQueue) Now() time.Time {
|
||||
return queue.now
|
||||
}
|
||||
|
||||
func (queue *runtimeRaidPhaseTestQueue) ScheduleAfter(
|
||||
name string,
|
||||
delay time.Duration,
|
||||
callback timequeue.Callback,
|
||||
) error {
|
||||
queue.scheduleCalls++
|
||||
queue.scheduledName = name
|
||||
queue.scheduledDelay = delay
|
||||
if queue.remainingScheduleFailures > 0 {
|
||||
queue.remainingScheduleFailures--
|
||||
return queue.scheduleErr
|
||||
}
|
||||
queue.callback = callback
|
||||
return nil
|
||||
}
|
||||
|
||||
func (queue *runtimeRaidPhaseTestQueue) RegisterDaily(string, int, int, timequeue.Callback) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (queue *runtimeRaidPhaseTestQueue) Cancel(string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (queue *runtimeRaidPhaseTestQueue) Start(context.Context) {}
|
||||
Reference in new issue
Block a user