服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
215 lines
7.1 KiB
Go
215 lines
7.1 KiB
Go
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
|
||
}
|