Files
90CNServer/server/internal/modules/dnf/party/request.go
T
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

290 lines
10 KiB
Go
Raw 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 party
import (
"encoding/binary"
"errors"
)
// SetPartyInfoRequest 是创建/修改队伍弹窗提交后的已知字段。
type SetPartyInfoRequest struct {
Prefix0 byte
Prefix1 byte
NameBytes []byte
MemberSelectCode byte
MaxMembers byte
SelectionID uint32
SelectionCode byte
SelectionValue uint16
RecruitFlag byte
TargetMode byte
TargetDungeonID uint16
}
// QuickPartyTarget 是 0x01BB 快速组队登记里的一条目标记录。
type QuickPartyTarget struct {
TargetID uint32
Available byte
}
// RegisterQuickPartyRequest 保存 0x01BB 的批量目标列表。
type RegisterQuickPartyRequest struct {
Targets []QuickPartyTarget
}
// ReserveLeavePartyRequest 保存 0x02B3 预约离队/取消预约的本地状态位。
type ReserveLeavePartyRequest struct {
Flag byte
}
// EntryIntoPartyRequest 保存 0x02C1 申请进入队伍时携带的目标角色/对象 ID。
type EntryIntoPartyRequest struct {
TargetID uint32
}
// ResponsePeerRequest 保存 0x000B 的通用 peer 回应体;组队邀请只使用其中 mode=4/13 的分支。
type ResponsePeerRequest struct {
TargetID uint16
Mode byte
Value uint32
}
// RequestPeerRequest 保存 0x000A 的通用 peer 请求体。
// 当前 EXE 按 mode 使用两种尾部:mode 0/10/12/15 写
// u32 value0,u16 value1,u32 value2;mode 1/4/13 写
// u32 value0,u32 value2。
type RequestPeerRequest struct {
TargetID uint16
Mode byte
Value0 uint32
Value1 uint16
Value2 uint32
}
// WalkoutPartyMemberRequest 保存 0x000E 队长踢出队员时写入的槽位号。
type WalkoutPartyMemberRequest struct {
Slot byte
}
// ChangePartyMemberPositionRequest 保存 0x014F 队伍槽位位置调整请求。
type ChangePartyMemberPositionRequest struct {
Slot byte
Position byte
}
var errSetPartyInfoBodyShort = errors.New("set party info body too short")
var errRegisterQuickPartyBodyShort = errors.New("register quick party body too short")
var errReserveLeavePartyBodyShort = errors.New("reserve leave party body too short")
var errEntryIntoPartyBodyShort = errors.New("entry into party body too short")
var errRequestPeerBodyShort = errors.New("request peer body too short")
var errResponsePeerBodyShort = errors.New("response peer body too short")
var errWalkoutPartyMemberBodyShort = errors.New("walkout party member body too short")
var errChangePartyMemberPositionBodyShort = errors.New("change party member position body too short")
var errChangePartyMemberPositionInvalid = errors.New("change party member position invalid")
var partyNamePresetTable = [][]byte{
[]byte("\u5F3A\u5F3A\u8054\u5408~"),
[]byte("\u75AF\u72C2\u7EC3\u7EA7\uFF01 \u6709\u610F\u8005\u901F\u5EA6\u7EC4\u961F\uFF01 "),
[]byte("\u4EBA\u54C1\u597D\u7684\uFF0C \u8BF7\u6765\u8FD9\u91CC\u5427\u3002 "),
[]byte("\u53D7\u8D5B\u4E3D\u4E9A\u795D\u798F\u7684\u961F\u4F0D"),
[]byte("\u5237\u7A00\u6709\u88C5\u5907\uFF0C \u6709\u610F\u8005\u901F\u6765\u3002 "),
[]byte("\u52DF\u96C6\u5F3A\u8005\u961F\u4F0D\u3002 "),
}
// DecodeSetPartyInfoRequest follows the current NoPack.exe op12 writer:
// mode,u8 namePreset,[DSTR customName],u8 memberCode,u32 selectionID,
// u8 selectionCode,u16 selectionValue,u8 recruitFlag,u8 targetMode,u16 targetID.
func DecodeSetPartyInfoRequest(body []byte) (SetPartyInfoRequest, error) {
const fixedTailSize = 12
if len(body) < 2+fixedTailSize {
return SetPartyInfoRequest{}, errSetPartyInfoBodyShort
}
req := SetPartyInfoRequest{
Prefix0: body[0],
Prefix1: body[1],
MaxMembers: 4,
}
tailOffset := 2
if req.Prefix1 == 0 {
if len(body) < 6+fixedTailSize {
return SetPartyInfoRequest{}, errSetPartyInfoBodyShort
}
nameLen := int(int32(binary.LittleEndian.Uint32(body[2:6])))
if nameLen < 0 || nameLen > 128 || 6+nameLen+fixedTailSize != len(body) {
return SetPartyInfoRequest{}, errSetPartyInfoBodyShort
}
req.NameBytes = append([]byte(nil), body[6:6+nameLen]...)
tailOffset = 6 + nameLen
} else if len(body) != 2+fixedTailSize {
return SetPartyInfoRequest{}, errSetPartyInfoBodyShort
}
if len(body)-tailOffset != fixedTailSize {
return SetPartyInfoRequest{}, errSetPartyInfoBodyShort
}
req.MemberSelectCode = body[tailOffset]
req.SelectionID = binary.LittleEndian.Uint32(body[tailOffset+1 : tailOffset+5])
req.SelectionCode = body[tailOffset+5]
req.SelectionValue = binary.LittleEndian.Uint16(body[tailOffset+6 : tailOffset+8])
req.RecruitFlag = body[tailOffset+8]
req.TargetMode = body[tailOffset+9]
req.TargetDungeonID = binary.LittleEndian.Uint16(body[tailOffset+10 : tailOffset+12])
req.MaxMembers = decodePartyMaxMembers(req.MemberSelectCode)
if len(req.NameBytes) == 0 {
req.NameBytes = resolvePartyNamePreset(req.Prefix1)
}
return req, nil
}
// DecodeRegisterQuickPartyRequest 按 EXE 0x21B20C0 读取顺序解析:u32 count + count*(u32 target,u8 available)。
func DecodeRegisterQuickPartyRequest(body []byte) (RegisterQuickPartyRequest, error) {
if len(body) < 4 {
return RegisterQuickPartyRequest{}, errRegisterQuickPartyBodyShort
}
count := binary.LittleEndian.Uint32(body[:4])
if count > uint32((len(body)-4)/5) {
return RegisterQuickPartyRequest{}, errRegisterQuickPartyBodyShort
}
need := 4 + int(count)*5
if len(body) != need {
return RegisterQuickPartyRequest{}, errRegisterQuickPartyBodyShort
}
req := RegisterQuickPartyRequest{Targets: make([]QuickPartyTarget, 0, int(count))}
offset := 4
for i := uint32(0); i < count; i++ {
req.Targets = append(req.Targets, QuickPartyTarget{
TargetID: binary.LittleEndian.Uint32(body[offset : offset+4]),
Available: body[offset+4],
})
offset += 5
}
return req, nil
}
// DecodeReserveLeavePartyRequest 按 EXE 0x2B3 发送点解析单字节 flag。
func DecodeReserveLeavePartyRequest(body []byte) (ReserveLeavePartyRequest, error) {
if len(body) != 1 {
return ReserveLeavePartyRequest{}, errReserveLeavePartyBodyShort
}
return ReserveLeavePartyRequest{Flag: body[0]}, nil
}
// DecodeEntryIntoPartyRequest 按 EXE 0x12C8B47 发送点解析:body 只写入一个 u32 目标 ID。
func DecodeEntryIntoPartyRequest(body []byte) (EntryIntoPartyRequest, error) {
if len(body) != 4 {
return EntryIntoPartyRequest{}, errEntryIntoPartyBodyShort
}
return EntryIntoPartyRequest{TargetID: binary.LittleEndian.Uint32(body[:4])}, nil
}
// DecodeRequestPeerRequest 按当前 EXE sub_325F9D0 等发送点解析:
// u16 target + u8 mode + u32 value0 + u16 value1 + u32 value2。
func DecodeRequestPeerRequest(body []byte) (RequestPeerRequest, error) {
if len(body) < 7 {
return RequestPeerRequest{}, errRequestPeerBodyShort
}
req := RequestPeerRequest{
TargetID: binary.LittleEndian.Uint16(body[:2]),
Mode: body[2],
Value0: binary.LittleEndian.Uint32(body[3:7]),
}
switch req.Mode {
case 0, 10, 12, 15:
if len(body) != 13 {
return RequestPeerRequest{}, errRequestPeerBodyShort
}
req.Value1 = binary.LittleEndian.Uint16(body[7:9])
req.Value2 = binary.LittleEndian.Uint32(body[9:13])
case 1, 4, 13:
if len(body) != 11 {
return RequestPeerRequest{}, errRequestPeerBodyShort
}
req.Value2 = binary.LittleEndian.Uint32(body[7:11])
default:
if len(body) != 7 {
return RequestPeerRequest{}, errRequestPeerBodyShort
}
}
return req, nil
}
// DecodeResponsePeerRequest 按 EXE 发送点解析:u16 target + u8 mode + u32 value。
func DecodeResponsePeerRequest(body []byte) (ResponsePeerRequest, error) {
if len(body) != 7 {
return ResponsePeerRequest{}, errResponsePeerBodyShort
}
return ResponsePeerRequest{
TargetID: binary.LittleEndian.Uint16(body[:2]),
Mode: body[2],
Value: binary.LittleEndian.Uint32(body[3:7]),
}, nil
}
// ResponsePeerRefusalReason 是当前 EXE 在「拒绝」时追加的 u16 原因码。
//
// 现场实测(packet_log 2026-09-26 07:24:24 / 08:32:39 / 08:32:48,三次点「否」):
//
// 点「否」 body = 16 00 | 00 | 00 00 00 00 | 85 00 (9 字节)
// 点「是」 body = 16 00 | 00 | 00 00 00 00 (7 字节)
//
// 86JP 参考服务端认的是同一个形状,并且直接把它叫做 captured refusal
// (body.Length == 9 && BitConverter.ToUInt16(body, 7) == 0x0085)。
const ResponsePeerRefusalReason uint16 = 0x0085
// DecodeResponsePeerRefusal 解析「拒绝」那种九字节形状:七字节回应 + u16 原因。
//
// 它存在的原因:DecodeResponsePeerRequest 只收正好七字节,而唯一的九字节兜底曾经把
// mode 限制在 10。于是一次**被拒绝的组队申请**根本解不出来 —— 服务端只留下一句
// game-party-response-peer-body-short 就返回,待处理的邀请从未被消费,申请人也收不到
// 任何东西,客户端因此永远不弹「对方不在线,或拒绝了您的组队请求」。
func DecodeResponsePeerRefusal(body []byte) (ResponsePeerRequest, uint16, bool) {
if len(body) != 9 {
return ResponsePeerRequest{}, 0, false
}
parsed, err := DecodeResponsePeerRequest(body[:7])
if err != nil {
return ResponsePeerRequest{}, 0, false
}
return parsed, binary.LittleEndian.Uint16(body[7:9]), true
}
// DecodeWalkoutPartyMemberRequest 按 EXE sub_32513D0 case 6 解析:body 只写入 u8 slot。
func DecodeWalkoutPartyMemberRequest(body []byte) (WalkoutPartyMemberRequest, error) {
if len(body) != 1 {
return WalkoutPartyMemberRequest{}, errWalkoutPartyMemberBodyShort
}
return WalkoutPartyMemberRequest{Slot: body[0]}, nil
}
// DecodeChangePartyMemberPositionRequest 按 EXE sub_30D0320 解析:u8 slot,u8 pos;pos 只接受 1 或 3。
func DecodeChangePartyMemberPositionRequest(body []byte) (ChangePartyMemberPositionRequest, error) {
if len(body) != 2 {
return ChangePartyMemberPositionRequest{}, errChangePartyMemberPositionBodyShort
}
req := ChangePartyMemberPositionRequest{
Slot: body[0],
Position: body[1],
}
if req.Slot > 7 || (req.Position != 1 && req.Position != 3) {
return ChangePartyMemberPositionRequest{}, errChangePartyMemberPositionInvalid
}
return req, nil
}
func decodePartyMaxMembers(code byte) byte {
switch code {
case 1, 2, 3, 4:
return code
case 6:
return 4
default:
return 4
}
}
func resolvePartyNamePreset(code byte) []byte {
if code < 1 || int(code) > len(partyNamePresetTable) {
return nil
}
return append([]byte(nil), partyNamePresetTable[int(code)-1]...)
}