Files
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

193 lines
6.7 KiB
Go

package dnfbridge
import (
"bytes"
"errors"
"testing"
"longheng.io/server/internal/modules/dnf/channelcatalog"
"longheng.io/server/internal/modules/dnf/raid"
)
func requireCurrentRaidAvailabilityPacket(t *testing.T, stream []byte) []byte {
t.Helper()
packet, rest := splitGameServerUpperPacket(t, stream)
if packet.Header.Classification != 0 || packet.Header.MsgID != raid.RaidAvailabilityMsgID {
t.Fatalf("raid availability = class %d msg %#x body=%x", packet.Header.Classification, packet.Header.MsgID, packet.Body)
}
want := raid.BuildNativeRaidAvailabilityBody()
if !bytes.Equal(packet.Body, want) {
t.Fatalf("raid availability body=%x, want %x", packet.Body, want)
}
return rest
}
func requireCurrentRaidDirectoryReadyPacket(t *testing.T, stream []byte) []byte {
t.Helper()
packet, rest := splitGameServerUpperPacket(t, stream)
if packet.Header.Classification != 0 || packet.Header.MsgID != raid.RaidDirectoryReadyMsgID ||
len(packet.Body) != 1 || packet.Body[0] != 0 {
t.Fatalf("raid directory ready = class %d msg %d body=%x", packet.Header.Classification, packet.Header.MsgID, packet.Body)
}
return rest
}
func TestCurrentRaidAvailabilitySendsFixedBodyOnce(t *testing.T) {
connection := &bufferConn{}
session := &gameSession{conn: connection, channel: channelcatalog.Channel{ID: 23, Type: 23, Name: "anton"}}
service := &Service{}
if err := service.sendCurrentRaidAvailabilityOnce(session, "test_initial"); err != nil {
t.Fatal(err)
}
if !session.raidAvailabilitySent {
t.Fatal("successful availability write did not mark the session")
}
if rest := requireCurrentRaidAvailabilityPacket(t, connection.write.Bytes()); len(rest) != 0 {
t.Fatalf("availability packet left trailing bytes: %x", rest)
}
connection.write.Reset()
if err := service.sendCurrentRaidAvailabilityOnce(session, "test_repeat"); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("repeated availability write emitted %d bytes", connection.write.Len())
}
}
func TestCurrentRaidAvailabilityWriteFailureRemainsRetryable(t *testing.T) {
wantErr := errors.New("raid availability write failed")
connection := &failNthDungeonWriteConn{failAt: 1, err: wantErr}
session := &gameSession{conn: connection}
service := &Service{}
if err := service.sendCurrentRaidAvailabilityOnce(session, "test_failure"); !errors.Is(err, wantErr) {
t.Fatalf("send error=%v, want %v", err, wantErr)
}
if session.raidAvailabilitySent {
t.Fatal("failed availability write marked the session")
}
session.conn = &bufferConn{}
if err := service.sendCurrentRaidAvailabilityOnce(session, "test_retry"); err != nil {
t.Fatal(err)
}
if !session.raidAvailabilitySent {
t.Fatal("successful availability retry did not mark the session")
}
}
func TestCurrentRaidDirectoryReadySendsOnceOnlyForTeamChannels(t *testing.T) {
for _, channelType := range []uint8{23, 32, 42} {
connection := &bufferConn{}
session := &gameSession{
conn: connection,
channel: channelcatalog.Channel{Type: channelType},
sceneBootstrapTailSent: true,
}
service := &Service{}
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "test_initial"); err != nil {
t.Fatal(err)
}
if !session.raidDirectoryReadySent {
t.Fatalf("channel type %d was not marked ready", channelType)
}
if rest := requireCurrentRaidDirectoryReadyPacket(t, connection.write.Bytes()); len(rest) != 0 {
t.Fatalf("channel type %d left trailing bytes: %x", channelType, rest)
}
connection.write.Reset()
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "test_repeat"); err != nil {
t.Fatal(err)
}
if connection.write.Len() != 0 {
t.Fatalf("channel type %d repeated ready write emitted %d bytes", channelType, connection.write.Len())
}
}
connection := &bufferConn{}
session := &gameSession{
conn: connection,
channel: channelcatalog.Channel{Type: 11},
sceneBootstrapTailSent: true,
}
if err := (&Service{}).sendCurrentRaidDirectoryReadyOnce(session, "ordinary_channel"); err != nil {
t.Fatal(err)
}
if session.raidDirectoryReadySent || connection.write.Len() != 0 {
t.Fatalf("ordinary channel emitted raid directory ready: sent=%t bytes=%d", session.raidDirectoryReadySent, connection.write.Len())
}
}
func TestCurrentRaidDirectoryReadyWaitsForFinalizedScene(t *testing.T) {
connection := &bufferConn{}
session := &gameSession{
conn: connection,
channel: channelcatalog.Channel{Type: 23},
sceneBootstrapTailDeferred: true,
}
service := &Service{}
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "before_scene_tail"); err != nil {
t.Fatal(err)
}
if session.raidDirectoryReadySent || connection.write.Len() != 0 {
t.Fatalf("pre-scene ready sent=%t bytes=%d", session.raidDirectoryReadySent, connection.write.Len())
}
session.sceneBootstrapTailDeferred = false
session.sceneBootstrapTailSent = true
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "after_scene_tail"); err != nil {
t.Fatal(err)
}
if !session.raidDirectoryReadySent {
t.Fatal("finalized scene did not send raid directory readiness")
}
if rest := requireCurrentRaidDirectoryReadyPacket(t, connection.write.Bytes()); len(rest) != 0 {
t.Fatalf("finalized scene ready left trailing bytes: %x", rest)
}
}
func TestCurrentRaidDirectoryReadyFailureRemainsRetryable(t *testing.T) {
wantErr := errors.New("raid directory ready write failed")
session := &gameSession{
conn: &failNthDungeonWriteConn{failAt: 1, err: wantErr},
channel: channelcatalog.Channel{Type: 23},
sceneBootstrapTailSent: true,
}
service := &Service{}
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "test_failure"); !errors.Is(err, wantErr) {
t.Fatalf("send error=%v, want %v", err, wantErr)
}
if session.raidDirectoryReadySent {
t.Fatal("failed ready write marked the session")
}
session.conn = &bufferConn{}
if err := service.sendCurrentRaidDirectoryReadyOnce(session, "test_retry"); err != nil {
t.Fatal(err)
}
if !session.raidDirectoryReadySent {
t.Fatal("successful ready retry did not mark the session")
}
}
func TestCurrentRaidDirectoryReadyResetsWhenCharacterChanges(t *testing.T) {
session := &gameSession{channel: channelcatalog.Channel{Type: 23}}
service := &Service{}
service.bindGameSessionCharacter(session, 1001)
session.raidDirectoryReadySent = true
service.bindGameSessionCharacter(session, 1001)
if !session.raidDirectoryReadySent {
t.Fatal("rebinding the same character reset raid directory readiness")
}
service.bindGameSessionCharacter(session, 1002)
if session.raidDirectoryReadySent {
t.Fatal("changing character preserved stale raid directory readiness")
}
}