服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
67 lines
3.1 KiB
Go
67 lines
3.1 KiB
Go
// peer_response_refusal.go owns the nine-byte "player pressed 否" form of the
|
|
// class-1/op11 peer response.
|
|
//
|
|
// Live capture 2026-09-26 08:32:39 and 08:32:48 -- two presses of 否 in the
|
|
// party-application prompt -- plus 07:24:24 before them:
|
|
//
|
|
// 点「否」 body = 16 00 | 00 | 00 00 00 00 | 85 00 (9 bytes)
|
|
// 点「是」 body = 16 00 | 00 | 00 00 00 00 (7 bytes)
|
|
//
|
|
// party.DecodeResponsePeerRequest insists on exactly seven bytes and the only
|
|
// nine-byte fallback was runtimeRaidPeerResponseDeclined, which gated on mode
|
|
// 10. A refused PARTY application therefore never decoded: the server logged
|
|
// `game-party-response-peer-body-short` and returned, so the pending invite was
|
|
// never consumed and the applicant received nothing at all.
|
|
package dnfbridge
|
|
|
|
import (
|
|
"longheng.io/server/internal/modules/dnf/party"
|
|
)
|
|
|
|
// currentPeerResponseAnswer is one op11 answer, refusal included.
|
|
type currentPeerResponseAnswer struct {
|
|
Request party.ResponsePeerRequest
|
|
Refused bool
|
|
Reason uint16
|
|
}
|
|
|
|
// decodeCurrentPeerResponseAnswer accepts both wire forms of op11: the plain
|
|
// seven-byte answer, and the nine-byte refusal. It returns the original decode
|
|
// error when neither matches, so the caller can still log the short-body case.
|
|
func decodeCurrentPeerResponseAnswer(body []byte) (currentPeerResponseAnswer, error) {
|
|
parsed, err := party.DecodeResponsePeerRequest(body)
|
|
if err == nil {
|
|
return currentPeerResponseAnswer{Request: parsed}, nil
|
|
}
|
|
if refusal, reason, ok := party.DecodeResponsePeerRefusal(body); ok {
|
|
return currentPeerResponseAnswer{Request: refusal, Refused: true, Reason: reason}, nil
|
|
}
|
|
return currentPeerResponseAnswer{}, err
|
|
}
|
|
|
|
// A refusal is now consumed, logged, and then deliberately left unanswered.
|
|
//
|
|
// Two live attempts on 2026-09-26 both failed, and the client reverse-engineering
|
|
// explains why:
|
|
//
|
|
// - class-0/op8 (09:43) made the requester's client print 玩家[%s]已接受组队请求
|
|
// -- the opposite of what happened. That line is string 624 (trade version
|
|
// 625) and the client has no refusal twin for it, so op8 cannot express a
|
|
// refusal at all.
|
|
// - class-1/op10 failure (09:50, code 0) was received -- the client trace shows
|
|
// `class=1 opcode=10 packet_len=18` right after our two-byte body -- and did
|
|
// nothing. A request takes exactly one answer and the success ACK already
|
|
// spent it.
|
|
//
|
|
// The message the operator expected (对方不在线,或拒绝了您的组队请求) exists twice in
|
|
// the client's string table, but neither copy is reachable in this build: the
|
|
// bytes `push 0x111A6` / `push 0x111A7` occur nowhere in DNF.exe, and 70054 does
|
|
// not appear as a 4-byte value in any section. The scanner behind that claim was
|
|
// validated against ids that ARE referenced -- `push 0x11205` (70149) has 26
|
|
// sites, and `push 0x112e1` (70369) resolves to 0x1911B39, the exact instruction
|
|
// found by hand. The same holds for the trade pair 70056/70057, so the whole
|
|
// quartet is dead in this client.
|
|
//
|
|
// This matches the reference server: 86JP's PartyHandler cancels the invite on a
|
|
// type-0 refusal and sends the inviter nothing.
|