小地图定位

返回:文档索引 / API 参考 / 开发指南 / 网格导航

本文只说明实时定位链路。路径规划、路线跟随和游戏输入见网格导航。

1. 职责边界

定位由应用级 WorldPoseService 维护;服务在首次被消费者读取时懒创建, 不会注册成常驻 TriggerTask:

  • 接收官方地图 WS 或本地 WS 的绝对坐标;
  • 从小地图纹理估计高频相对位移;
  • 在静止时用 WS 坐标重锚;
  • 同时产出水平坐标、高度、朝向、可信状态和诊断字段;
  • 通过 RuntimeStateHub 发布 world.pose。

物品导航、网格导航、滑索和其他任务只读取 world.pose,不直接启动 WS、里程计或融合器。

2. 模块

模块 职责 不负责
src/localization/position_service.py 应用级定位服务;维护位置源并发布状态 路径规划、游戏输入
src/localization/minimap_position_mixin.py 编排里程计、融合、位置源和可信状态 A* 与动作执行
src/localization/minimap_odometry.py 从小地图纹理估计相对位移 WS 校准、绝对坐标
src/localization/minimap_position_fusion.py 用静止 WS 坐标重锚并融合位移 网格通行判断
src/localization/ws_position_mixin.py 官方地图 WS、本地 WS 和认证协议 路径与业务判断
src/tasks/mixin/runtime_state_mixin.py 向业务任务提供统一坐标读取接口 定位算法
src/runtime_state/ 保存 latest-value 状态快照与主题契约 采样和融合

3. 数据流

flowchart TD
    W[官方地图 WS / 本地 WS] --> FUS[绝对坐标]
    F[游戏帧] --> O[小地图里程计]
    O --> FUS
    FUS -->|静止时重锚| FUS
    FUS --> PUB[WorldPoseService 发布]
    PUB --> HUB[RuntimeStateHub: world.pose]
    HUB --> ITEM[物品导航]
    HUB --> NAV[网格导航]
    HUB --> ZIP[滑索状态判断]

ok-script 执行器是单线程协作调度,定位服务不是 TriggerTask,也不会在后台固定频率取帧。 消费者通过 RuntimeStateMixin.world_pose(frame=...) 请求 WorldPoseService 按需采样一帧并 发布快照;没有消费者时不会做 0.2 秒一轮的小地图视觉采样。

4. 坐标系

4.1 世界系

  • 水平位置统一为 (x, z),单位为米;
  • 朝向使用罗盘方位角:正北 0°、正东 90°、正南 180°、正西 270°;
  • 世界 +x 为东,世界 +z 为北。

4.2 地图系

  • 图像坐标使用 OpenCV 约定:x 向右、y 向下;
  • 小地图纹理移动方向与玩家移动方向相反;
  • 相位相关得到内容位移 (dx, dy) 后,玩家地图系位移为 (-dx, -dy);
  • 轴映射负责把地图系像素转换为世界米:
world_delta = map_to_world_px @ map_delta_px

5. 位置源

5.1 官方地图 WS

全局 Nav Config 的「真值content」或「真值地图账号」提供登录凭证。客户端执行:

content -> HG OAuth -> 地图 cred/token -> 角色绑定 -> WebSocket token
        -> wss://ws.skland.com/ws/v1/game/endfield/map

位置消息来源于 type=1012,解析 mapId 和 pos.x/y/z。

5.2 本地 WS

没有官方真值时,定位服务监听 ws://127.0.0.1:3001。油猴脚本拦截官方地图页的 WebSocket,并把原始 JSON 转发给本地端口。该模式也需要保持网页地图和位置同步开启。

5.3 抓包直通(实验技术)

另一条路完全绕开上面两条:游戏主 TCP 连接里周期性上报主控角色的世界坐标, 一个独立的外部程序被动抓包并离线解密后,通过本地 HTTP 暴露同一个世界坐标系的坐标 ——与官方地图 WS 逐位一致,并且额外自带真实高度、朝向与速度。

endfield-capture-backend.exe serve --no-dashboard --no-map-ws --port 8765
        ▲                                    │
        └──── GET /api/state(轮询)──────────┘
模块 职责
src/localization/capture_position_source.py 只读后端,翻成定位契约。不含任何抓包/解密代码,因此应用侧不需要新增依赖
src/localization/capture_backend_installer.py 从 GitHub Release 下载 exe 并检查更新;校验 sha256、原子落盘、失败不阻断
src/localization/capture_backend_launcher.py 按需拉起后端 exe,并做探活、冷却重试与整棵进程树的收尾
src/patches/capture_backend_autostart_patch.py 在游戏启动之前触发上面那个拉起器

后端程序的获取与更新

exe 不再需要用户手工放置:打开「使用实验技术」后由本应用从 luqing233/endfield-capture-backend 的 Release 下载,并在有新版时静默更新。

时机 做什么
勾上风险开关(确认后) 后台预下载,不阻塞界面。那时用户还没启动游戏,正好用这段时间
点「启动游戏」前 阻塞补齐(缺了就下载/更新)。宁可启动多等十几秒,也不能让游戏先连上
点「检查并更新后端」 手动触发一次,忽略节流

安装位置是 <应用目录>/downloads/capture_backend/。不能放 configs/:那里的东西会被配置 导出整个打包,而导入侧对单个成员硬性限制 16 MB(config_transfer_patch._MAX_IMPORT_MEMBER_BYTES), 18 MB 的 exe 一旦进去,导出的配置包就再也导不回来。downloads/ 已被 .gitignore 覆盖。

取版本有两条路,主用 GitHub REST API(额外给出官方 sha256 摘要,可校验完整性);实测未认证 访问会按 IP 限流(撞到过 403),所以拿不到就退回不加 API 的 /releases/latest 302 重定向拿 tag ——这条路不消耗配额,代价是没有官方摘要,只校验文件大小并告警一次。

下载下到哪儿都不会阻断游戏:拿不到新版就用手上这个旧的,一个都没有就报「后端不可用」。 下载先落到 .download 暂存文件,校验通过才 os.replace 成正式 exe——不存在"半截 exe 被拉起" 的窗口。

指定了「实验技术后端程序路径」或环境变量 EF_CAPTURE_BACKEND 时完全跳过联网:用户自带后端 就不该再去下载。同理「自动启动后端」关掉时也不下载。

运行产物的保留策略

后端每局建一个 runs/<YYYYMMDD-HHMMSS>/,里面是 capture.pcap + positions.jsonl (+ service.json),一局约 20 MB,不设上限能攒到几个 GB。

我们拉起自己下载的那份时显式传 --runs-dir 指到 <托管目录>/runs,并在每次拉起探活成功 之后按「只留最近 5 局」清理一次。顺序很关键:后端是先建目录、再起 HTTP 服务的,所以端口应答 时新目录必然已存在——放在 _launch 之后立刻清会早于它建目录,稳态会留成 6 份而不是 5 份。

清理只动名字像时间戳的目录(^\d{8}-\d{6}$)。认不出的一律不碰:这个函数会删东西,宁可漏删 也不能误删用户放在那儿的东西。时间戳定宽,所以字典序就是时间序。

用户自带的后端不加 --runs-dir——那会把它自己的产物挪到我们的目录里,属越界。

顺序是成败关键:后端必须早于游戏的主连接就绪,否则拿不到登录握手,后端会明确报 「缺少连接起点」而不是解出错数据。

拉起的时机不能放在普通任务里——任务要等游戏窗口能截屏才会被执行(can_capture()), 那时登录握手早就过去了。所以补丁包装了 StartController.do_start,那是游戏进程存在之前的 最后时机;WorldPoseService 的按需采样会再兜底一次(后端中途挂了、或玩家在游戏已经跑起来 之后才打开开关)。

直通模式下整条校准体系都不参与(坐标本来就是绝对值,没有锚点可设、没有漂移可修), 所以只要坐标可用 sync_needed 就恒为假——否则导航的 CALIBRATE 状态会停下来等一个永远 不会到来的静止校准。

会话边界:坐标是绑在一条 TCP 连接上的

「绝对」不等于「当前」。坐标由某一条 TCP 连接解出,而那条连接会随离开游戏、换账号、 重启游戏而结束。后端把最后一条坐标一直留着(它的 state.last 没有清空点),于是:

旧会话结束之后、新会话报出第一条坐标之前,/api/state 给的仍是上一局的坐标。

那可能是另一张地图、乃至另一个账号的位置。多账号日常任务正好撞在这条路上: BaseEfTask.iter_multi_account_context 对每个账号先调 login_flow,紧接着就跑送货等导航任务。 切号的实际动作是离开游戏再重新进入(login_flow 走 UI:退出到登录界面 → 选账号 → 进入), 而重新进入会建立新的 TCP 连接,所以每一轮切号都自带一次"旧连接结束 + 新连接就绪"的交替。

这条对实现是有利的:切号不会复用连接,因此后端侧那个"同四元组复用会让 flow 永久失效" 的隐患在真实切号路径上不会出现(见下文反面情形)。但它同时说明空窗期是真实存在且必然发生 的——游戏退到登录界面、选账号、再进入,这段时间里 lastPosition 一直是上一个账号的坐标。

所以直通模式的信任是两条一起判,任一不成立就不给导航用(_capture_trust):

判据 trust_reason 说明
产出这条坐标的连接已不再喂数据 capture_session_ended 连接的 flows[].status 是终态(连接已结束 / 解码停止:…),或它在 flows 里已查不到,或这条连接已 30 秒没收到任何报文(兜住终态没被捕捉到的情形)
认不出地图 capture_no_parent_map / capture_unknown_map 见 5.3 下方「不猜」一节

两条都只扣信任、不清空坐标:x/z 照给,让导航停在 WAIT_TRUST 时能把真正的原因 打出来,而不是含糊地报「未锚定」。网络侧信息不足时(老版本后端不报 flows)一律当活着的 处理——缺信息不等于会话已死,没有证据就不扣信任。

导航等待态的文案会带上 pose 里的 trust_reason / odom_reason(形如 等待坐标: capture_unavailable),所以一眼能分清是「融合链还没锚定」还是「抓包源没数据」—— 这两种情况该查的东西完全不同。

抓包模式不启动小地图定位链:start_minimap_position() 在本模式下直接返回, minimap_position_ready 也恒为真。两处短路缺一不可——导航入口 (navigate_grid_to → ensure_runtime_position_service)会无条件调前者、紧接着检查后者, 而抓包根本不需要比例尺/里程计/融合/地图 WS。不短路的话有两重代价:拿不到比例尺时导航会 立刻失败并报一条指向全局「导航配置」和游戏窗口的错(该查的其实是抓包后端);拿得到时也会 白建两个对象并启动地图 WS——正是本模式声明要绕开的那一整套。

sync_seq 在抓包源上的含义是"坐标基准变了",不是"坐标变了":换连接时它一定推进 (连接的 id 参与判定),所以"登出后在原地重新登录"这种坐标逐位相同的重登也会让导航失效 已走过的已知格;而同一连接内怎么移动都不推进。后者是必须的——导航把 sync_seq 变化当 "真重锚 → 世界坐标整体平移",抓包源移动时每拍坐标都在变,若按"采样变了"推进,移动本身就会 每拍清空导航的"走过的已知格",偏航回退(网格导航文档 §7.5)会退化成"没有已知格可返回"。

三类终态由两条判据分别接住,缺一不可——后端的 snapshot 会把状态计算覆盖成 等待 TCP 补包(any(gaps) and not flow.failed 时顶替真实状态),而收到 FIN 的连接并不是 failed,所以连接结束时若还有没补齐的 TCP 空洞,它的终态就被遮住了:

  • 解码停止:… 只可能出现在 failed 时 → 覆盖不生效 → 状态判据接住(立即);
  • 连接已结束 被空洞覆盖遮住时 → 此时 lastPacket 已停止更新 → 30 秒静默判据接住。

30 秒是个刻意偏松的值:误判为「还活着」只是让陈旧窗口变长,误判为「已死」却会把一条正常 连接的导航停掉;而活着的游戏连接不会这么久完全静默(移动中约 0.27 秒一条,静止时仍有其他 对象的报文)。代价是登出后最长 30 秒的陈旧窗口——但比改动前的「永久信任」已经是严格改善。

后端侧还有几条边界值得记着(核实的是 endfield-capture-backend 的 decoder.py / server.py):

  • 只抓 30000 / 40000 两个端口。重登若换到别的端口,后端看不见那条连接,"自动接管新连接" 也就不成立;
  • flows[] 只回最近 12 条,更早的连接查不到——这也是"查不到即判死"的依据;
  • --seed-pcap 不是重登录的解法:它只能把同一条仍在使用的连接的密钥流接上(所以 "上次 runs/ 里的 capture.pcap"可用)。游戏重启换了连接后,重放出来的是另一条已经死掉的 连接,反而会解出一批陈旧坐标;
  • 契约没有任何版本字段,也没有 reset / 重连 / 健康检查端点(/api/state 200 即探活, 应用侧 probe_backend 用的就是它);
  • 每次轮询都会全量返回 trajectory(最多 1200 点,且跨连接混在一起)。应用侧不消费它, 但会在 0.2 秒的轮询周期里反复传输与解析——真嫌这个开销,就换成后端的命名管道 (serve --pipe --no-http,事件驱动、不占端口,字段与 lastPosition 完全一致), 应用侧目前还没接。

会话交替是自愈的:离开游戏再进入会新建一条 TCP 连接

离开游戏再重新进入(含多账号切号)走新 TCP 连接。所以整条恢复路径是顺的,不需要任何额外处理:

离开游戏  → 旧连接收 FIN → 旧 flow 终态 → 应用扣掉信任(WAIT_TRUST),导航停车
重新进入  → 新连接 SYN → 后端按四元组新建 flow → 抓到新握手 → 报出新坐标
          → 新 flow 活着且地图认得出 → 信任恢复,导航接着走

关键在于后端不需要重启:它按 TCP 四元组归档连接,新连接自然是一条新 flow, 只要后端还在抓包,新一轮握手就会被捕获。这也是"默认不在停止定位时关闭后端"更有力的理由 ——后端的客户端材料(DLL 私钥、公钥模数)是进程级的,会话 key/nonce 才是每连接现解的, 所以它可以跨掉线一直用下去。同理,应用侧不需要在切号时做任何特殊处理(也不必去拉一次 后端):do_start 若被再次调用,补丁只会探到后端已在跑并直接采用。

中间那段空窗期就是上面那条 capture_session_ended 生效的窗口:旧 flow 已经结束、新 flow 还没报出坐标,此时 lastPosition 仍是上一局的。窗口长度就是离开游戏到重新进入的耗时。

反面情形(后端侧尚未处理的边界,实际不会碰到):若游戏复用同一条四元组发起新会话, 后端的 Reassembler.feed 只在基线为空时才接受 SYN,新明文握手会被当成加密帧、整条 flow 永久失效(重开游戏才能恢复,重启后端也没用)。这种情况由 30 秒静默判据接住。 真实的切号与重登都走新连接,所以这条不构成风险——但如果是用 kill_game 那种 TerminateProcess 强杀游戏(危险状态守卫、任务完成后勾选的「仅退出游戏」走的就是它), 对端来不及发 FIN,旧 flow 会一直停在「正在解码」、lastPacket 也停更,这时只剩 30 秒 静默判据在兜底,窗口内旧坐标仍会被当成可信。

绝不要在游戏运行期间重启后端

比上面那条更硬的一条约束:会话密钥由服务端在登录响应里下发,每个账号、每次登录都不同 (后端用客户端内嵌的 RSA 私钥从登录响应里解出 32 字节 key + 12 字节 nonce)。所以 "重新登录"在协议上就是一次全新的握手,密钥只在那一刻出现过一次。

推论:重启后端不能解决任何解码问题,只会制造问题——它把一个本来会被自动接管的局面 (新连接新握手)变成必须重开游戏才能恢复(新后端看到的是一条没有 SYN 的连接,只会报 缺少连接起点)。同理,「停止定位时关闭后端」打开后如果应用在游戏还开着时退出,下一次 开启应用时后端虽然会被重新拉起,但当次登录已经救不回来了,只能重开游戏。

应用侧对这条约束的复查(结论:满足):

  • stop_capture_backend() 的生产调用点是 WorldPoseService.on_destroy();
  • 服务销毁由包装后的 TaskExecutor.destroy() 触发,而它来自执行器循环结束或 MainWindow.closeEvent,即应用退出,不会因为任务禁用/重新加载/切号而触发;
  • 真正杀进程还要 实验技术停止定位时关闭后端 为真,默认为假——所以默认连应用退出都不会 动后端(这也正是它默认为假的原因);
  • 反之,ensure_capture_backend() 每拍都会走一遍,但它只会拉起、从不重启已在跑的后端 (BackendLauncher.running 挡住重复启动,健康期 5 秒内连探活都省掉)。

唯一残留的坑是人为的:若用户打开了「停止定位时关闭后端」,又在游戏还开着时退出应用, 就必须重开游戏才能恢复定位。这条已写在那个配置项的说明里。

校验抓包是否真的接上了,没接上就重建连接

坐标缺失有好几种成因,混在一起就没法决定该做什么——所以 CapturePositionSource.health() 把连接状态单独分类出来(CaptureHealth),只看连接、不看坐标:

状态 含义 该做什么
ok / decoding 有坐标 / 握手已成、坐标在路上 正常
no_connection 后端在跑,但一条游戏连接都没看到 重建连接
handshake_pending 连接已发现,握手还没完成 等
backend_down 后端不可达 查后端本身
missed_handshake 抓包晚于该连接建立 重建连接

判定口径是"后端在跑、但当前没有坐标且不会自己恢复":no_connection(这一局的位置源已经 没了)与 missed_handshake(后端没看到 SYN,推不出密钥流)都属于这一类,换一条连接让它从头抓 握手就行。handshake_pending(握手正在进行)再等一会就好,backend_down(后端根本没起来) 重建也没用——对它们动手只会白白打断玩家。连接已结束(人家正常结束)与 解码停止(这条流真 出问题了)也不值得重建。

「一条死连接 + 一条活连接」必须判成正常:后端把历史连接一直留着,拿旧的死连接判断会让 每一次正常抓包都被误判成需要重建。

重建动作是回登录界面再进入(force_new_capture_connection),不切换账号——用户口径是 "这个流程不需要重新选择账号",所以不走会点「最近」、重选账号的 login_flow。实际动作委托给 LoginMixin.relogin_same_account:回到主界面 → 点 main_out 登出入口 → 通用确认 → 回到登录 界面(这时能匹配到 fL.logout,游戏显示"点击任意位置继续")→ 点一下屏幕中央继续进入。拿不到 可用的重进实现、或没回到登录界面,都干净失败并提示手动进入一次,绝不乱点。

登录界面的「最近 / 登录」按钮仍只能走 click_text(login_ocr → login_screenshot,从屏幕 截窗口;普通帧通道在那种界面上取不到有效画面)——那条路是 login_flow 切号用的,本重进流程 不用它。为此 WorldPoseService 混入了 LoginMixin(它没有 __init__/default_config, 不会带进「多账户模式」那套配置),重进直接用它现成的 relogin_same_account。

两次重建之间至少隔 CAPTURE_RELOGIN_INTERVAL_SECONDS(20 秒)。ensure_capture_session 在 "没坐标"时每拍都会被调,没有这个间隔三次尝试会在一分钟内打光——实测两次只隔了 14 秒,而每次 都要把玩家从游戏里踢回登录界面。

三个时机都会校验(ensure_capture_session):启用后第一拍、每次导航前、每次切号后。 最后一条值得说明:切号本身会重建连接、后端会自动接管(登录密钥随之重抓),不需要为它做特殊 处理——但"应该会发生"和"确实发生了"是两回事,所以还是要校验。

最多尝试 3 次,成功即清零(重登后能正常抓包,下次再出问题理应还能修);用尽就停手并 报错——无人值守时反复登出比不修更糟。可用「抓包失败时自动重新登录」关掉。

首次使用建议盯一眼:重建动作依赖游戏界面的特征模板(登出入口 main_out、登录界面的 fL.logout)。版本不符时匹配不到,这一步会干净失败并提示"请手动退回登录界面再进入一次", 而不会乱点。这种情况把它关掉、手动退一次即可。

map_id 参与导航的换图判定,而导航侧算的是 str(pose.get("map_id") or session.request.map_id or "")——会静默回退到调用方请求的地图。 所以「认不出地图」必须在定位源里就扣掉信任,不能只靠导航自己判断:实测场景 463 是自带独立 坐标空间的可走区域(那 11 秒里角色以 5.0 的速度正常行走,坐标在 (16, 107, -41) 附近, 而 map02 的 y≈320、相距约 1400 个世界单位),拿它当 map02 会在错的网格上、从错的起点规划 路线。这条与 _resolve_map_id 的「不猜」是同一条原则,只是贯彻到了 pose 的信任位上。

上报是事件驱动的:移动中约每 0.27 秒一条,完全静止时没有位置包,同图传送会有数秒到 数十秒的空档。CaptureSample.age_seconds(后端 Unix 墙钟减去当前墙钟)因此只显示不拦截 ——静止时年龄大是正常的。它不进信任判定,它的用处是出现「坐标跳变」或「导航拿着旧位置走」时 一眼看出数据源当时到底有没有在更新。后端跑在本机、需要 Npcap 驱动与客户端文件,属于用户自行 选择开启的功能(开关带风险确认,见 src/patches/danger_switch_patch.py)。

6. 融合策略

WS 坐标绝对但有延迟,小地图里程计高频但会漂移。融合规则是:

静止时:P0 = WS(x, z),并清零里程计
移动时:P = P0 + map_to_world @ odometry_delta
再次静止:用新的 WS 坐标重新校准

静止判定要求:

  • 小地图里程计低速;
  • 已经收到至少两次 WS 样本时,WS 相邻样本的位移也必须足够小。

移动途中收到的 WS 样本不会暂存,也不会在以后补用,否则会把已推进的位置拉回。

6.1 关键帧视觉锚定链校正(导航位置修正)

静止重锚只能把漂移限制在两个静止点之间;移动距离越长,累计错位越大。实时链路额外维护 一个关键帧视觉锚定链校正器,只输出位置,不落盘、不截图、不绘图:

  • 每拍把当前小地图圆环加入局部滑动窗口;
  • 新切片和最近若干拍建立相位相关相对平移边;
  • 静止校准的 WS 坐标作为高权重绝对锚点,并向后续关键帧传播;
  • 关键帧之间继续建立视觉重叠边,使 WS 绝对基准沿 A -> B -> C -> ... 传递;
  • 用 Huber IRLS + PCG 求解局部窗口和关键帧锚定链,把最新位置发布为 world.pose.x/z。

原始融合坐标保留在 raw_x/raw_z,关键帧视觉锚定链校正量、边数、锚点数、残差和原因记录在 visual_anchor_chain_* 字段。校正失败或超过安全上限时回退原始融合值。

离线的 MinimapPositionRecorder 和 stitch_records() 只用于事后诊断与参考图, 不进入导航实时链路。

7. 配置

定位的标定与真值参数集中在全局 Nav Config:

  • 真值content / 真值地图账号:绝对坐标来源;
  • 比例尺常数(米):非标准档位的兜底比例尺;
  • 1K/2K/4K比例尺(米/像素):对应 1920/2560/3840 宽;
  • 1K/2K/4K轴映射(逗号4值):地图系像素到世界米的矩阵;

运行时参数保存在全局 Position Config:

  • WS等待稳定秒数 / WS稳定最小位置数;
  • 航点校准最小距离(米);
  • 位移提交阈值(像素)。
  • 关键帧视觉锚定链校正:是否启用实时位置修正;
  • 关键帧视觉锚定链校正窗口(拍) / 关键帧视觉锚定链校正边间隔(拍) / 关键帧视觉锚定链校正最小响应;
  • 关键帧视觉锚定链校正锚点权重 / 关键帧视觉锚定链校正修正上限(米);
  • 关键帧视觉锚定链校正关键帧间隔(拍) / 关键帧视觉锚定链校正关键帧边数 / 关键帧视觉锚定链校正关键帧最小响应 / 关键帧视觉锚定链校正关键帧锚点权重。

实验技术(抓包直通,见 5.3)另有一组,默认关闭;打开风险开关后才出现:

  • 使用实验技术:总开关,带风险确认;
  • 实验技术后端端口:默认 8765,探活与拉起都用它;
  • 实验技术后端程序路径:留空则自动探测(托管下载目录、同级 endfield-capture-backend/dist/、环境变量 EF_CAPTURE_BACKEND、PATH 等)。填了就完全 不联网——用户自带后端;
  • 实验技术自动启动后端:默认开;
  • 实验技术停止定位时关闭后端:默认关。后端状态绑在当前这一局连接上,关掉再拉起等于 丢了登录握手,必须重启游戏才能恢复;
  • 检查并更新后端:按钮,立刻查一次更新(见 5.3「后端程序的获取与更新」)。

分辨率、比例尺或轴映射变化时,定位器会重建。真值来源变化时,绝对锚点会清空并等待重新校准。

8. world.pose 契约

主要字段:

字段 含义
x / y / z 世界坐标;y 为最新 WS 高度
heading / heading_score 罗盘方位角和置信度
map_id 当前地图
position_trusted / trust_reason 绝对坐标是否可信
anchor_set / rest 是否已锚定、当前是否静止
odom_ok / odom_reason 是否有有效位移样本
sync_seq / sync_checked / sync_needed 坐标基准序号(融合源上即静止校准序号)与校准状态
sync_supported 这个数据源上「静止校准」这个概念是否成立。默认 True;抓包直通这类绝对坐标源给 False,导航据此不断言要等校准。刻意做成契约字段,好让导航依赖 pose 的接口而不是某个定位实现的名字(见 §5.3)
sync_residual 校准前原始融合误差,以及上一拍关键帧视觉锚定链校正输出误差
ws / ws_xyz 最新 WS 水平坐标和完整坐标
sample_t / source 采样时间和数据源
raw_x / raw_z 关键帧视觉锚定链校正前的原始融合坐标
visual_anchor_chain_x / visual_anchor_chain_z 校正后的坐标(仅有效时)
visual_anchor_chain_correction_px / visual_anchor_chain_correction_m 本拍校正量
visual_anchor_chain_nodes / visual_anchor_chain_edges / visual_anchor_chain_anchors 局部窗口节点、边和 WS 锚点数
visual_anchor_chain_min_response / visual_anchor_chain_median_response 新边的相关响应(最小值 / 中位数)
visual_anchor_chain_residual_median / visual_anchor_chain_residual_max 校正后的边残差
visual_anchor_chain_reason 局部校正结果(ok / no_edges / correction_too_large 等)
visual_anchor_chain_keyframe_reason 关键帧视觉锚定链校正的关键帧更新结果

消费者必须检查 position_trusted。快照超过 max_age 后返回 None,不能继续拿旧坐标执行动作。

9. 调试与测试

定位相关调试任务位于 src/tasks/localization/:

  • MinimapRealtimePosition:逐拍查看融合坐标、朝向、速度、WS 和里程计位移;
  • MinimapPositionRecorder:逐拍落盘融合状态、原始里程计位移、校准残差和小地图切片, 结束后生成 fused / odom 两种像素对齐拼接图;
  • MinimapRegionCheck:检查当前分辨率下的小地图环带区域;
  • MinimapScaleCapture:采集静止截图与 WS 坐标,用于比例尺标定;
  • MinimapTurnToHeading:验证朝向读取和转向闭环。

采样记录的产物在 logs/minimap_position_recorder/<时间戳>/:samples.jsonl 是逐拍 数据,calibrations.jsonl 是每次静止校准的残差,patches/ 是 BGRA 圆环切片, stitch_*_fused*.png / stitch_*_odom*.png 分别是按融合坐标和连续里程计轨迹拼接的 参考图。需要换拼接参数时用:

uv run --locked python scripts/data-capture/stitch_minimap_recorder.py `
  logs/minimap_position_recorder/<时间戳>

常用测试:

uv run --locked python -m unittest tests.TestMinimapOdometry tests.TestMinimapPositionFusion tests.TestMinimapPositionMixin -v
uv run --locked python -m unittest tests.TestRuntimeStateHub tests.TestRuntimeStateMixin -v
uv run --locked python -m unittest tests.TestCapturePositionSource tests.TestCaptureBackendLauncher tests.TestCaptureBackendInstaller -v

定位异常排查顺序:

  1. world.pose 是否存在且未过期;
  2. anchor_set 和 position_trusted 是否为真;
  3. odom_reason 是否长期不是 ok 或 shift_too_small;
  4. 静止校准后的 error 是否接近零;
  5. 比例尺、轴映射和 map_id 是否匹配当前分辨率与地图。

开了实验技术时改看这几条:

  1. trust_reason 是不是 capture_unavailable——是就说明后端没数据,先看后端本身;
  2. 后端日志 logs/capture_backend/backend-<端口>.log:缺 Npcap 驱动、客户端目录不对、 「缺少连接起点」(抓包晚于登录握手,需重开游戏或用后端的 --seed-pcap)都会写在那里;
  3. exe 没下下来:看日志里那句「拿不到抓包后端的发布信息」(网络不通或 GitHub 接口被限流) 或「下载抓包后端 vX 失败」。装到哪儿了、装的是哪个版本看 downloads/capture_backend/installed.json;点「检查并更新后端」可立刻重试;
  4. capture_age 是不是一直很大——上报是事件驱动的,静止时没有新包属正常,不拦截;
  5. trust_reason 是 capture_session_ended——坐标还在,但它来自已经结束的一条连接 (离开游戏、换账号、重启游戏)。等新连接报出第一条坐标即可自愈;一直不变就说明新连接没解出来, 回去看第 2 条;
  6. capture_flow 是不是变了——切号会换一格,这是判断「换了一局」最直接的现场信息;
  7. capture_map_id_source 是 no_parent_map 或 capture_unknown_map——该场景没有可用的导航 网格,导航会明确停车(前者是正确结果:试炼场这类场景本就不属于任何大地图,不是故障)。

日志里那句「还没有解出角色坐标」会自动分成两种,因为它们的处理方式正好相反:

  • 一般情形 → 提示确认后端早于本局连接启动;
  • flows[] 里有一条停在「缺少连接起点」→ 直接提示重开游戏,并说明重启后端没有用 (它看到的仍是一条没有 SYN 的连接)。这条的典型成因就是后端被重启过——而后端绝不该在 游戏运行期间被重启,见上文。
在 GitHub 查看源文件 ↗ · 页面生成时间: 2026年10月23日