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,167 @@
package dnfbridge
import (
"encoding/hex"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"
"time"
"longheng.io/server/internal/platform/logcrypt"
"longheng.io/server/internal/platform/logkey"
)
const (
// packetLogHexLimit 是单行记录的 hex 截断长度(未变)。
packetLogHexLimit = 2048
// 下面三个沿用既有名字,值来自 logcrypt 的默认值,并在 openPacketLogger 里
// 显式传进配置——测试与诊断代码一直以它们为"多久会落盘"的依据。
packetLogQueueSize = logcrypt.DefaultQueueSize
packetLogBufferSize = logcrypt.DefaultBufferSize
packetLogFlushInterval = logcrypt.DefaultFlushInterval
// packetLogArchiveDirName 是锚点同级下的归档目录名。
packetLogArchiveDirName = "packet"
// packetLogArchiveExt 是加密归档的扩展名;.enc 明确表示"这是密文,别用文本工具开"。
packetLogArchiveExt = ".log.enc"
// packetLogDropMarkerKind 与旧实现保持同名,解密后的事件流与历史一致。
packetLogDropMarkerKind = "packet-log-backpressure-dropped"
// 默认密钥路径,**相对于服务进程工作目录**(也就是 runtime/),刻意放在 logs/ 之外:
// 只归档 logs/ 不会连带把密钥一起带走。公钥随分发包分发;私钥**绝不**放进分发包。
defaultLogPublicKeyFile = "keys/log-public.key"
defaultLogPrivateKeyFile = "keys/log-private.key"
)
// packetLogger 把既有的日志行格式交给 logcrypt 做逐行加密与轮转。
//
// 落盘布局(锚点 path 默认是 logs/packet_log.txt):
//
// logs/packet/<YYYYMMDD>/packet_log-<YYYYMMDD>-NNN.log.enc 密文分片,单文件上限 100 MB
// logs/packet/current.txt 当前分片指针(解密工具默认读它)
//
// 收件人公钥固化在二进制里(internal/platform/logkey),**服务端不需要任何密钥文件**。
//
// 为什么不把锚点本身当输出文件:records 目录是从锚点路径的**父目录**推导出来的
// (defaultRecordDirFor),保持父目录不变,logs/records 才不会被搬走或孤立。
//
// 解密后的文本与加密前的历史格式逐字节一致,因此仓库里那些"从 packet_log 抄下来的"
// 现场注释与既有分析脚本在解密之后仍然适用。
type packetLogger struct {
writer *logcrypt.Writer
}
func openPacketLogger(path string) (*packetLogger, error) {
if path == "" {
return nil, nil
}
anchorDir := filepath.Dir(path)
archiveDir := filepath.Join(anchorDir, packetLogArchiveDirName)
prefix := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
if prefix == "" || prefix == "." {
prefix = "packet"
}
// 收件人公钥**固化在二进制里**(internal/platform/logkey):用服务端的人不需要
// 理解、放置或配置任何密钥文件,拿到就能跑。公钥只能加密、不能解密,写死是安全的。
// 只有显式设了 DNFBRIDGE_LOG_PUBLIC_KEY_FILE 才用它覆盖(轮换/测试用)。
recipient, err := logkey.ResolveRecipient()
if err != nil {
return nil, err
}
writer, err := logcrypt.Open(logcrypt.Config{
Dir: archiveDir,
Prefix: prefix,
Ext: packetLogArchiveExt,
Recipient: recipient,
MaxBytes: int64(envInt("DNFBRIDGE_LOG_MAX_BYTES", logcrypt.DefaultMaxBytes)),
RetentionDays: envInt("DNFBRIDGE_LOG_RETENTION_DAYS", logcrypt.DefaultRetentionDays),
QueueSize: packetLogQueueSize,
BufferSize: packetLogBufferSize,
FlushInterval: packetLogFlushInterval,
DropMarkerKind: packetLogDropMarkerKind,
Logger: slog.Default(),
})
if err != nil {
return nil, err
}
logger := &packetLogger{writer: writer}
logger.writeLine("=== LongHeng Go dnfbridge packet log started " + time.Now().UTC().Format(time.RFC3339) + " ===")
return logger, nil
}
func (l *packetLogger) Close() error {
if l == nil || l.writer == nil {
return nil
}
return l.writer.Close()
}
// Dropped 返回因写队列溢出被丢弃的行数。诊断用它判断"某段日志里的计数是不是下限"。
func (l *packetLogger) Dropped() uint64 {
if l == nil || l.writer == nil {
return 0
}
return l.writer.Dropped()
}
// Path 当前分片路径(测试与诊断用)。
func (l *packetLogger) Path() string {
if l == nil || l.writer == nil {
return ""
}
return l.writer.Path()
}
func (l *packetLogger) Log(direction string, kind string, data []byte, fields ...any) {
if l == nil || l.writer == nil {
return
}
hexData := data
truncated := ""
if len(hexData) > packetLogHexLimit {
hexData = hexData[:packetLogHexLimit]
truncated = " truncated=true"
}
line := fmt.Sprintf("%s kind=%s", direction, kind)
for i := 0; i+1 < len(fields); i += 2 {
line += fmt.Sprintf(" %v=%v", fields[i], fields[i+1])
}
line += fmt.Sprintf(" len=%d%s hex=%s", len(data), truncated, hex.EncodeToString(hexData))
l.writeLine(line)
}
func (l *packetLogger) Event(kind string, fields ...any) {
if l == nil || l.writer == nil {
return
}
line := "EVENT kind=" + kind
for i := 0; i+1 < len(fields); i += 2 {
line += fmt.Sprintf(" %v=%v", fields[i], fields[i+1])
}
l.writeLine(line)
}
func (l *packetLogger) writeLine(line string) {
if l == nil || l.writer == nil {
return
}
l.writer.Write(time.Now().UTC().Format(time.RFC3339Nano) + " " + line + "\n")
}
// logPrivateKeyPath 解析私钥路径。
//
// **服务端运行不需要私钥**(写日志只要公钥)。这里只为两种情况保留:
// 测试解密断言,以及运营在自己服务器上做本机回读。分发包里绝不放私钥。
func logPrivateKeyPath(anchorPath string) string {
if explicit := strings.TrimSpace(os.Getenv("DNFBRIDGE_LOG_PRIVATE_KEY_FILE")); explicit != "" {
return explicit
}
anchorDir := filepath.Dir(anchorPath)
if anchorDir == "" || anchorDir == "." {
return filepath.Join("keys", logcrypt.PrivateKeyFileName)
}
return filepath.Join(filepath.Dir(anchorDir), "keys", logcrypt.PrivateKeyFileName)
}