- 服务端新增活动开关运行时配置与 GM 接口(GET/POST /dnf/gm/activity-switches), 持久化到 Settings,接入月签到/周签到/PC房↔累计在线的实时下发 - 服务端新增频道开关 GM 接口(channel-switches),目录生成时过滤禁用频道 - channelcatalog/channelinfo 支持按禁用频道 ID 过滤目录与客户端脚本 - 启动器新增「游戏设置」页面:频道开关 Tab + 活动开关 Tab - native.py/server.py 新增对应转发 API - 窗口标题/页面 title/侧边栏「90轮回」统一改为「DNF90 灵狐小竹整合单机版」
192 lines
6.2 KiB
Go
192 lines
6.2 KiB
Go
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) {
|
||
return Build90CNOnlineScriptFiltered(data, sourceServerID, advertiseServerID, bootstrapChannelID, nil)
|
||
}
|
||
|
||
// Build90CNOnlineScriptFiltered 是 Build90CNOnlineScript 的带禁用频道过滤版。
|
||
// disabledIDs 里的频道不会进入下发给客户端的频道表(与 channelcatalog 的过滤保持一致)。
|
||
func Build90CNOnlineScriptFiltered(data []byte, sourceServerID, advertiseServerID, bootstrapChannelID int, disabledIDs map[int]bool) ([]byte, error) {
|
||
script, bootstrapFound, err := build90CNOnlineScript(data, sourceServerID, advertiseServerID, bootstrapChannelID, disabledIDs)
|
||
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, nil)
|
||
}
|
||
|
||
// Build90CNOnlineScriptAllowingMissingBootstrapFiltered 是带禁用频道过滤的宽松版。
|
||
func Build90CNOnlineScriptAllowingMissingBootstrapFiltered(
|
||
data []byte,
|
||
sourceServerID, advertiseServerID, bootstrapChannelID int,
|
||
disabledIDs map[int]bool,
|
||
) ([]byte, bool, error) {
|
||
return build90CNOnlineScript(data, sourceServerID, advertiseServerID, bootstrapChannelID, disabledIDs)
|
||
}
|
||
|
||
func build90CNOnlineScript(
|
||
data []byte,
|
||
sourceServerID, advertiseServerID, bootstrapChannelID int,
|
||
disabledIDs map[int]bool,
|
||
) ([]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])
|
||
if disabledIDs != nil && disabledIDs[id] {
|
||
start = end
|
||
continue
|
||
}
|
||
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
|
||
}
|