服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
129 lines
4.3 KiB
Go
129 lines
4.3 KiB
Go
package protocol
|
||
|
||
import "encoding/binary"
|
||
|
||
const (
|
||
ChannelHeaderSize = 13
|
||
GameServerUpperHeaderSize = ChannelHeaderSize
|
||
GameServerUpperHeaderSize16 = 16
|
||
DefaultChannelClassification = 1
|
||
)
|
||
|
||
// ChannelHeader 是客户端发来的 raw upper 13 字节包头。
|
||
type ChannelHeader struct {
|
||
Classification byte
|
||
MsgID uint16
|
||
Length uint32
|
||
Checksum uint32
|
||
Seq uint16
|
||
}
|
||
|
||
type ChannelPacket struct {
|
||
Header ChannelHeader
|
||
Body []byte
|
||
}
|
||
|
||
// BuildChannelPacket 封装客户端到服务端方向使用的 13 字节 raw upper 包。
|
||
func BuildChannelPacket(msgID uint16, body []byte, seq uint16, classification byte) ([]byte, error) {
|
||
if classification == 0 {
|
||
classification = DefaultChannelClassification
|
||
}
|
||
total := ChannelHeaderSize + len(body)
|
||
if total > int(^uint32(0)) {
|
||
return nil, ErrPacketTooLarge
|
||
}
|
||
packet := make([]byte, total)
|
||
packet[0] = classification
|
||
binary.LittleEndian.PutUint16(packet[1:3], msgID)
|
||
binary.LittleEndian.PutUint32(packet[3:7], uint32(total))
|
||
binary.LittleEndian.PutUint16(packet[11:13], seq)
|
||
copy(packet[ChannelHeaderSize:], body)
|
||
sum, err := ChecksumRange(packet, 11, total-11)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
binary.LittleEndian.PutUint32(packet[7:11], sum)
|
||
return packet, nil
|
||
}
|
||
|
||
// BuildGameServerUpperPacket 封装 game 端口服务端发给客户端的 raw upper 包。
|
||
// 最新 C# 重写文件与 7001 upper 一致使用 13 字节头;16 字节头只保留为兼容实验。
|
||
func BuildGameServerUpperPacket(msgID uint16, body []byte, seq uint16, classification byte) ([]byte, error) {
|
||
return BuildGameServerUpperPacketWithHeaderSize(msgID, body, seq, classification, GameServerUpperHeaderSize)
|
||
}
|
||
|
||
// BuildGameServerUpperPacketWithHeaderSize 按指定头长封装 game raw upper。
|
||
//
|
||
// headerSize 只允许 13 或 16。16 字节模式会在 seq 后补 3 个保留字节,
|
||
// 用于回退旧实验;主路径必须保持 13 字节,避免客户端按 ChannelPacket 读体时错位。
|
||
func BuildGameServerUpperPacketWithHeaderSize(msgID uint16, body []byte, seq uint16, classification byte, headerSize int) ([]byte, error) {
|
||
if headerSize != ChannelHeaderSize && headerSize != GameServerUpperHeaderSize16 {
|
||
return nil, ErrPacketLength
|
||
}
|
||
total := headerSize + len(body)
|
||
if total > int(^uint32(0)) {
|
||
return nil, ErrPacketTooLarge
|
||
}
|
||
packet := make([]byte, total)
|
||
packet[0] = classification
|
||
binary.LittleEndian.PutUint16(packet[1:3], msgID)
|
||
binary.LittleEndian.PutUint32(packet[3:7], uint32(total))
|
||
binary.LittleEndian.PutUint16(packet[11:13], seq)
|
||
copy(packet[headerSize:], body)
|
||
sum, err := ChecksumRange(packet, 11, total-11)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
binary.LittleEndian.PutUint32(packet[7:11], sum)
|
||
return packet, nil
|
||
}
|
||
|
||
func ParseChannelPacket(packet []byte) (ChannelPacket, error) {
|
||
return parseChannelPacket(packet, true)
|
||
}
|
||
|
||
// ParseChannelPacketUnchecked 只按 13 字节 upper 包头切 body,不校验 checksum。
|
||
//
|
||
// 最新 game 端口上行的部分 raw upper 包会在 body 里携带 codec/encrypted payload,
|
||
// 线上的 0x02B2 样本显示同 msg/length 下 body 每次变化但 header checksum 固定。
|
||
// 分流阶段必须先保住 msg/length 语义,checksum 只能作为后续诊断信号。
|
||
func ParseChannelPacketUnchecked(packet []byte) (ChannelPacket, error) {
|
||
return parseChannelPacket(packet, false)
|
||
}
|
||
|
||
func parseChannelPacket(packet []byte, verifyChecksum bool) (ChannelPacket, error) {
|
||
if len(packet) < ChannelHeaderSize {
|
||
return ChannelPacket{}, ErrPacketTooShort
|
||
}
|
||
header := ChannelHeader{
|
||
Classification: packet[0],
|
||
MsgID: binary.LittleEndian.Uint16(packet[1:3]),
|
||
Length: binary.LittleEndian.Uint32(packet[3:7]),
|
||
Checksum: binary.LittleEndian.Uint32(packet[7:11]),
|
||
Seq: binary.LittleEndian.Uint16(packet[11:13]),
|
||
}
|
||
if header.Length < ChannelHeaderSize || int(header.Length) != len(packet) {
|
||
return ChannelPacket{}, ErrPacketLength
|
||
}
|
||
if verifyChecksum {
|
||
sum, err := ChecksumRange(packet, 11, len(packet)-11)
|
||
if err != nil {
|
||
return ChannelPacket{}, err
|
||
}
|
||
if sum != header.Checksum {
|
||
return ChannelPacket{}, ErrChecksumInvalid
|
||
}
|
||
}
|
||
return ChannelPacket{
|
||
Header: header,
|
||
Body: cloneBytes(packet[ChannelHeaderSize:]),
|
||
}, nil
|
||
}
|
||
|
||
func cloneBytes(data []byte) []byte {
|
||
if len(data) == 0 {
|
||
return nil
|
||
}
|
||
return append([]byte(nil), data...)
|
||
}
|