Files
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

246 lines
8.6 KiB
Go

package dnfbridge
import (
"encoding/binary"
"encoding/hex"
"fmt"
"longheng.io/server/internal/modules/dnf/dnfenum"
)
const (
currentPartyPeerDefaultPathMTU = uint32(1472)
currentPartyNetworkReportSize = 18
currentPartyNetworkVersion = uint32(5)
currentPartyPeerStateOpcode = uint16(97)
currentPartyHolePunchOpcode = uint16(966)
currentPartyAveragePingOpcode = uint16(1295)
)
// currentPartyClientInputRejection wraps one unproven client shape in the
// non-fatal sentinel and keeps every fact a capture needs in the message.
//
// The native P2P telemetry opcodes below are written by a client generation
// whose exact layouts this bridge recovers by observation. A body it does not
// recognise therefore means "shape not proved yet", not "session is broken";
// returning the bare parser error made the fatal inbound boundary close the game
// connection instead (see errGameClientInputRejected for the live incident).
func currentPartyClientInputRejection(opcode uint16, body []byte, reason error) error {
return fmt.Errorf("%w: type=%d body_len=%d body_hex=%s: %v",
errGameClientInputRejected, opcode, len(body), hex.EncodeToString(body), reason)
}
type currentPartyPeerStateReport struct {
InternalStateWasZero byte
PeerActor uint16
}
func parseCurrentPartyPeerStateReport(body []byte) (currentPartyPeerStateReport, error) {
if len(body) != 3 {
return currentPartyPeerStateReport{}, fmt.Errorf("current party peer-state body length %d, want 3", len(body))
}
if body[0] > 1 {
return currentPartyPeerStateReport{}, fmt.Errorf("current party peer-state flag %d, want 0 or 1", body[0])
}
return currentPartyPeerStateReport{
InternalStateWasZero: body[0],
PeerActor: binary.LittleEndian.Uint16(body[1:3]),
}, nil
}
type currentPartyHolePunchReport struct {
OpaqueMode byte
OpaqueSequence uint32
ObservationWindowMS uint32
OpaqueU32A uint32
OpaqueU32B uint32
OpaqueU32C uint32
PeerActor uint16
}
func parseCurrentPartyHolePunchReport(body []byte) (currentPartyHolePunchReport, error) {
if len(body) != 23 {
return currentPartyHolePunchReport{}, fmt.Errorf("current party hole-punch body length %d, want 23", len(body))
}
return currentPartyHolePunchReport{
OpaqueMode: body[0],
OpaqueSequence: binary.LittleEndian.Uint32(body[1:5]),
ObservationWindowMS: binary.LittleEndian.Uint32(body[5:9]),
OpaqueU32A: binary.LittleEndian.Uint32(body[9:13]),
OpaqueU32B: binary.LittleEndian.Uint32(body[13:17]),
OpaqueU32C: binary.LittleEndian.Uint32(body[17:21]),
PeerActor: binary.LittleEndian.Uint16(body[21:23]),
}, nil
}
type currentPartyAveragePingReport struct {
Slot uint16
PeerKey uint32
AveragePingMS uint32
}
func parseCurrentPartyAveragePingReport(body []byte) (currentPartyAveragePingReport, error) {
if len(body) != 10 {
return currentPartyAveragePingReport{}, fmt.Errorf("current party average-ping body length %d, want 10", len(body))
}
report := currentPartyAveragePingReport{
Slot: binary.LittleEndian.Uint16(body[0:2]),
PeerKey: binary.LittleEndian.Uint32(body[2:6]),
AveragePingMS: binary.LittleEndian.Uint32(body[6:10]),
}
if report.Slot >= 8 {
return currentPartyAveragePingReport{}, fmt.Errorf("current party average-ping slot %d, want 0..7", report.Slot)
}
return report, nil
}
func (s *Service) handleCurrentPartyTransportTelemetry(session *gameSession, opcode uint16, body []byte) error {
switch opcode {
case currentPartyPeerStateOpcode:
report, err := parseCurrentPartyPeerStateReport(body)
if err != nil {
return currentPartyClientInputRejection(opcode, body, err)
}
s.logGameEvent(session, "game-party-peer-state-accepted",
"peer_actor", report.PeerActor,
"internal_state_was_zero", report.InternalStateWasZero)
return nil
case currentPartyHolePunchOpcode:
report, err := parseCurrentPartyHolePunchReport(body)
if err != nil {
return currentPartyClientInputRejection(opcode, body, err)
}
s.logGameEvent(session, "game-party-hole-punch-accepted",
"peer_actor", report.PeerActor,
"observation_window_ms", report.ObservationWindowMS,
"opaque_mode", report.OpaqueMode,
"opaque_sequence", report.OpaqueSequence,
"opaque_u32_a", report.OpaqueU32A,
"opaque_u32_b", report.OpaqueU32B,
"opaque_u32_c", report.OpaqueU32C)
return nil
case currentPartyAveragePingOpcode:
report, err := parseCurrentPartyAveragePingReport(body)
if err != nil {
return currentPartyClientInputRejection(opcode, body, err)
}
s.logGameEvent(session, "game-party-average-ping-accepted",
"slot", report.Slot,
"peer_key", report.PeerKey,
"average_ping_ms", report.AveragePingMS)
return nil
default:
return currentPartyClientInputRejection(opcode, body,
fmt.Errorf("unsupported current party transport telemetry opcode %d", opcode))
}
}
func (s *Service) assignPartyTransportIdentity(session *gameSession) {
if s == nil || session == nil {
return
}
session.partyPeerMu.Lock()
defer session.partyPeerMu.Unlock()
s.assignPartyTransportIdentityLocked(session)
}
func (s *Service) assignPartyTransportIdentityLocked(session *gameSession) {
if session.partyRoutingKey == 0 {
for session.partyRoutingKey == 0 {
session.partyRoutingKey = s.nextPartyRoutingKey.Add(1)
}
}
if session.partyPathMTU == 0 {
session.partyPathMTU = currentPartyPeerDefaultPathMTU
}
}
func (s *Service) partyTransportIdentity(session *gameSession) (uint32, uint32) {
if s == nil || session == nil {
return 0, 0
}
session.partyPeerMu.Lock()
defer session.partyPeerMu.Unlock()
s.assignPartyTransportIdentityLocked(session)
return session.partyRoutingKey, session.partyPathMTU
}
func currentPartyPathMTU(value uint32) bool {
switch value {
case 512, 1024, 1280, 1472:
return true
default:
return false
}
}
// currentPartyPeerEndpointRegistration is the authenticated source tuple
// observed by the native UDP reachability service. It must never be populated
// from the unrelated 31-byte class1/op2 channel-reconnect probe.
type currentPartyPeerEndpointRegistration struct {
CharacterAttribute byte
InnerIPv4 [4]byte
OuterIPv4 [4]byte
Port uint16
MTU uint32
}
func currentPartyPeerPort(session *gameSession) uint16 {
return currentPartyPeerEndpointSnapshot(session).Port
}
func currentPartyPeerEndpointSnapshot(session *gameSession) currentPartyPeerEndpointRegistration {
if session == nil {
return currentPartyPeerEndpointRegistration{}
}
session.partyPeerMu.Lock()
defer session.partyPeerMu.Unlock()
return session.partyPeer
}
func currentPartyPeerAdvertisedMTU(registration currentPartyPeerEndpointRegistration) uint32 {
if !currentPartyPathMTU(registration.MTU) {
return 0
}
return registration.MTU
}
// handleCurrentPartyNetworkReport consumes the exact 18-byte class1/op434
// native path report. NoPack only produces four MTU buckets and version 5;
// publishing zero or an arbitrary Ethernet MTU makes its reliable-UDP layer
// construct empty frames indefinitely.
//
// A body of the wrong length never reaches this function: legacy op434 also
// carries the 组队传送 request, and the length discriminator in
// handleCurrentPartyTeleportOrNetworkReport routes that shape elsewhere. What is
// left here is a genuine 18-byte report with an unproven MTU/version, which is
// refused as client input instead of as a session failure.
func (s *Service) handleCurrentPartyNetworkReport(session *gameSession, body []byte) error {
opcode := uint16(dnfenum.CmdPacketPartyTeleport)
if session == nil || len(body) != currentPartyNetworkReportSize {
return currentPartyClientInputRejection(opcode, body,
fmt.Errorf("current party network report body length %d, want %d", len(body), currentPartyNetworkReportSize))
}
pathMTU := binary.LittleEndian.Uint32(body[0:4])
version := binary.LittleEndian.Uint32(body[10:14])
if !currentPartyPathMTU(pathMTU) {
return currentPartyClientInputRejection(opcode, body,
fmt.Errorf("current party network report path MTU %d is not a native bucket", pathMTU))
}
if version != currentPartyNetworkVersion {
return currentPartyClientInputRejection(opcode, body,
fmt.Errorf("current party network report version %d, want %d", version, currentPartyNetworkVersion))
}
routingKey, _ := s.partyTransportIdentity(session)
session.partyPeerMu.Lock()
session.partyPathMTU = pathMTU
session.partyPeerMu.Unlock()
s.logGameEvent(session, "game-party-network-report-accepted",
"char_id", session.selectedCharacterID,
"routing_key", routingKey,
"path_mtu", pathMTU,
"version", version,
"source", "same_version_current_exe_op434_native_path_report")
return nil
}