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,227 @@
package dnfbridge
import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/binary"
"errors"
"fmt"
"net"
"net/netip"
"sort"
"strings"
"time"
"longheng.io/server/internal/modules/dnf/dnfenum"
dnfrepo "longheng.io/server/internal/modules/dnf/repository"
)
const (
currentRobotSessionBindOpcode uint16 = 0xfff0
currentRobotSessionBindVersion byte = 1
currentRobotSessionBindSignatureLen = sha256.Size
currentRobotSessionBindMaxAccountLen = 96
currentRobotSessionBindClockSkew = 30 * time.Second
currentRobotSessionBindReplayTTL = 2 * time.Minute
currentRobotSessionBindReplayLimit = 4096
)
const (
currentRobotSessionBindErrorMalformed byte = 1
currentRobotSessionBindErrorDisabled byte = 2
currentRobotSessionBindErrorAuth byte = 3
currentRobotSessionBindErrorAccount byte = 4
currentRobotSessionBindErrorReplay byte = 5
)
type currentRobotSessionBindRequest struct {
TimestampUnix int64
Nonce uint64
DesiredCharacterID uint16
AccountID string
Signed []byte
Signature [currentRobotSessionBindSignatureLen]byte
}
func parseCurrentRobotSessionBindRequest(body []byte) (currentRobotSessionBindRequest, error) {
const fixedPrefix = 1 + 8 + 8 + 2 + 1
if len(body) < fixedPrefix+currentRobotSessionBindSignatureLen || body[0] != currentRobotSessionBindVersion {
return currentRobotSessionBindRequest{}, errors.New("invalid robot session bind prefix")
}
accountLen := int(body[fixedPrefix-1])
if accountLen <= 0 || accountLen > currentRobotSessionBindMaxAccountLen || len(body) != fixedPrefix+accountLen+currentRobotSessionBindSignatureLen {
return currentRobotSessionBindRequest{}, errors.New("invalid robot session bind account length")
}
accountID := strings.TrimSpace(string(body[fixedPrefix : fixedPrefix+accountLen]))
if accountID == "" || len(accountID) != accountLen || strings.IndexByte(accountID, 0) >= 0 {
return currentRobotSessionBindRequest{}, errors.New("invalid robot session bind account")
}
request := currentRobotSessionBindRequest{
TimestampUnix: int64(binary.LittleEndian.Uint64(body[1:9])),
Nonce: binary.LittleEndian.Uint64(body[9:17]),
DesiredCharacterID: binary.LittleEndian.Uint16(body[17:19]),
AccountID: accountID,
Signed: append([]byte(nil), body[:fixedPrefix+accountLen]...),
}
copy(request.Signature[:], body[fixedPrefix+accountLen:])
return request, nil
}
func (s *Service) handleCurrentRobotSessionBind(session *gameSession, body []byte) error {
if s == nil || session == nil {
return errors.New("robot session bind service is unavailable")
}
request, err := parseCurrentRobotSessionBindRequest(body)
if err != nil {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorMalformed)
}
if len(s.robotSessionSecret) == 0 {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorDisabled)
}
if !currentRobotSessionRemoteAllowed(session.conn) {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAuth)
}
now := time.Now().UTC()
timestamp := time.Unix(request.TimestampUnix, 0).UTC()
if timestamp.Before(now.Add(-currentRobotSessionBindClockSkew)) || timestamp.After(now.Add(currentRobotSessionBindClockSkew)) {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAuth)
}
mac := hmac.New(sha256.New, s.robotSessionSecret)
_, _ = mac.Write(request.Signed)
expected := mac.Sum(nil)
if subtle.ConstantTimeCompare(expected, request.Signature[:]) != 1 {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAuth)
}
if !s.reserveCurrentRobotSessionBindReplay(request.Signature, now) {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorReplay)
}
repositories, ok := s.repositoryGroup()
if !ok || repositories.Character == nil {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAccount)
}
ctx, cancel := context.WithTimeout(context.Background(), createWriteTimeout)
defer cancel()
characters, err := repositories.Character.ListByAccount(ctx, request.AccountID, dnfrepo.MaxCharacterSlotLimit)
if err != nil || len(characters) == 0 {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAccount)
}
character, ok := selectCurrentRobotSessionCharacter(characters, request.DesiredCharacterID)
if !ok {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAccount)
}
characterID := numericCharacterID(character)
if characterID <= 0 || characterID > int(^uint16(0)) || character.Slot < 0 || character.Slot > int(^uint8(0)) {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAccount)
}
if existing := strings.TrimSpace(session.accountID); existing != "" && existing != request.AccountID {
return s.sendCurrentRobotSessionBindFailure(session, currentRobotSessionBindErrorAuth)
}
session.accountID = request.AccountID
session.robotSessionBound = true
bodyOut := []byte{1, byte(character.Slot), 0, 0}
binary.LittleEndian.PutUint16(bodyOut[2:4], uint16(characterID))
s.logGameEvent(session, "game-robot-session-bound",
"char_id", characterID,
"slot", character.Slot,
"source", "hmac_private_lan")
return s.sendGame(session, byte(dnfenum.GameCmdCommand), currentRobotSessionBindOpcode, bodyOut)
}
func (s *Service) sendCurrentRobotSessionBindFailure(session *gameSession, code byte) error {
return s.sendGame(session, byte(dnfenum.GameCmdCommand), currentRobotSessionBindOpcode, []byte{0, code})
}
func selectCurrentRobotSessionCharacter(characters []dnfrepo.CharacterRecord, desired uint16) (dnfrepo.CharacterRecord, bool) {
ordered := append([]dnfrepo.CharacterRecord(nil), characters...)
sort.SliceStable(ordered, func(left, right int) bool {
if ordered[left].Slot != ordered[right].Slot {
return ordered[left].Slot < ordered[right].Slot
}
return numericCharacterID(ordered[left]) < numericCharacterID(ordered[right])
})
for _, character := range ordered {
id := numericCharacterID(character)
if id <= 0 || id > int(^uint16(0)) {
continue
}
if desired == 0 || uint16(id) == desired {
return character, true
}
}
return dnfrepo.CharacterRecord{}, false
}
func (s *Service) reserveCurrentRobotSessionBindReplay(signature [currentRobotSessionBindSignatureLen]byte, now time.Time) bool {
s.robotSessionReplayMu.Lock()
defer s.robotSessionReplayMu.Unlock()
if s.robotSessionReplays == nil {
s.robotSessionReplays = make(map[[currentRobotSessionBindSignatureLen]byte]time.Time)
}
cutoff := now.Add(-currentRobotSessionBindReplayTTL)
for digest, seenAt := range s.robotSessionReplays {
if seenAt.Before(cutoff) {
delete(s.robotSessionReplays, digest)
}
}
if _, exists := s.robotSessionReplays[signature]; exists {
return false
}
if len(s.robotSessionReplays) >= currentRobotSessionBindReplayLimit {
var oldestDigest [currentRobotSessionBindSignatureLen]byte
var oldest time.Time
for digest, seenAt := range s.robotSessionReplays {
if oldest.IsZero() || seenAt.Before(oldest) {
oldestDigest, oldest = digest, seenAt
}
}
delete(s.robotSessionReplays, oldestDigest)
}
s.robotSessionReplays[signature] = now
return true
}
func currentRobotSessionRemoteAllowed(conn net.Conn) bool {
if conn == nil || conn.RemoteAddr() == nil {
return false
}
var addr netip.Addr
switch value := conn.RemoteAddr().(type) {
case *net.TCPAddr:
addr, _ = netip.AddrFromSlice(value.IP)
default:
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
return false
}
addr, err = netip.ParseAddr(strings.Trim(host, "[]"))
if err != nil {
return false
}
}
if !addr.IsValid() {
return false
}
addr = addr.Unmap()
return addr.IsLoopback() || addr.IsPrivate()
}
func buildCurrentRobotSessionBindRequest(secret []byte, now time.Time, nonce uint64, desired uint16, accountID string) ([]byte, error) {
accountID = strings.TrimSpace(accountID)
if len(secret) == 0 || accountID == "" || len(accountID) > currentRobotSessionBindMaxAccountLen {
return nil, fmt.Errorf("invalid robot bind fixture")
}
body := make([]byte, 1+8+8+2+1+len(accountID)+currentRobotSessionBindSignatureLen)
body[0] = currentRobotSessionBindVersion
binary.LittleEndian.PutUint64(body[1:9], uint64(now.Unix()))
binary.LittleEndian.PutUint64(body[9:17], nonce)
binary.LittleEndian.PutUint16(body[17:19], desired)
body[19] = byte(len(accountID))
copy(body[20:], accountID)
mac := hmac.New(sha256.New, secret)
_, _ = mac.Write(body[:20+len(accountID)])
copy(body[20+len(accountID):], mac.Sum(nil))
return body, nil
}