DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,548 @@
|
||||
// options.go 负责 DNF bridge 启动配置和环境变量兼容。
|
||||
// 这里只归一化协议开关和监听参数,不持有运行期玩家状态。
|
||||
package dnfbridge
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"longheng.io/server/internal/platform/config"
|
||||
platformpvf "longheng.io/server/internal/platform/pvf"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultChannelListen = ":7001"
|
||||
defaultChannelInfoFile = "data/dnf/channel_info.etc"
|
||||
defaultServerIP = "127.0.0.1"
|
||||
// Bump when the downloaded channel_info.etc semantics change. Keeping the
|
||||
// old value makes clients reuse their cached script and continue showing
|
||||
// team channels as unavailable even after the server-side schedule fix.
|
||||
defaultScriptVersion = "60"
|
||||
channelInfoBodyLatest = "latest"
|
||||
gameInitialModeNotice = "notice"
|
||||
gameInitialModeLogin = "login_success"
|
||||
gameInitialModeUpper = "upper_endpoint"
|
||||
gameInitialModeNone = "none"
|
||||
gamePreBootstrapNone = "none"
|
||||
gamePreBootstrapGuard = "guard_clear"
|
||||
gamePostBootstrapNone = "none"
|
||||
gamePostBootstrapCheck = "client_check"
|
||||
gamePostBootstrapRoster = "roster_init"
|
||||
gameUpperHeaderChannel13 = "channel13"
|
||||
gameUpperHeaderServer16 = "server16"
|
||||
gameUpperBodyCodecAuto = "auto"
|
||||
gameUpperBodyCodecPlain = "plaintext"
|
||||
gameUpperClientBodyCodecPlain = "plaintext"
|
||||
gameUpperClientBodyCodecProbe = "probe"
|
||||
gameUpperClientBodyCodecNative = "native"
|
||||
gameDprotoModeLegacy = "legacy_patch"
|
||||
gameDprotoModeNative = "native"
|
||||
// S2C 回包二次加密模式(见 protocol/s2ccipher.go)。
|
||||
// off = 明文(默认;客户端 DLL 也是直通,两侧行为与加这个功能之前完全一致)
|
||||
// shadow = 只协商、只记账,**仍然发明文**(用来验证"哪些连接会启用",零风险)
|
||||
// on = 协商成功的连接加密 body
|
||||
gameS2CCipherOff = "off"
|
||||
gameS2CCipherShadow = "shadow"
|
||||
gameS2CCipherOn = "on"
|
||||
// DOVE 明文首包里 UDP 字段是 2311/2312;这里只作为 InitialLoginNotice 的兼容默认值。
|
||||
defaultInitialUDPPort1 = 2311
|
||||
defaultInitialUDPPort2 = 2312
|
||||
defaultCommandCount = 1382
|
||||
defaultNotificationCount = 1326
|
||||
defaultMaxPacketBytes = 1048576
|
||||
defaultAccountPrefix = "dnf:"
|
||||
// defaultLauncherAuthVerifyURL is the loopback verifier that the external
|
||||
// game connection auth gate calls after it receives a launcher ticket.
|
||||
// The URL stays internal to the host: launcher.exe owns the endpoint and
|
||||
// dnfbridge only talks to it over 127.0.0.1.
|
||||
defaultLauncherAuthVerifyURL = "http://127.0.0.1:19091/internal/verify-game-ticket"
|
||||
defaultLauncherAuthTimeout = 3 * time.Second
|
||||
defaultLauncherOnlineReportInterval = 30 * time.Second
|
||||
defaultPVFPath = "data/dnf/Script.pvf"
|
||||
defaultPartyUDPRelayPortStart = 30000
|
||||
// defaultPartyUDPRelayPortCount 是队伍 UDP 中继的端口池大小。
|
||||
//
|
||||
// 一个四人队需要 4×3 = 12 条有向腿(每对成员两个方向各占一个端口),所以池子
|
||||
// 直接决定"同时能有多少个队伍拿到中继":64 ≈ 5 个四人队,512 ≈ 42 个。
|
||||
// 拿不到中继的队伍会回退成"直连打洞",两边都在 NAT 后面时就连不上 —— 玩家看到
|
||||
// 的就是红电脑;而旧值 64 太小的直接证据是线上实测已占用 40/64(≈3 个四人队)。
|
||||
// 参照实现的池子是 1000(队伍侧约 500 ≈ 40 个并发四人队),这里对齐到同一量级。
|
||||
// 注意:线上 instance.json 若显式写了 partyUdpRelayPortCount,需要一并改大。
|
||||
defaultPartyUDPRelayPortCount = 512
|
||||
)
|
||||
|
||||
type options struct {
|
||||
channelListen string
|
||||
gameListenHost string
|
||||
channelInfoFile string
|
||||
channelServerID int
|
||||
channelAdvertiseID int
|
||||
channelInfoBodyMode string
|
||||
// channelTableStrict 恢复"频道表必须含默认城镇频道 11 / 引导频道 19"的旧硬校验。
|
||||
//
|
||||
// 默认关闭:运维会主动只开少数频道(例如只留一个特殊区域频道),此时
|
||||
// - 缺少引导频道 19 不该让服务端起不来(Build90CNOnlineScript 的化石校验,见其注释);
|
||||
// - 缺少默认城镇频道 11 不该让热重载被拒(原来会让运维工具"关不掉")。
|
||||
// 关掉后这两条降级为启动/热重载日志里的告警;"表里一个频道都没有""一个游戏端口都没有"
|
||||
// 这类真正致命的状态仍然是硬错误。
|
||||
channelTableStrict bool
|
||||
serverIP string
|
||||
partyUDPBindIP string
|
||||
scriptVersion string
|
||||
gameOuterToken uint32
|
||||
gameInitialMode string
|
||||
gamePreBootstrap string
|
||||
gamePostBootstrap string
|
||||
gameUpperHeader string
|
||||
gameUpperBodyCodec string
|
||||
gameUpperClientBodyCodec string
|
||||
gameDprotoMode string
|
||||
// gameS2CCipher 控制 S2C 回包二次加密(off / shadow / on,默认 off)。
|
||||
// gameS2CMasterKeyBase64 覆盖固化主密钥,仅用于本地联调与灰度。
|
||||
gameS2CCipher string
|
||||
gameS2CMasterKeyBase64 string
|
||||
initialUDPPort1 int
|
||||
initialUDPPort2 int
|
||||
commandCount int
|
||||
notificationCount int
|
||||
maxPacketBytes int
|
||||
packetLogEnabled bool
|
||||
packetLogPath string
|
||||
// recordDir 三类审计记录(点券消费/对话/副本)的落盘目录。
|
||||
// 默认取 packet_log 的同级 records/,让"日志放哪、记录就放哪";
|
||||
// DNFBRIDGE_RECORD_DIR 可覆盖。
|
||||
recordDir string
|
||||
accountID string
|
||||
accountPrefix string
|
||||
pvfPath string
|
||||
pvfMaxBytes int64
|
||||
partyUDPRelayEnabled bool
|
||||
partyUDPRelayPortStart int
|
||||
partyUDPRelayPortCount int
|
||||
raidCreatedRecoverySeed string
|
||||
regionID int
|
||||
// dungeonRoomPartySlotOff drops the dungeon-room op9 party slot table that
|
||||
// arms the current client's host arbitration (sceneActorManager+0x48/+0x68).
|
||||
// Off by default; it exists for attribution runs, at the cost of every party
|
||||
// member answering "not host" and nobody simulating the room.
|
||||
dungeonRoomPartySlotOff bool
|
||||
// raidLukeWeeklyLimitEnforced turns Luke's [WEEKLY ENTER COUNT] from an
|
||||
// observation into a refusal. OFF by default: the reset weekday the document
|
||||
// does not declare is still unconfirmed, and enforcing a wrongly guessed node
|
||||
// locks players out of a raid they are entitled to enter. Turn it on only
|
||||
// after the node has been read off the observation lines in
|
||||
// raid_luke_weekly_observation.go. Turning it on requires a durable weekly
|
||||
// counter to exist first -- see that file's header.
|
||||
raidLukeWeeklyLimitEnforced bool
|
||||
// launcherAuth* gate external game connections through the loopback
|
||||
// launcher verifier before any DNF protocol flow starts. All four options
|
||||
// default to disabled/empty so local PID / WireGuard / Wine fast paths are
|
||||
// untouched unless an operator explicitly enables the external mode.
|
||||
launcherAuthEnabled bool
|
||||
launcherAuthVerifyURL string
|
||||
launcherAuthInternalToken string
|
||||
launcherAuthTimeout time.Duration
|
||||
// launcherIdentity* bind every game connection to the deployment's own
|
||||
// launcher. launcherIdentityMode is off/audit/enforce; the keys are the
|
||||
// pinned ECDSA P-256 public keys (PEM or base64 DER). An operator who has
|
||||
// not run keygen gets mode "off", which leaves every legacy path intact.
|
||||
launcherIdentityMode string
|
||||
launcherIdentityPublicKeys []string
|
||||
launcherIdentityAllowWireGuard bool
|
||||
// launcherOnlineReport* push the real online account count to the ops
|
||||
// tool so its /launcher/v1/health can show/limit capacity. Enabled by
|
||||
// default; harmless on the same host as the ops tool.
|
||||
launcherOnlineReportEnabled bool
|
||||
launcherOnlineReportInterval time.Duration
|
||||
}
|
||||
|
||||
func optionsFromConfig(cfg config.ServiceConfig) options {
|
||||
opts := options{
|
||||
channelListen: envString("DNFBRIDGE_CHANNEL_LISTEN", defaultChannelListen),
|
||||
gameListenHost: strings.TrimSpace(os.Getenv("DNFBRIDGE_GAME_LISTEN_HOST")),
|
||||
channelInfoFile: firstEnv([]string{"DNFBRIDGE_CHANNEL_INFO_PATH", "DNF_CHANNEL_INFO_FILE"}, defaultChannelInfoFile),
|
||||
channelServerID: envIntAny([]string{"DNFBRIDGE_CHANNEL_SERVER_INDEX", "DNF_CHANNEL_SOURCE_SERVER_INDEX"}, 1),
|
||||
channelAdvertiseID: envIntAny(
|
||||
[]string{"DNFBRIDGE_CHANNEL_ADVERTISE_SERVER_INDEX", "DFO_CHANNEL_SERVER_INDEX"},
|
||||
-1,
|
||||
),
|
||||
channelInfoBodyMode: normalizeChannelInfoBodyMode(envString(
|
||||
"DNFBRIDGE_CHANNEL_INFO_BODY_MODE",
|
||||
channelInfoBodyLatest,
|
||||
)),
|
||||
channelTableStrict: envBool("DNFBRIDGE_CHANNEL_TABLE_STRICT", false),
|
||||
serverIP: chooseServerIP(cfg),
|
||||
partyUDPBindIP: choosePartyUDPBindIP(cfg),
|
||||
scriptVersion: envString("DNF_SCRIPT_VERSION", defaultScriptVersion),
|
||||
gameOuterToken: envUint32Any([]string{"DNFBRIDGE_GAME_OUTER_TOKEN", "DFO_GAME_OUTER_TOKEN"}, 0),
|
||||
gameInitialMode: normalizeGameInitialMode(envString("DNFBRIDGE_GAME_INITIAL_MODE", gameInitialModeNotice)),
|
||||
gamePreBootstrap: normalizeGamePreBootstrap(envString("DNFBRIDGE_GAME_PRE_BOOTSTRAP", gamePreBootstrapNone)),
|
||||
gamePostBootstrap: normalizeGamePostBootstrap(envString("DNFBRIDGE_GAME_POST_BOOTSTRAP", gamePostBootstrapNone)),
|
||||
gameUpperHeader: normalizeGameUpperHeader(envString("DNFBRIDGE_GAME_UPPER_HEADER", gameUpperHeaderChannel13)),
|
||||
gameUpperBodyCodec: normalizeGameUpperBodyCodec(envString(
|
||||
"DNFBRIDGE_GAME_UPPER_BODY_CODEC",
|
||||
gameUpperBodyCodecAuto,
|
||||
)),
|
||||
gameUpperClientBodyCodec: normalizeGameUpperClientBodyCodec(envString(
|
||||
"DNFBRIDGE_GAME_UPPER_CLIENT_BODY_CODEC",
|
||||
gameUpperClientBodyCodecPlain,
|
||||
)),
|
||||
gameDprotoMode: normalizeGameDprotoMode(envString("DNFBRIDGE_DPROTO_MODE", gameDprotoModeLegacy)),
|
||||
gameS2CCipher: normalizeGameS2CCipher(envString("DNFBRIDGE_GAME_S2C_CIPHER", gameS2CCipherOff)),
|
||||
gameS2CMasterKeyBase64: strings.TrimSpace(os.Getenv("DNFBRIDGE_GAME_S2C_KEY_BASE64")),
|
||||
initialUDPPort1: envInt("DNF_INITIAL_UDP_PORT1", defaultInitialUDPPort1),
|
||||
initialUDPPort2: envInt("DNF_INITIAL_UDP_PORT2", defaultInitialUDPPort2),
|
||||
commandCount: envInt("DNF_COMMAND_PACKET_COUNT", defaultCommandCount),
|
||||
notificationCount: envInt("DNF_NOTIFICATION_PACKET_COUNT", defaultNotificationCount),
|
||||
maxPacketBytes: envInt("DNF_MAX_PACKET_BYTES", defaultMaxPacketBytes),
|
||||
packetLogEnabled: envBool("DNFBRIDGE_PACKET_LOG", false),
|
||||
packetLogPath: envString("DNFBRIDGE_PACKET_LOG_PATH", "logs/packet_log.txt"),
|
||||
recordDir: envString("DNFBRIDGE_RECORD_DIR", defaultRecordDirFor(envString("DNFBRIDGE_PACKET_LOG_PATH", "logs/packet_log.txt"))),
|
||||
accountID: envString("DNFBRIDGE_ACCOUNT_ID", ""),
|
||||
accountPrefix: envString("DNFBRIDGE_ACCOUNT_PREFIX", defaultAccountPrefix),
|
||||
pvfPath: choosePVFPath(cfg),
|
||||
pvfMaxBytes: choosePVFMaxBytes(cfg),
|
||||
partyUDPRelayEnabled: envBool("DNFBRIDGE_PARTY_UDP_RELAY_ENABLED", false),
|
||||
partyUDPRelayPortStart: envInt("DNFBRIDGE_PARTY_UDP_RELAY_PORT_START", defaultPartyUDPRelayPortStart),
|
||||
partyUDPRelayPortCount: envInt("DNFBRIDGE_PARTY_UDP_RELAY_PORT_COUNT", defaultPartyUDPRelayPortCount),
|
||||
dungeonRoomPartySlotOff: envBool("DNFBRIDGE_DUNGEON_ROOM_PARTY_SLOT_OFF", false),
|
||||
raidLukeWeeklyLimitEnforced: envBool("DNFBRIDGE_RAID_LUKE_WEEKLY_LIMIT_ENFORCED", false),
|
||||
raidCreatedRecoverySeed: strings.TrimSpace(os.Getenv(
|
||||
"DNFBRIDGE_RAID_CREATED_RECOVERY_SEED_BASE64",
|
||||
)),
|
||||
regionID: envInt("DNFBRIDGE_REGION_ID", 1),
|
||||
launcherAuthEnabled: envBool(
|
||||
"DNFBRIDGE_LAUNCHER_AUTH_ENABLED",
|
||||
false,
|
||||
),
|
||||
launcherAuthVerifyURL: envString(
|
||||
"DNFBRIDGE_LAUNCHER_AUTH_VERIFY_URL",
|
||||
defaultLauncherAuthVerifyURL,
|
||||
),
|
||||
launcherAuthInternalToken: envString(
|
||||
"DNFBRIDGE_LAUNCHER_AUTH_INTERNAL_TOKEN",
|
||||
"",
|
||||
),
|
||||
launcherAuthTimeout: envDuration(
|
||||
"DNFBRIDGE_LAUNCHER_AUTH_TIMEOUT",
|
||||
defaultLauncherAuthTimeout,
|
||||
),
|
||||
launcherIdentityMode: envString(
|
||||
"DNFBRIDGE_LAUNCHER_LID_MODE",
|
||||
LauncherIdentityModeOff,
|
||||
),
|
||||
launcherIdentityPublicKeys: envStringList(
|
||||
"DNFBRIDGE_LAUNCHER_LID_PUBLIC_KEYS",
|
||||
),
|
||||
launcherIdentityAllowWireGuard: envBool(
|
||||
"DNFBRIDGE_LAUNCHER_LID_ALLOW_WIREGUARD",
|
||||
false,
|
||||
),
|
||||
launcherOnlineReportEnabled: envBool(
|
||||
"DNFBRIDGE_LAUNCHER_ONLINE_REPORT_ENABLED",
|
||||
true,
|
||||
),
|
||||
launcherOnlineReportInterval: envDuration(
|
||||
"DNFBRIDGE_LAUNCHER_ONLINE_REPORT_INTERVAL",
|
||||
defaultLauncherOnlineReportInterval,
|
||||
),
|
||||
}
|
||||
if opts.maxPacketBytes <= 0 {
|
||||
opts.maxPacketBytes = defaultMaxPacketBytes
|
||||
}
|
||||
if opts.channelServerID <= 0 {
|
||||
opts.channelServerID = 1
|
||||
}
|
||||
if opts.channelAdvertiseID < 0 {
|
||||
opts.channelAdvertiseID = opts.channelServerID
|
||||
}
|
||||
// Current NoPack's class-0/op273 friend-row reader rejects region values
|
||||
// >= 250. Zero is also kept out of operator configuration because it is a
|
||||
// client-reserved resource-table entry rather than a normal region.
|
||||
if opts.regionID < 1 || opts.regionID >= 250 {
|
||||
opts.regionID = 1
|
||||
}
|
||||
if opts.partyUDPRelayPortStart < 1 || opts.partyUDPRelayPortStart > 65535 {
|
||||
opts.partyUDPRelayPortStart = defaultPartyUDPRelayPortStart
|
||||
}
|
||||
if opts.partyUDPRelayPortCount < 1 ||
|
||||
opts.partyUDPRelayPortCount > 1024 ||
|
||||
opts.partyUDPRelayPortStart+opts.partyUDPRelayPortCount-1 > 65535 {
|
||||
opts.partyUDPRelayPortCount = defaultPartyUDPRelayPortCount
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func choosePVFPath(cfg config.ServiceConfig) string {
|
||||
for _, key := range []string{"DNFBRIDGE_PVF_PATH", "DNF_SCRIPT_PVF_PATH", "DNF_PVF_PATH"} {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if value := strings.TrimSpace(cfg.PVF.Path); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultPVFPath
|
||||
}
|
||||
|
||||
func choosePVFMaxBytes(cfg config.ServiceConfig) int64 {
|
||||
for _, key := range []string{"DNFBRIDGE_PVF_MAX_BYTES", "DNF_SCRIPT_PVF_MAX_BYTES", "DNF_PVF_MAX_BYTES"} {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
parsed, err := strconv.ParseInt(value, 10, 64)
|
||||
if err == nil && (parsed > 0 || parsed == platformpvf.NoMaxBytes) {
|
||||
return parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
if cfg.PVF.MaxBytes > 0 || cfg.PVF.MaxBytes == platformpvf.NoMaxBytes {
|
||||
return cfg.PVF.MaxBytes
|
||||
}
|
||||
return platformpvf.DefaultMaxBytes
|
||||
}
|
||||
|
||||
func normalizeChannelInfoBodyMode(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", channelInfoBodyLatest, "csharp", "legacy", "legacy_csharp", "old_csharp":
|
||||
// IDA/MCP 与 86JP_protocol.log 复核后,最新客户端 0x12 明文必须带 server_count/server_id/count。
|
||||
// 旧 C# 的 2 字节保留位前缀会导致客户端把 0x000B0000 当 server_count 反复解析。
|
||||
return channelInfoBodyLatest
|
||||
default:
|
||||
return channelInfoBodyLatest
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGameInitialMode(value string) string {
|
||||
// DOVE/C# 对齐:首包模式不再可配置,game 建连后固定主动发送 class=0 op=1。
|
||||
return gameInitialModeNotice
|
||||
}
|
||||
|
||||
func normalizeGamePreBootstrap(value string) string {
|
||||
// C# 端 game 连接后只主动发首包;其余初始化包都由客户端 C2S 触发。
|
||||
// 旧探针开关保留为兼容配置名,但正常链路统一关闭,避免提前推送请求触发包。
|
||||
return gamePreBootstrapNone
|
||||
}
|
||||
|
||||
func normalizeGamePostBootstrap(value string) string {
|
||||
// C# 行为证据:GET_USERINFO/SELECT_CHARACTER 后续包必须等客户端请求后再回。
|
||||
// 因此 post bootstrap 探针不再通过环境变量开启。
|
||||
return gamePostBootstrapNone
|
||||
}
|
||||
|
||||
func normalizeGameUpperHeader(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", gameUpperHeaderChannel13, "13", "channel", "raw13":
|
||||
return gameUpperHeaderChannel13
|
||||
case gameUpperHeaderServer16, "16", "raw16":
|
||||
return gameUpperHeaderServer16
|
||||
default:
|
||||
return gameUpperHeaderChannel13
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGameUpperBodyCodec(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", gameUpperBodyCodecAuto, "codec", "encoded", "latest":
|
||||
return gameUpperBodyCodecAuto
|
||||
case gameUpperBodyCodecPlain, "plain", "none", "off", "decodecopy":
|
||||
return gameUpperBodyCodecPlain
|
||||
default:
|
||||
return gameUpperBodyCodecAuto
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGameUpperClientBodyCodec(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", gameUpperClientBodyCodecPlain, "plain", "none", "off", "disabled", "passthrough":
|
||||
return gameUpperClientBodyCodecPlain
|
||||
case gameUpperClientBodyCodecProbe, "detect", "log", "log_only", "log-only":
|
||||
return gameUpperClientBodyCodecProbe
|
||||
case gameUpperClientBodyCodecNative, "decode", "decoded", "server", "server_native":
|
||||
return gameUpperClientBodyCodecNative
|
||||
default:
|
||||
return gameUpperClientBodyCodecPlain
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeGameDprotoMode(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case gameDprotoModeNative, "native_dproto", "protected":
|
||||
return gameDprotoModeNative
|
||||
case "", gameDprotoModeLegacy, "legacy", "patch", "compat":
|
||||
return gameDprotoModeLegacy
|
||||
default:
|
||||
return gameDprotoModeLegacy
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeGameS2CCipher 归一化 S2C 回包加密模式。默认 off:
|
||||
// 任何拼写错误都不许悄悄打开加密——那会让客户端收到乱码并表现为"卡住"。
|
||||
func normalizeGameS2CCipher(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case gameS2CCipherOn, "enable", "enabled", "true", "1":
|
||||
return gameS2CCipherOn
|
||||
case gameS2CCipherShadow, "observe", "dry_run", "dry-run":
|
||||
return gameS2CCipherShadow
|
||||
case "", gameS2CCipherOff, "disable", "disabled", "false", "0":
|
||||
return gameS2CCipherOff
|
||||
default:
|
||||
return gameS2CCipherOff
|
||||
}
|
||||
}
|
||||
|
||||
func chooseServerIP(cfg config.ServiceConfig) string {
|
||||
for _, key := range []string{"DNFBRIDGE_SERVER_IP", "DNFBRIDGE_PUBLIC_HOST", "DFO_PUBLIC_SERVER_IP", "SERVER_IP"} {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
if host := hostFromAddr(cfg.Service.PublicAddr); host != "" {
|
||||
return host
|
||||
}
|
||||
return defaultServerIP
|
||||
}
|
||||
|
||||
// choosePartyUDPBindIP selects the local address the party UDP reachability
|
||||
// probe binds. The legacy (pre-merge) behaviour binds 0.0.0.0 so the probe
|
||||
// receives datagrams on every interface. Public-cloud NAT rewrites inbound
|
||||
// datagrams to the private interface address, so a listener pinned to the
|
||||
// advertised public IP never sees them; binding a single interface IP is only
|
||||
// useful when the operator wants to restrict the probe to one NIC. Set
|
||||
// DNFBRIDGE_UDP_BIND_IP=<ip> to override the wildcard default.
|
||||
func choosePartyUDPBindIP(cfg config.ServiceConfig) string {
|
||||
if value := strings.TrimSpace(os.Getenv("DNFBRIDGE_UDP_BIND_IP")); value != "" {
|
||||
return value
|
||||
}
|
||||
return "0.0.0.0"
|
||||
}
|
||||
|
||||
func hostFromAddr(addr string) string {
|
||||
addr = strings.TrimSpace(addr)
|
||||
if addr == "" {
|
||||
return ""
|
||||
}
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
return strings.Trim(host, "[]")
|
||||
}
|
||||
return strings.Trim(addr, "[]")
|
||||
}
|
||||
|
||||
func envString(key, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func firstEnv(keys []string, fallback string) string {
|
||||
for _, key := range keys {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// envStringList reads a comma or semicolon separated list. Pinned launcher
|
||||
// identity keys travel this way because each one is a single-line base64 blob
|
||||
// and an operator may pin more than one launcher build at a time.
|
||||
func envStringList(key string) []string {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
fields := strings.FieldsFunc(value, func(r rune) bool {
|
||||
return r == ',' || r == ';' || r == '\n' || r == '\r'
|
||||
})
|
||||
values := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if trimmed := strings.TrimSpace(field); trimmed != "" {
|
||||
values = append(values, trimmed)
|
||||
}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envIntAny(keys []string, fallback int) int {
|
||||
for _, key := range keys {
|
||||
if strings.TrimSpace(os.Getenv(key)) == "" {
|
||||
continue
|
||||
}
|
||||
return envInt(key, fallback)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func envUint32(key string, fallback uint32) uint32 {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := strconv.ParseUint(value, 10, 32)
|
||||
if err != nil {
|
||||
parsed, err = parseFlexibleUint32(value)
|
||||
}
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return uint32(parsed)
|
||||
}
|
||||
|
||||
func envUint32Any(keys []string, fallback uint32) uint32 {
|
||||
for _, key := range keys {
|
||||
if strings.TrimSpace(os.Getenv(key)) == "" {
|
||||
continue
|
||||
}
|
||||
return envUint32(key, fallback)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func parseFlexibleUint32(value string) (uint64, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
trimmed := strings.TrimPrefix(strings.TrimPrefix(value, "0x"), "0X")
|
||||
if len(trimmed) > 8 {
|
||||
// 启动参数里的 32 位 hex token 不是 uint32;调试时传入该值时只取前 4 字节作为 outer 校验候选。
|
||||
trimmed = trimmed[:8]
|
||||
}
|
||||
return strconv.ParseUint(trimmed, 16, 32)
|
||||
}
|
||||
|
||||
func envDuration(key string, fallback time.Duration) time.Duration {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return fallback
|
||||
}
|
||||
parsed, err := time.ParseDuration(value)
|
||||
if err != nil || parsed <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
func envBool(key string, fallback bool) bool {
|
||||
value := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
|
||||
switch value {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
case "0", "false", "no", "off":
|
||||
return false
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user