小地圖定位

返回:文件索引 / 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}# 小地圖定位

返回:文件索引 / 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 份。

清理只動名字像時間戳的目錄()。認不出的一律不碰:這個函式會刪東西,寧可漏刪 也不能誤刪使用者放在那兒的東西。時間戳定寬,所以字典序就是時間序。

使用者自帶的後端不加 --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日