# ===== 雷达节点配置 =====
# 复制为 .env；改完重启: systemctl restart radar-node

# --- 原版雷达界面 ---
NODE_RADAR_HOST=0.0.0.0
NODE_RADAR_PORT=9718

# --- SOCKS5 监听 ---
NODE_HOST=0.0.0.0
NODE_PORT=19190
# UDP 内核缓冲（字节）。低于 1MB 高负载会丢包，记得同步调 rmem_max
NODE_UDP_BUFFER=2097152

# --- UDP 中继端口段（必须落在防火墙已放行范围，否则游戏流量被丢包）---
NODE_UDP_PORT_MIN=39190
NODE_UDP_PORT_MAX=40189
# 中继零流量这么久就回收关联、释放端口（秒）
# ★ 2026-10-18 改 120 -> 45：手机端把 UDP 超时从 60 秒改成 1 小时之后，
#   一条会话能被端上留很久；实测一个账号同时挂 94 条中继、吃掉 94 个端口
#   （端口池只有 1000 个）。16 个人就会把池子吃光，后来的人 bind 不到端口。
#   45 秒没流量就回收，中继寿命从 240 秒降到约 60 秒，单账号并发回到 20~30。
NODE_UDP_IDLE_RECLAIM=180
# 中继零包这么久判定为僵尸并强制回收（秒）
NODE_UDP_ZOMBIE_RECLAIM=90
# SOCKS5 控制连接的读取超时（秒）——它同时是「多久检查一次有没有流量」的周期。
# ★ 2026-10-18 改 60 -> 15：回收精度从 60 秒提高到 15 秒，
#   配合上面的 45 秒阈值，闲置中继大约 60 秒内一定被扫掉。
NODE_UDP_READ_TIMEOUT=30
# 控制连接断开后，UDP 中继再活多久才真正拆掉（秒）。0 = 恢复旧行为。
# ★ 现场实测（36.139.14.98，13 小时）：建立 12,927 条 UDP 关联、结束 13,013 条，
#   每一条存活时间都正好 60.1~60.2 秒，其中 97% 是「控制 TCP 被客户端关掉 →
#   旧代码立刻 break」这条路径，而且原来这条路上一条日志都没有。
#   立刻拆 = 在途回包全丢 + 端口立刻释放 → 玩家体感「位置被拉回、雷达断一下」。
#   现在给 8 秒宽限，期间 UDP 还有包就续期，上限 180 秒。
NODE_UDP_EOF_GRACE_SEC=30
NODE_UDP_EOF_GRACE_MAX_SEC=180
# UDP 中继绑定端口时的最大尝试次数（2026-10-17 修）。
#   原实现把整段 1000 个端口全扫一遍，端口紧张时一次关联试上千次，
#   每次失败都产生带调用栈的异常 -> 实测堆内存涨到 3.7GB / 2450 万对象
#   -> 整机卡死被硬件看门狗重启。现在有界探测：随机起点环形推进，最多这么多次。
#   40 = 端口回收收紧（IDLE_RECLAIM=120）后足够；全占满就回 GENERAL 让客户端重试。
NODE_UDP_BIND_TRIES=40
#   以上四项是「UDP 中继僵尸通道」事故的修复参数：
#   实测建立 1785 / 结束 885 = 900 条僵尸通道，每条每 15 秒醒一次并漏 8 个对象，
#   13 分钟堆出 2450 万对象 / 3.7GB。收紧后内存增速 374MB/分钟 -> 0.8MB/分钟。

# --- 面板联动（留空则不做鉴权，任何人可连，仅内网测试用）---
# ★ 指向【本机面板】，不要指向其他节点
NODE_PANEL_URL=http://127.0.0.1:7218
NODE_TOKEN=
NODE_HEARTBEAT_SEC=30

# --- 节点标识（★ 必须与面板 nodes 表里的行一致）---
NODE_ID=ser998967364370
NODE_NAME=ser998967364370
# 地区标签（显示用）
NODE_REGION=

# --- 对外地址（云主机在 NAT 后必须显式配置）---
# 对外展示地址（公网 IP / 域名）；留空则自动探测
NODE_PUBLIC_HOST=
# ★ UDP ASSOCIATE 对外公布的地址；留空则自动探测
#   配错会导致客户端把 UDP 发到错误地址 → 游戏卡加载 85%
NODE_UDP_PUBLIC_HOST=

# --- 日志 ---
NODE_LOG_DIR=/var/log/radar-node
NODE_LOG_LEVEL=info
# 1 = 把解密后的明文前 64 字节也写日志（调试用，量大）
NODE_LOG_PLAINTEXT=0

# --- 排查接口（只监听本机，走 SSH 隧道访问）---
NODE_DIAG_ENABLED=1
NODE_DIAG_HOST=127.0.0.1
NODE_DIAG_PORT=19191

# --- 账号网关 ---
GATEWAY_INSTANCE_PORT_MIN=17000
GATEWAY_INSTANCE_PORT_MAX=17999
GATEWAY_IDLE_TTL=300
GATEWAY_SESSION_TTL=43200
GATEWAY_SUB_TOKEN_TTL=900
# 单个账号雷达实例（er_radar.py）的常驻内存上限（MB）。0 = 关闭看护。
# ★ 实测一个实例会涨到 347 MB 并保持；16 个账号 ≈ 5.6 GB，会先撞穿 radar.service
#   的 MemoryMax（物理内存 20%），导致内核把整个网关一起杀掉。
#   超上限只回收那一个账号的实例，页面重连时自动重建，不影响别人。
GATEWAY_INSTANCE_MEM_MAX_MB=420
GATEWAY_INSTANCE_MEM_MIN_AGE=180
NODE_PANEL_DIR=/root/radar-src-lanes/panel

# --- 容量（人数，非连接数）---
# ★ 这两个值会被 autotune.sh 按「网关预算 ÷ 单账号 420MB」重算并覆盖：
#    2G→1 人 / 4G→3 人 / 8G→6 人 / 16G→12 人 / 32G→25 人
#   别手抄一个 30 到 4G 机器上，第三个人进来网关就撞线整片黑。
NODE_CAPACITY_SOFT=4
NODE_CAPACITY_HARD=6

# --- 地图/3D 静态资产外置图床 ---
GATEWAY_ASSET_BASE=http://157.250.207.207:2580
# 这些前缀不走图床、始终由本机直读（免登录），用于本地覆盖/修复过的资源
GATEWAY_ASSET_SKIP=/avatars/

# --- 解析运行层 ---
# 0=单线程 EngineWorker；1=单进程解析（GIL 隔离）；>=2=按账号哈希分片多进程
# ★ 合并期改动：原来写 0（单线程），会**覆盖** config.py 的默认 2。
#   deploy.sh 每次重装都用本文件重建 .env，所以写 0 = 「解析搬出事件循环」
#   这个修复被自己的配置文件原地作废。
NODE_PARSE_PROCESSES=2
# 每账号独立缓冲上限（包数）：某账号洪泛只丢它自己的最旧包，不挤掉别人
# ★ 2026-10-17 修：500 太小 —— 对局主流量一个账号就能顶满，一顶满就按配额
#   强删最旧帧（lane_drop_quota），把**还没发出去的握手帧**删掉，雷达缺的正是那几帧。
#   实测提到 3000 后不再误删。退回值 = 500。
NODE_PARSE_ACCOUNT_QUEUE=3000
# --- 解析节奏（越小越"跟手"，CPU 越高）---
# ★ 代码里原来**硬编码** PARSE_PERIOD = 0.5 秒 = 雷达数据 2 秒才刷新一次，
#   实测就是"刷新有点延迟"的原因。默认已调到 0.2 秒（约 5 次/秒）。
#   机器富余可以试 0.1（10 次/秒）；CPU 吃紧就调回 0.3~0.5。
NODE_PARSE_PERIOD=0.1
# 单账号单片最长（秒）：决定事件循环单次停顿上限
NODE_PARSE_SLICE=0.005
# 本轮解析最多占多少墙钟：剩下明确留给转发/心跳
NODE_PARSE_DUTY=0.85
# 单片最多处理帧数：防"小片变大块"
NODE_PARSE_FRAMES=600
# --- 雷达引擎（er_radar.py）每 tick 耗时预算 ---
# ★ 2026-10-17 修（__FIX_PARSE_BUDGET__，测试机已验证）：
#   代码里原来 hardcode TICK_SEC=1/60、TICK_BUDGET_SEC=0.012。
#   实测 20879 个 tick：平均 0.88ms、最慢 236ms，平均只占 15ms 预算的 5.9%。
#   这里把预算提到 15ms（仍 < 16.7ms 节拍，不会 tick 重叠），消化能力 +25%。
#   心跳频率故意不动。两个键留空则用代码默认（60 / 15）。
NODE_ER_TICK_HZ=60
NODE_ER_TICK_BUDGET_MS=15

# --- 分通道（线路A 只转发 / 线路B 解密+落盘+解析）---
# ★ 为什么写在这儿：原版 deploy.sh 第 642 行那个 `.env.keep-$STAMP` 分支
#   全脚本没有任何地方创建，所以**每次重装都会用本文件重建 .env** ——
#   分通道开关如果不写在这里，重装一次就丢了（表现为"改了代码却没生效"）。
#   mono = 改造前的单进程行为；split = 两个进程各跑各的 GIL（推荐）
LANE_MODE=split
LANE_SOCKET=/run/radar-decode.sock
# 线路A 内队列上限（条）：满了丢最旧，绝不阻塞转发
LANE_FORWARD_QUEUE=60000
LANE_RECONNECT_SEC=1.0
LANE_SEND_TIMEOUT=5.0
# 线路B 诊断口（只绑本机）
LANE_DECODE_DIAG_HOST=127.0.0.1
LANE_DECODE_DIAG_PORT=19193
# 线路A 拉取线路B 统计的节拍/超时（拉不到就退回本地口径，不阻塞）
LANE_REMOTE_STATS_SEC=1.0
LANE_REMOTE_STATS_TIMEOUT=0.6
# 线路B 的日志目录（必须与线路A 分开，否则轮转会互踩）
LANE_DECODE_LOG_DIR=/var/log/radar-node/decode
# 线路B 由 main.py 当子进程拉起（0=自己管）；挂了自动重启，反复挂会放弃并报错
LANE_DECODE_AUTOSTART=1
LANE_RESTART_DELAY=2.0
LANE_MAX_FAIL=5
LANE_FAIL_WINDOW=120

# --- 账号活跃度 / 单账号内存上限（★ 2026-10-16 事故相关调优键，别再调大）---
# 单账号未绑定流的最大挂起数。bank.py 代码默认就是 768。
# ★ 千万别调大：2026-10-16 事故就是把这里放大，未绑定流内存线性增长，实例被拖死。
NODE_BANK_MAX_PENDING=4096
# __PENDINGGLOBAL__ 全流 pending 总闸（包个数，2026-10-17 修）。
#   问题：每条流最多 768 个包（上面那行）、流最多 1024 条（MAX_FLOWS），
#   乘起来是 78.6 万包 ≈ 1.2GB 的隐藏天花板。实测 decode_node 就沿这条线
#   一路涨，约 12 小时撞到守护的 6GB 重启线。另外 _pending_clear 原来
#   只 clear() 不减计数，pending_total 虚高，任何总闸都会失效 —— 已一并修掉。
#   60000 = 正常只用到 1~2 万，留 3 倍余量；超了按队头最旧批量丢，只丢数据不丢流。
# 死流靠 NODE_BANK_FLOW_TTL_SEC 兜底，总闸抬高到 20 万（约 30~40MB），不再靠硬裁
NODE_BANK_MAX_PENDING_TOTAL=120000
# ---- 死流清理（2026-10-17 修：实测丢了 18.8 万个包）----
# 原来「流」只在 len(flows) >= 1024 时淘汰一条，而实际永远只有 400~900 条，
# 于是一条都不清：死掉的流（QUIC/广告/心跳）各自抱着最多 768 个包不放，
# 全流 pending 长期顶在 60000 总闸上，总闸只能不停裁 —— 真正在对局的流被误伤。
# 下面三个值把这件事按「年龄」和「缓存量」各收一道。
#
# 多少秒没收到包就整条流清掉（连同它占的 pending）。0/负数 = 不清理
# ★ 2026-10-18 改 400 -> 150：人多玩久了流表会涨到 60+ 条/账号（正常 5~15），
#   死流占着 pending 把总闸顶满，握手包在绑上之前就被裁掉 -> 雷达整片空白。
NODE_BANK_FLOW_TTL_SEC=150
# 未绑定流的包缓存上限（比 NODE_BANK_MAX_PENDING 小得多）。
#   绑定判定只看「最近 3 条以上下行信封」和 pend[-8:]，64 个足够；
#   绑定成功的流仍按 NODE_BANK_MAX_PENDING(768) 走，不受影响。
# ★★ 2026-10-18 改 768 -> 128（这条很关键）：代码里的默认本来就是 64
#   （bank.py: UNBOUND_MAX_PENDING = _env_int("NODE_BANK_UNBOUND_PENDING", 64)），
#   是这个模板把它抬到 768，等于 190 条未绑定流 × 768 = 14.6 万包，
#   而总闸只有 6 万 -> 永远在按「最旧优先」裁 -> 握手包先被裁掉
#   -> 绑不上 -> 流永远留在未绑定状态 -> pending 更满（正反馈）。
#   改回 128（不用 64：老注释说 64/256 试过偏紧、会漏人；128 是折中）：
#   380 条未绑定流 × 128 = 4.9 万 < 6 万总闸，不再饱和，也不会漏人。
NODE_BANK_UNBOUND_PENDING=512
# __UNBOUNDTTL__ 未绑定密钥的流回收 TTL（秒）。
#   太长 = 死流抱着包不放，pending 被堆满填死（长沙用 3600 时卡死 8 小时、
#          广州涨到 82% 只涨不降，bound_flows 直接掉 0）；
#   太短 = 样本不够，走宽松判定 any_recent_envelope_confirmed 错绑密钥
#          （江苏用 90 秒时踩过，表现为实例反复整局重解）。
#   300 是实测折中值。
NODE_BANK_UNBOUND_FLOW_TTL_SEC=300
# 每多少个包扫一次死流（成本摊薄；0 = 不扫）。扫一遍 500 条流约 20~30 微秒
NODE_BANK_PURGE_EVERY=512
# 账号多久没动静就判定为「空闲」并回收（秒）。config.py 代码默认 20。
# ★ 20 太小：实例正在读的抓包局会被当空闲删掉 → 抓包变空。
# ★★ 2026-10-18 改 90 -> 300（配合"人多玩久了雷达空白"的根因）：
#   空闲就 rotate，会把一局切成很多段 run；而 UE4 的握手只在开局发生一次，
#   中途切出来的 run 没有握手 -> 探测器判它"非战斗流" -> 实例永远不绑定视图。
#   实测那台长沙节点 30 分钟轮转了 139 次，绝大多数 run 都是半路切出来的。
#   拉到 300 秒之后，绝大多数 run 是"开局那次"建的，天然带握手。
NODE_ACCOUNT_IDLE_SEC=300
# 战斗流判定「阴性结果」的有效期（秒）。实例 er_radar 用它决定隔多久重探一次。
# ★ 2026-10-18 新增：原来 n_ps=0 会被**永久缓存**（还落盘），run 中途被判死后
#   永远不重探 -> 实例 current=null -> 雷达整片空白，直到玩家重开一局。
#   现在阴性 60 秒后允许重探，阳性（n_ps>=3）仍然永久缓存。
NODE_DETECT_NEG_TTL_SEC=60
# 空白兜底：实例连续这么多秒没有任何"合格战斗流"时，强制挂到最近还在写的那条流上
#   先把位置画出来（宁可画得不全，也不给空页面）。0 = 关闭兜底。
# ★ 2026-10-18 新增。实测：没有开局握手的 run（半路切出来的），旧代码 120 秒
#   全程 current=null；加上兜底后 25 秒（阈值设 20 秒）就挂上并出图。
NODE_LIVE_BLIND_FALLBACK_SEC=90
# 已锁定的流超过这么多秒没写入（frames.jsonl 的 mtime），而同一账号还有更新的流，
# 就主动切过去（治"玩一半换图后雷达不显示"）。0 = 关闭。
NODE_LIVE_SWITCH_STALE_SEC=20
# 实时视图每个 tick 最多推进多少帧（默认 4000）。调小可以让单次卡顿变短。
NODE_ER_TICK_MAX_FRAMES=600
# 线路B observe 队列上限（条）。老版本代码硬编码 50000，不认这个键；
# 认的版本会读它。不认也无害，所以先写上。
# ★ 合并期核对：config.py 默认 200000、observe_worker.env_queue_max() 下限 256。
NODE_OBSERVE_QUEUE_MAX=100000
# ★ 合并期新增：单账号在 observe 队列里的条目上限（条）。<=0 = 不限。
#   治「一个账号洪泛吃满整池队列、把别人连带挤掉」。config.py 默认 3000。
NODE_QUEUE_ACCOUNT_QUOTA=20000

# --- bank 绑定节流（★ 合并期新增：这三个键 fix1 的 bank.py 真正在消费）---
# 同一个流两次试绑的最小间隔（秒）；bank.py 默认 2.0。太小吃 CPU，太大绑定慢。
NODE_BANK_BIND_RETRY_SEC=2.0
# 单次 add_bank 里花在「试绑」上的时间预算（毫秒）；bank.py 默认 5。<=0 不限。
# ★ 2026-10-17 修（__FIX_BIND_GATES__）：5ms 连几条碎流都扫不完，扫到对局主流量时
#   预算早烧光 -> 永远绑不上 -> 雷达空白。改成 50（与改造前一致）。退回值 = 5。
NODE_BANK_BIND_BUDGET_MS=50
# 绑定失败后的冷却期（秒）；bank.py 默认 60。<=0 = 不冷却。
# ★ 2026-10-17 修（__FIX_BIND_GATES__）：一次判失败就把那唯一一把 key 冻 60 秒，
#   期间 _cool_hit() 让**所有流**都跳过它 -> 彻底绑不上。改成 0（老版没有冻结）。
NODE_BANK_COOLDOWN_SEC=0
# 单个账号可挂起的 bank 候选数；bank.py 默认 16。
NODE_BANK_MAX_PER_ACCOUNT=16

# ── 绑定闸门（__FIX_BIND_GATES__，测试机 36.212.206.246 已验证）────────────
# 背景：合并且分线后新加的「2 秒节流 + 5ms 预算 + 失败后 60 秒冻结」把绑定机会
#       掐掉了，导致密钥到手也绑不上 -> 雷达空白。下面三项恢复老版口径。
# ★ 上面第 135-141 行的 NODE_BANK_BIND_BUDGET_MS / NODE_BANK_COOLDOWN_SEC 已就地改好，
#   这里只列**新增**的三个键（配置加载器按"先出现的先生效"，所以同一个键别写两遍）。
# 挑候选时按「待处理包数」降序（1=开）；0 = 退回按 last_seen 挑选。
# 退回值 0（改成 0 = 退回按 last_seen 挑选）
NODE_BANK_PREFER_PENDING=1
# 节流口径：legacy = 老版（只对「pending 打满 / 反复失败」限速，正常流每包都试）
#           current = 无条件 2 秒节流（合并期新加的，实测会掐掉绑定机会）
# 退回值 current（无条件 2 秒节流，实测会掐掉绑定机会）
NODE_BANK_THROTTLE_MODE=legacy
# 高 pending 流的「快车道」门槛（条）；>=该值的流免除外层共享预算，保证被检查。
# 实测对局主流量 pending 会打到 768，而它是唯一能绑上的流，不能被挤出。0=关闭。
# 退回值 0（0 = 关闭这条快车道）
NODE_BANK_PRIO_MIN_PENDING=256
# 每个流两次试绑之间的最小间隔（毫秒）；bank.py 默认 150，0=关闭这条保底节流。
# ★ 为什么必须有：实测单次 _try_bind ≈ 12ms 且是**固定开销**（与 pending 长度、
#   候选 key 数无关）。老版口径"正常流每包都试"在流多时会吃满 CPU：
#   6 条流 × 每秒 5 次 ≈ 32% 单核；20 条流 × 每秒 2 次 ≈ 43% 单核。
#   150ms 对应每流每秒最多约 6.7 次，绑定响应仍在 0.15 秒内。
# 退回值 0（0 = 关闭这条保底节流）
NODE_BANK_MIN_TRY_GAP_MS=150

# ── 运维页开关不在本文件，在 panel/.env 的 OPS_ENABLED ────────────────────
# 忘了写会让后台「运维」页全部 403（看起来像"模块没启动"）。

# __ENVCOMMENTFIX__ 不要在 .env 里写行尾注释（KEY=值 后面跟 # 注释）。
#   加载器虽然已经能剥掉，但手写时容易踩；事故记录见 node/config.py 的 _strip_env_comment。

# --- 实时性保护：超过期限停止接收新账号，在线账号不主动踢出 ---
NODE_REALTIME_MAX_QUEUE_AGE_MS=250
NODE_REALTIME_MAX_SNAPSHOT_AGE_MS=500
NODE_REALTIME_TRIP_AFTER=3
NODE_REALTIME_RECOVER_AFTER=10
# 一个账号静默多久算「已经不在统计范围内」。不设这个，账号一下线，
# 它的快照年龄就永远增长，闸门 trip 之后再也 recover 不了，节点会永久拒绝新账号。
NODE_REALTIME_MAX_IDLE_MS=15000
# 读不到线路B 的实时状态多久之后，开始拒绝新账号（秒）。
# ★ 旧逻辑：读不到就当「没过载」放行 —— B 挂了的时候最该挡人，它反倒全放进来，
#   结果一屋子人陪你看黑屏，日志一个字不写。现在超过这个时长就挡新号并记
#   realtime_blind（已在线账号完全不受影响，只是雷达要等 B 回来）。
#   5~7 秒是故意的：B 重启一般几秒就回来，短抖不误伤；真死了才挡。
NODE_GATE_BLIND_SEC=6
# 客户端「半关闭」（只关写、还在等响应）之后，最多再等上游回多久（秒）。
# ★ HTTP/1.0 的 launcher 就这德行。半关闭时如果立刻收掉下行，响应永远不回，
#   客户端表现是「卡在检查扩展资源 0%」并反复重连同一个目标。
NODE_TCP_HALF_CLOSE_WAIT=300
NODE_PARSE_QUEUE_MAX=12000

# --- 安卓 APP 下载地址（网关向导页二维码用）---
# 不填就退回 gateway.py 里的默认值（旧服务器 103.231.58.199），新机器必须改成自己面板。
# 一键部署.sh 会按 NODE_PUBLIC_HOST 自动写这一行。
NODE_ANDROID_APK_URL=
NODE_BANK_MAX_PENDING_PER_ACCOUNT=60000
NODE_BANK_PENDING_ACCOUNT_TRIM_HIGH=54000
NODE_BANK_PENDING_ACCOUNT_TRIM_LOW=42000
NODE_BANK_PENDING_TRIM_HIGH=108000
NODE_BANK_PENDING_TRIM_LOW=84000
