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,289 @@
// 本文件解析队伍模块已追到读取顺序的客户端请求体。
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]...)
}