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,171 @@
package channelinfo
import (
"bufio"
"bytes"
"errors"
"fmt"
"strconv"
"strings"
)
// Build90CNOnlineScript derives the channel table downloaded by the 90CN
// client from the raw PVF channel_info.etc. Source names and groups remain
// authoritative; only the advertised server ID and proved online type
// projection may differ from the raw in-game catalog.
//
// 严格版:源表缺少引导频道时直接失败。只开少数频道(甚至只开特殊区域频道)的运维配置
// 请走 Build90CNOnlineScriptAllowingMissingBootstrap。
func Build90CNOnlineScript(data []byte, sourceServerID, advertiseServerID, bootstrapChannelID int) ([]byte, error) {
script, bootstrapFound, err := build90CNOnlineScript(data, sourceServerID, advertiseServerID, bootstrapChannelID)
if err != nil {
return nil, err
}
if !bootstrapFound {
return nil, fmt.Errorf(
"channel script source server %d is missing bootstrap channel %d",
sourceServerID,
bootstrapChannelID,
)
}
return script, nil
}
// Build90CNOnlineScriptAllowingMissingBootstrap 是 Build90CNOnlineScript 的宽松版:
// 源频道表里没有引导频道时不再失败,而是返回 bootstrapPresent=false,由调用方决定
// 是告警还是拒绝。
//
// 为什么需要它:引导频道这条硬校验来自官方 PVF 流程——那边的引导频道是 id 1 / type 2,
// 由 OnlineTypeFor90CN 映成线上的 type 11;而线上这份 channel_info.etc 里并没有 id 1,
// 于是 dnfenum.BootstrapChannelID 被定成 19,校验就变成了"表里必须有 19"。
// 实测(2026-09-30,1192 次客户端连接)客户端**从不**连 10019:它从 7001 拿目录后
// 直接从目录里挑端口连。所以这条约束对这条链路是化石约束,运维只开任意一个频道时
// 不该把服务端拦在门外。
func Build90CNOnlineScriptAllowingMissingBootstrap(
data []byte,
sourceServerID, advertiseServerID, bootstrapChannelID int,
) ([]byte, bool, error) {
return build90CNOnlineScript(data, sourceServerID, advertiseServerID, bootstrapChannelID)
}
func build90CNOnlineScript(
data []byte,
sourceServerID, advertiseServerID, bootstrapChannelID int,
) ([]byte, bool, error) {
if sourceServerID <= 0 || advertiseServerID < 0 || bootstrapChannelID <= 0 {
return nil, false, errors.New("channel script source, advertisement, and bootstrap IDs are invalid")
}
scanner := bufio.NewScanner(bytes.NewReader(data))
scanner.Buffer(make([]byte, 1024), 1024*1024)
var prefix strings.Builder
serverSectionSeen := false
inServer := false
activeServer := 1
records := make([][]string, 0, 64)
bootstrapFound := false
for lineNumber := 1; scanner.Scan(); lineNumber++ {
line := scanner.Text()
trimmed := strings.TrimSpace(line)
if serverID, ok, err := parseServerSectionHeader(trimmed); ok {
if err != nil {
return nil, false, fmt.Errorf("channel script line %d: %w", lineNumber, err)
}
serverSectionSeen = true
inServer = true
activeServer = serverID
continue
}
switch strings.ToLower(trimmed) {
case "[/server]":
inServer = false
continue
}
if !serverSectionSeen {
prefix.WriteString(line)
prefix.WriteByte('\n')
continue
}
if !inServer {
continue
}
if trimmed == "" {
continue
}
fields := splitFields(trimmed)
if len(fields) == 0 {
continue
}
if len(fields) == 1 {
if id, err := strconv.Atoi(fields[0]); err == nil {
activeServer = id
}
continue
}
start := 0
if len(fields) >= 5 && isInt(fields[0]) && isChannelStart(fields, 1) {
activeServer, _ = strconv.Atoi(fields[0])
start = 1
}
for start < len(fields) {
if !isChannelStart(fields, start) {
return nil, false, fmt.Errorf("channel script line %d: invalid packed channel row", lineNumber)
}
end := len(fields)
for next := start + 4; next < len(fields); next++ {
if isChannelStart(fields, next) {
end = next
break
}
}
if activeServer == sourceServerID {
record := append([]string(nil), fields[start:end]...)
id, _ := strconv.Atoi(record[0])
channelType, _ := strconv.Atoi(record[2])
record[2] = strconv.Itoa(OnlineTypeFor90CN(id, channelType))
if id == bootstrapChannelID {
bootstrapFound = true
}
for len(record) > 0 && record[len(record)-1] == "``" {
record = record[:len(record)-1]
}
record = append(record, "``")
records = append(records, record)
}
start = end
}
}
if err := scanner.Err(); err != nil {
return nil, false, err
}
if len(records) == 0 {
return nil, false, fmt.Errorf("channel script source server %d has no channels", sourceServerID)
}
var output strings.Builder
output.WriteString(prefix.String())
output.WriteString("[server]\n")
output.WriteString(strconv.Itoa(advertiseServerID))
output.WriteByte('\n')
for _, record := range records {
output.WriteString(strings.Join(record, " "))
output.WriteByte('\n')
}
output.WriteString("[/server]\n")
return []byte(output.String()), bootstrapFound, nil
}
// OnlineTypeFor90CN converts a raw PVF channel type into the type stored in
// the downloaded online directory. It must never be used for game CHANNELINFO.
func OnlineTypeFor90CN(channelID, channelType int) int {
if channelType == 0 || channelType == 1 {
return 22
}
if channelID == 1 && channelType == 2 {
return 11
}
return channelType
}