DNF90 服务端与启动器完整源码(含 GM 工具功能增强)

服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
hanfuit committed 2026-11-04 14:31:55 +08:00
commit 645b64fe21
4388 files changed
+1405457

No files matched your search

@@ -0,0 +1,214 @@
package protocol
import "encoding/binary"
const (
TCPOuterHeaderSize = 12
InnerHeaderSize = 9
MinTCPFrameSize = TCPOuterHeaderSize + InnerHeaderSize + LatestGameHeaderSize
NormalInnerKind = 2
)
// TransportOptions 控制最新 game TCP/UDP 外壳字段。
type TransportOptions struct {
Sequence uint32
Route byte
Flag byte
OuterKind uint16
OuterValue4 uint32
OuterValue8 uint32
}
// LatestGameTransportHeader 是最新 game transport 外壳视图。
type LatestGameTransportHeader struct {
OuterKind uint16
TotalLength uint16
OuterValue4 uint32
OuterValue8 uint32
InnerKind byte
Sequence uint32
BusinessLength uint16
Route byte
Flag byte
}
// LatestGameTransportRecord 是一个 transport inner record。
type LatestGameTransportRecord struct {
TransportHeader LatestGameTransportHeader
GameHeader LatestGameHeader
Body []byte
}
// BuildLatestGameTCP 按最新 game TCP transport 封包。
//
// 对应 IDA:0x35F27E0 按 frame+2 的 u16 totalLen 拆 TCP 帧;
// 0x35EC7A0 跳过 12 字节外层;0x35EDED0 按 u8 kind、u32 seq、+5 长度、+9 payload 解析 kind=2 inner record。
func BuildLatestGameTCP(cmd byte, typ uint16, body []byte, options TransportOptions) ([]byte, error) {
businessPacket, err := BuildLatestGamePacket(cmd, typ, body)
if err != nil {
return nil, err
}
return BuildLatestGameTCPFromBusinessPacket(businessPacket, options)
}
// BuildLatestGameTCPFromBusinessPacket 把 7 字节业务包包进最新 TCP transport。
func BuildLatestGameTCPFromBusinessPacket(businessPacket []byte, options TransportOptions) ([]byte, error) {
if len(businessPacket) < LatestGameHeaderSize {
return nil, ErrPacketTooShort
}
if len(businessPacket) > int(^uint16(0)) {
return nil, ErrPacketTooLarge
}
total := TCPOuterHeaderSize + InnerHeaderSize + len(businessPacket)
if total > int(^uint16(0)) {
return nil, ErrPacketTooLarge
}
packet := make([]byte, total)
binary.LittleEndian.PutUint16(packet[0:2], options.OuterKind)
binary.LittleEndian.PutUint16(packet[2:4], uint16(total))
binary.LittleEndian.PutUint32(packet[4:8], options.OuterValue4)
binary.LittleEndian.PutUint32(packet[8:12], options.OuterValue8)
writeInnerRecord(packet[TCPOuterHeaderSize:], businessPacket, options)
return packet, nil
}
// BuildLatestGameUDP 按最新 game UDP inner record 封包。
func BuildLatestGameUDP(cmd byte, typ uint16, body []byte, options TransportOptions) ([]byte, error) {
businessPacket, err := BuildLatestGamePacket(cmd, typ, body)
if err != nil {
return nil, err
}
return BuildLatestGameUDPFromBusinessPacket(businessPacket, options)
}
// BuildLatestGameUDPFromBusinessPacket 把 7 字节业务包包进 UDP inner record。
func BuildLatestGameUDPFromBusinessPacket(businessPacket []byte, options TransportOptions) ([]byte, error) {
if len(businessPacket) < LatestGameHeaderSize {
return nil, ErrPacketTooShort
}
if len(businessPacket) > int(^uint16(0)) {
return nil, ErrPacketTooLarge
}
packet := make([]byte, InnerHeaderSize+len(businessPacket))
writeInnerRecord(packet, businessPacket, options)
return packet, nil
}
// ParseLatestGameTCPRecords 解析一个完整最新 game TCP frame。
func ParseLatestGameTCPRecords(frame []byte) ([]LatestGameTransportRecord, error) {
if len(frame) < TCPOuterHeaderSize {
return nil, ErrPacketTooShort
}
total := int(binary.LittleEndian.Uint16(frame[2:4]))
if total < MinTCPFrameSize || total > len(frame) {
return nil, ErrPacketLength
}
outerKind := binary.LittleEndian.Uint16(frame[0:2])
outerValue4 := binary.LittleEndian.Uint32(frame[4:8])
outerValue8 := binary.LittleEndian.Uint32(frame[8:12])
records := make([]LatestGameTransportRecord, 0, 1)
offset := TCPOuterHeaderSize
for offset < total {
record, consumed, err := parseInnerRecord(frame, offset, total-offset, outerKind, uint16(total), outerValue4, outerValue8)
if err != nil {
return nil, err
}
records = append(records, record)
offset += consumed
}
if len(records) == 0 {
return nil, ErrPacketLength
}
return records, nil
}
// ParseLatestGameUDPRecords 解析一个或多个 UDP inner record。
func ParseLatestGameUDPRecords(packet []byte) ([]LatestGameTransportRecord, error) {
if len(packet) < InnerHeaderSize+LatestGameHeaderSize {
return nil, ErrPacketTooShort
}
records := make([]LatestGameTransportRecord, 0, 1)
offset := 0
for offset < len(packet) {
record, consumed, err := parseInnerRecord(packet, offset, len(packet)-offset, 0, 0, 0, 0)
if err != nil {
return nil, err
}
records = append(records, record)
offset += consumed
}
if len(records) == 0 {
return nil, ErrPacketLength
}
return records, nil
}
// SplitLatestGameTCPFrames 从流式 buffer 中切出完整最新 game TCP frame。
//
// 如果 frame+2 的长度小于最小包长,按客户端拆帧逻辑跳 1 字节重同步。
func SplitLatestGameTCPFrames(buffer []byte) (frames [][]byte, remaining []byte, skipped int) {
offset := 0
for offset <= len(buffer)-4 {
frameLength := int(binary.LittleEndian.Uint16(buffer[offset+2 : offset+4]))
if frameLength < MinTCPFrameSize {
offset++
skipped++
continue
}
if len(buffer)-offset < frameLength {
break
}
frames = append(frames, cloneBytes(buffer[offset:offset+frameLength]))
offset += frameLength
}
return frames, cloneBytes(buffer[offset:]), skipped
}
func writeInnerRecord(target []byte, businessPacket []byte, options TransportOptions) {
target[0] = NormalInnerKind
binary.LittleEndian.PutUint32(target[1:5], options.Sequence)
binary.LittleEndian.PutUint16(target[5:7], uint16(len(businessPacket)))
target[7] = options.Route
target[8] = options.Flag
copy(target[InnerHeaderSize:], businessPacket)
}
func parseInnerRecord(data []byte, offset int, available int, outerKind uint16, tcpTotal uint16, outerValue4 uint32, outerValue8 uint32) (LatestGameTransportRecord, int, error) {
if available < InnerHeaderSize {
return LatestGameTransportRecord{}, 0, ErrPacketTooShort
}
innerKind := data[offset]
if innerKind != NormalInnerKind {
return LatestGameTransportRecord{}, 0, ErrInnerKind
}
sequence := binary.LittleEndian.Uint32(data[offset+1 : offset+5])
businessLength := int(binary.LittleEndian.Uint16(data[offset+5 : offset+7]))
route := data[offset+7]
flag := data[offset+8]
recordLength := InnerHeaderSize + businessLength
if businessLength < LatestGameHeaderSize || recordLength > available {
return LatestGameTransportRecord{}, 0, ErrPacketLength
}
businessOffset := offset + InnerHeaderSize
businessEnd := businessOffset + businessLength
packet, err := ParseLatestGamePacket(data[businessOffset:businessEnd])
if err != nil {
return LatestGameTransportRecord{}, 0, err
}
record := LatestGameTransportRecord{
TransportHeader: LatestGameTransportHeader{
OuterKind: outerKind,
TotalLength: tcpTotal,
OuterValue4: outerValue4,
OuterValue8: outerValue8,
InnerKind: innerKind,
Sequence: sequence,
BusinessLength: uint16(businessLength),
Route: route,
Flag: flag,
},
GameHeader: packet.Header,
Body: packet.Body,
}
return record, recordLength, nil
}