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

322 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package dnfbridge
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"longheng.io/server/internal/modules/dnf/dproto"
dnfproto "longheng.io/server/internal/modules/dnf/protocol"
)
func (s *Service) gameDprotoMode() string {
if s == nil {
return gameDprotoModeLegacy
}
return normalizeGameDprotoMode(s.options.gameDprotoMode)
}
func (s *Service) gameDprotoMaxPacketBytes() int {
if s != nil && s.options.maxPacketBytes > 0 {
return s.options.maxPacketBytes
}
return defaultMaxPacketBytes
}
func (s *Service) validateDprotoConfiguration() error {
if s.gameDprotoMode() == gameDprotoModeNative && s.dprotoProvider == nil {
return dproto.ErrProviderUnavailable
}
return nil
}
func (s *Service) openGameDprotoSession(ctx context.Context, session *gameSession) error {
if s.gameDprotoMode() != gameDprotoModeNative {
return nil
}
if s.dprotoProvider == nil {
return dproto.ErrProviderUnavailable
}
info := dproto.ConnectionInfo{
ConnectionID: session.connID,
ChannelID: session.channel.ID,
}
if session.conn != nil {
if address := session.conn.RemoteAddr(); address != nil {
info.RemoteAddr = address.String()
}
if address := session.conn.LocalAddr(); address != nil {
info.LocalAddr = address.String()
}
}
nativeSession, err := s.dprotoProvider.Open(ctx, info)
if err != nil {
return err
}
if nativeSession == nil {
return dproto.ErrProviderUnavailable
}
session.dproto = nativeSession
s.logGameEvent(session, "game-dproto-opened", "mode", gameDprotoModeNative)
return nil
}
func (s *Service) closeGameDprotoSession(session *gameSession) {
if session == nil || session.dproto == nil {
return
}
session.wireMu.Lock()
err := session.dproto.Close()
session.dproto = nil
session.wireMu.Unlock()
if err != nil {
s.logGameEvent(session, "game-dproto-close-failed", "error", err)
}
}
func (s *Service) handleGameDprotoPacket(session *gameSession, raw []byte) error {
if session.dproto == nil {
envelope, err := dnfproto.ParseDprotoClientEnvelope(raw, s.gameDprotoMaxPacketBytes())
if err != nil {
s.logGameEvent(session, "game-dproto-opaque-parse-failed",
"packet_len", len(raw),
"reason", "native_dproto_provider_unavailable_and_outer_envelope_invalid",
"error", err)
return nil
}
s.logGameEvent(session, "game-dproto-opaque-deferred",
"outer_sequence", envelope.Header.Seq,
"protected_len", len(envelope.Protected),
"reason", "native_dproto_provider_unavailable")
return nil
}
maxPacketBytes := s.gameDprotoMaxPacketBytes()
envelope, err := dnfproto.ParseDprotoClientEnvelope(raw, maxPacketBytes)
if err != nil {
return err
}
ctx := session.ctx
if ctx == nil {
ctx = context.Background()
}
var decoded dproto.DecodeResult
err = s.withGameWire(session, func() error {
var decodeErr error
decoded, decodeErr = session.dproto.DecodeClient(ctx, envelope.Raw)
if decodeErr != nil {
return decodeErr
}
return s.writeDprotoProviderPacketsLocked(session, decoded.OutboundPackets, "client_decode_control")
})
if err != nil {
return err
}
s.logGameEvent(session, "game-dproto-client-decoded",
"outer_sequence", envelope.Header.Seq,
"protected_len", len(envelope.Protected),
"inner_count", len(decoded.InnerPackets),
"control_count", len(decoded.OutboundPackets))
for _, inner := range decoded.InnerPackets {
if len(inner) > maxPacketBytes {
return dnfproto.ErrPacketTooLarge
}
packet, err := dnfproto.ParseChannelPacket(inner)
if err != nil {
return fmt.Errorf("validate decoded inner upper: %w", err)
}
if packet.Header.MsgID == dnfproto.DprotoClientEnvelopeOpcode ||
packet.Header.MsgID == dnfproto.DprotoServerEnvelopeOpcode {
return fmt.Errorf("decoded nested dproto opcode %d: %w", packet.Header.MsgID, dnfproto.ErrDprotoEnvelopeOpcode)
}
if err := s.handleGameUpper(session, inner); err != nil {
return err
}
}
return nil
}
func (s *Service) handleGameDprotoControl(session *gameSession, opcode uint16, raw []byte) error {
if session == nil || session.dproto == nil {
return dproto.ErrProviderUnavailable
}
ctx := session.ctx
if ctx == nil {
ctx = context.Background()
}
return s.withGameWire(session, func() error {
outbound, err := session.dproto.HandleClientControl(ctx, opcode, append([]byte(nil), raw...))
if err != nil {
return err
}
return s.writeDprotoProviderPacketsLocked(session, outbound, "client_control")
})
}
// gameConnWriteBudget 是单次游戏帧写入的期限。
//
// 这里的写全部发生在该会话【唯一】的 event loop 上。客户端一旦停止收包
// (家宽出口饱和、同机多开、进程卡住、机器休眠),conn.Write 会一直阻塞,
// 于是这条会话再也发不出任何包;而小队扇出会通过 callGameSession 同步等待
// 成员会话,阻塞还会向外传染给队长,再叠加入场栅栏就是整队一起卡死。
//
// 这正是 2026-09-24 23:24 那次 8 人双小队进黑雾之源集体掉线的链路:
// 服务端对外静默 22 秒 → 客户端超时 → 自己 RST。
//
// 这条期限把「无界阻塞」变成「有界阻塞」:超时就返回错误、让这条连接自己
// 被拆除,而不是拖住整条 event loop 乃至整支小队。
const gameConnWriteBudget = 5 * time.Second
// writeGameConn is the single exit for every game frame written to a client.
//
// It blocks only for as long as the session writer is allowed to make the loop
// wait. With no writer (a bare session, which is what unit tests build) it is
// the synchronous write it has always been, including its exact error.
func (s *Service) writeGameConn(session *gameSession, payload []byte) error {
if session == nil || session.conn == nil {
return errors.New("dnf game connection is unavailable")
}
if session.writer != nil {
return session.writer.submit(payload)
}
return s.writeGameConnDirect(session, payload)
}
// writeGameConnDirect is the historical body of writeGameConn: one bounded
// blocking write on the caller's goroutine.
func (s *Service) writeGameConnDirect(session *gameSession, payload []byte) error {
if session == nil || session.conn == nil {
return errors.New("dnf game connection is unavailable")
}
if err := session.conn.SetWriteDeadline(time.Now().Add(gameConnWriteBudget)); err != nil {
return err
}
defer func() { _ = session.conn.SetWriteDeadline(time.Time{}) }()
_, err := session.conn.Write(payload)
return err
}
func (s *Service) withGameWire(session *gameSession, operation func() error) error {
if session == nil || session.conn == nil {
return errors.New("dnf game connection is unavailable")
}
session.wireMu.Lock()
defer session.wireMu.Unlock()
return operation()
}
func (s *Service) writeGameRawPacket(session *gameSession, packet []byte) error {
return s.withGameWire(session, func() error {
return s.writeGameRawPacketLocked(session, packet)
})
}
func (s *Service) writeGameRawPacketLocked(session *gameSession, packet []byte) error {
return s.writeGameConn(session, packet)
}
func (s *Service) writeGameUpperPacket(session *gameSession, inner []byte) error {
return s.withGameWire(session, func() error {
return s.writeGameUpperPacketLocked(session, inner)
})
}
func (s *Service) writeGameUpperPacketLocked(session *gameSession, inner []byte) error {
// 诊断(只读):记住本会话最后发出的 upper 包,并在 op38 应答时结束卡住计量。
// 取样用**语义包** inner,dproto 信封不会让 opcode 失真;对发包结果没有任何影响。
s.noteUpperSendDiagnostics(session, inner)
wire := inner
protected := false
if session.dproto != nil {
ctx := session.ctx
if ctx == nil {
ctx = context.Background()
}
encoded, err := session.dproto.EncodeServer(ctx, append([]byte(nil), inner...))
if err != nil {
return err
}
if len(encoded.Packet) == 0 {
return dproto.ErrEmptyProviderOutput
}
wire = encoded.Packet
protected = encoded.Protected
outer, err := dnfproto.ParseChannelPacketUnchecked(wire)
if err != nil {
return fmt.Errorf("validate dproto server output: %w", err)
}
if protected {
if outer.Header.MsgID != dnfproto.DprotoServerEnvelopeOpcode {
return fmt.Errorf("protected server opcode %d: %w", outer.Header.MsgID, dnfproto.ErrDprotoEnvelopeOpcode)
}
} else if !bytes.Equal(wire, inner) {
return errors.New("dnf dproto provider mutated an unprotected server packet")
}
s.logGameEvent(session, "game-dproto-server-encoded",
"inner_len", len(inner),
"wire_len", len(wire),
"protected", protected,
"outer_msg_id", outer.Header.MsgID)
}
if session.upperWriteBatchActive {
session.upperWriteBatch = append(session.upperWriteBatch, wire...)
return nil
}
return s.writeGameConn(session, wire)
}
func (s *Service) beginGameUpperWriteBatch(session *gameSession) error {
return s.withGameWire(session, func() error {
if session.upperWriteBatchActive {
return errors.New("dnf game upper write batch is already active")
}
session.upperWriteBatchActive = true
session.upperWriteBatch = session.upperWriteBatch[:0]
return nil
})
}
func (s *Service) flushGameUpperWriteBatch(session *gameSession) error {
return s.withGameWire(session, func() error {
if !session.upperWriteBatchActive {
return errors.New("dnf game upper write batch is not active")
}
wire := append([]byte(nil), session.upperWriteBatch...)
session.upperWriteBatchActive = false
session.upperWriteBatch = session.upperWriteBatch[:0]
if len(wire) == 0 {
return nil
}
return s.writeGameConn(session, wire)
})
}
func (s *Service) writeDprotoProviderPacketsLocked(session *gameSession, packets [][]byte, owner string) error {
for _, packet := range packets {
if len(packet) == 0 {
return dproto.ErrEmptyProviderOutput
}
if len(packet) > s.gameDprotoMaxPacketBytes() {
return dnfproto.ErrPacketTooLarge
}
outer, err := dnfproto.ParseChannelPacketUnchecked(packet)
if err != nil {
return fmt.Errorf("validate dproto control output: %w", err)
}
switch outer.Header.MsgID {
case dnfproto.DprotoServerControlOpcode, dnfproto.DprotoServerEnvelopeOpcode:
default:
return fmt.Errorf("dproto control opcode %d: %w", outer.Header.MsgID, dnfproto.ErrDprotoEnvelopeOpcode)
}
s.logGameEvent(session, "game-dproto-control-send",
"owner", owner,
"msg_id", outer.Header.MsgID,
"packet_len", len(packet))
if err := s.writeGameConn(session, packet); err != nil {
return err
}
}
return nil
}