小地圖定位
返回:文件索引 / 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/state200 即探活, 應用側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
定位異常排查順序:
world.pose是否存在且未過期;anchor_set和position_trusted是否為真;odom_reason是否長期不是ok或shift_too_small;- 靜止校準後的
error是否接近零; - 比例尺、軸對映和
map_id是否匹配當前解析度與地圖。
開了實驗技術時改看這幾條:
trust_reason是不是capture_unavailable——是就說明後端沒資料,先看後端本身;- 後端日誌
logs/capture_backend/backend-<端口>.log:缺 Npcap 驅動、客戶端目錄不對、 「缺少連線起點」(抓包晚於登入握手,需重開遊戲或用後端的--seed-pcap)都會寫在那裡; - exe 沒下下來:看日誌裡那句「拿不到抓包後端的釋出資訊」(網路不通或 GitHub 介面被限流)
或「下載抓包後端 vX 失敗」。裝到哪兒了、裝的是哪個版本看
downloads/capture_backend/installed.json;點「檢查並更新後端」可立刻重試; capture_age是不是一直很大——上報是事件驅動的,靜止時沒有新包屬正常,不攔截;trust_reason是capture_session_ended——座標還在,但它來自已經結束的一條連線 (離開遊戲、換賬號、重啟遊戲)。等新連線報出第一條座標即可自愈;一直不變就說明新連線沒解出來, 回去看第 2 條;capture_flow是不是變了——切號會換一格,這是判斷「換了一局」最直接的現場資訊;capture_map_id_source是no_parent_map或capture_unknown_map——該場景沒有可用的導航 網格,導航會明確停車(前者是正確結果:試煉場這類場景本就不屬於任何大地圖,不是故障)。
日誌裡那句「還沒有解出角色座標」會自動分成兩種,因為它們的處理方式正好相反:
- 一般情形 → 提示確認後端早於本局連線啟動;
flows[]裡有一條停在「缺少連線起點」→ 直接提示重開遊戲,並說明重啟後端沒有用 (它看到的仍是一條沒有 SYN 的連線)。這條的典型成因就是後端被重啟過——而後端絕不該在 遊戲執行期間被重啟,見上文。