Persist full GSI payloads to per-match JSONL for later analysis.
Default-on dump keeps every POST body under samples/raw/<matchid>/gsi.jsonl, with CLI overrides to disable when disk use is a concern. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -30,7 +30,7 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
|
|||||||
|
|
||||||
运行时产物(**勿提交**,见 `.gitignore`):
|
运行时产物(**勿提交**,见 `.gitignore`):
|
||||||
|
|
||||||
- `samples/raw/<matchid>/` — GSI 会话截图;手动 `capture.py` 可写在 `raw/` 根下
|
- `samples/raw/<matchid>/` — GSI 会话截图与 `gsi.jsonl`;手动 `capture.py` 可写在 `raw/` 根下
|
||||||
- `preview/` — 标定 / sheet 预览
|
- `preview/` — 标定 / sheet 预览
|
||||||
- `results/` — 每局 JSON
|
- `results/` — 每局 JSON
|
||||||
- `failures/` — `--truth` 调试用错识裁切
|
- `failures/` — `--truth` 调试用错识裁切
|
||||||
@@ -39,6 +39,7 @@ Windows 上的 Dota 2 **天梯选将识别**工具:用 GSI 感知选将阶段
|
|||||||
|
|
||||||
```
|
```
|
||||||
Dota 2 GSI → gsi_watch.py (:3223)
|
Dota 2 GSI → gsi_watch.py (:3223)
|
||||||
|
→ 全量 payload → samples/raw/<matchid>/gsi.jsonl(可关)
|
||||||
→ DraftSession 轮询截屏 → samples/raw/<matchid>/
|
→ DraftSession 轮询截屏 → samples/raw/<matchid>/
|
||||||
→ recognize_image (CDN 模板 + 可选天梯遮罩)
|
→ recognize_image (CDN 模板 + 可选天梯遮罩)
|
||||||
→ roles / grid / modes(辅助)
|
→ roles / grid / modes(辅助)
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- GSI 全量落盘:默认把每包 POST 追加到 `samples/raw/<matchid>/gsi.jsonl`
|
||||||
|
(`gsi.dump_payloads` / `--dump-gsi` / `--no-dump-gsi`)。
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- 项目定名:**上分帝** / **Climperor**(目录 `climperor`),由 `dota2-draft-vision` 迁出独立仓库。
|
- 项目定名:**上分帝** / **Climperor**(目录 `climperor`),由 `dota2-draft-vision` 迁出独立仓库。
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ Valve 已明确关闭普通玩家视角的实时 draft
|
|||||||
GSI 会话写入 `samples/raw/{matchid}/`;手动 `capture.py` 仍写 `samples/raw/` 根目录。
|
GSI 会话写入 `samples/raw/{matchid}/`;手动 `capture.py` 仍写 `samples/raw/` 根目录。
|
||||||
结果 JSON 仍为 `results/draft_<时间戳>.json`(内含 `match_id`)。
|
结果 JSON 仍为 `results/draft_<时间戳>.json`(内含 `match_id`)。
|
||||||
|
|
||||||
|
**⑥ GSI 全量落盘**
|
||||||
|
|
||||||
|
`gsi.dump_payloads`(默认开)把每包 POST body 追加到 `samples/raw/{matchid}/gsi.jsonl`,
|
||||||
|
行格式 `{"t": <unix>, "payload": <原文>}`。内容上限仍是 cfg 订阅字段 + 普通玩家视角;
|
||||||
|
不能靠落盘补出双方 pick。CLI:`--dump-gsi` / `--no-dump-gsi`。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. 关键实测(2026-07-25)
|
## 3. 关键实测(2026-07-25)
|
||||||
@@ -174,6 +180,7 @@ climperor/
|
|||||||
| `gsi.revise_gain` | 选人阶段改判所需分数增益 | 0.15 |
|
| `gsi.revise_gain` | 选人阶段改判所需分数增益 | 0.15 |
|
||||||
| `gsi.strategy_tail_polls` | 决策阶段继续视觉轮询 | 8 |
|
| `gsi.strategy_tail_polls` | 决策阶段继续视觉轮询 | 8 |
|
||||||
| `gsi.strategy_gsi_wait` | 视觉结束后等待本人 GSI 英雄 | 3.0 |
|
| `gsi.strategy_gsi_wait` | 视觉结束后等待本人 GSI 英雄 | 3.0 |
|
||||||
|
| `gsi.dump_payloads` | 全量 GSI JSONL 落盘 | true |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -182,6 +189,7 @@ climperor/
|
|||||||
```
|
```
|
||||||
Dota 2 (-gamestateintegration)
|
Dota 2 (-gamestateintegration)
|
||||||
→ POST → gsi_watch.py :3223
|
→ POST → gsi_watch.py :3223
|
||||||
|
→ samples/raw/{matchid}/gsi.jsonl(可选)
|
||||||
→ DraftSession 轮询 recognize + roles/grid/modes
|
→ DraftSession 轮询 recognize + roles/grid/modes
|
||||||
→ results/draft_*.json
|
→ results/draft_*.json
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ python gsi_watch.py
|
|||||||
```
|
```
|
||||||
|
|
||||||
进入英雄选择后自动跟踪;凑齐 10 人或超时后写出 `results/draft_<时间戳>.json`(该目录已 gitignore)。
|
进入英雄选择后自动跟踪;凑齐 10 人或超时后写出 `results/draft_<时间戳>.json`(该目录已 gitignore)。
|
||||||
会话截图按对局落在 `samples/raw/<matchid>/`。
|
会话截图按对局落在 `samples/raw/<matchid>/`;默认同时把每包 GSI JSON 追加到同目录 `gsi.jsonl`(`--no-dump-gsi` 可关)。
|
||||||
|
|
||||||
> 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。
|
> 主菜单收不到 GSI 是正常的:客户端**第一次载入对局后**才开始推送。看到 `[gsi] connected` 才算链路通。
|
||||||
|
|
||||||
|
|||||||
+16
@@ -10,6 +10,7 @@ Notes:
|
|||||||
- Frames are saved as PNG at native resolution, named cap_HHMMSS.png.
|
- Frames are saved as PNG at native resolution, named cap_HHMMSS.png.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -19,6 +20,7 @@ import mss
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
RAW_DIR = Path(__file__).parent / "samples" / "raw"
|
RAW_DIR = Path(__file__).parent / "samples" / "raw"
|
||||||
|
GSI_JSONL = "gsi.jsonl"
|
||||||
|
|
||||||
|
|
||||||
def raw_dir_for_match(match_id: str | None = None) -> Path:
|
def raw_dir_for_match(match_id: str | None = None) -> Path:
|
||||||
@@ -32,6 +34,20 @@ def raw_dir_for_match(match_id: str | None = None) -> Path:
|
|||||||
return RAW_DIR / safe
|
return RAW_DIR / safe
|
||||||
|
|
||||||
|
|
||||||
|
def append_gsi_payload(match_id: str | None, payload: dict) -> Path:
|
||||||
|
"""Append one full GSI POST body to samples/raw/{match_id}/gsi.jsonl.
|
||||||
|
|
||||||
|
Each line is {"t": <unix seconds>, "payload": <original body>}.
|
||||||
|
"""
|
||||||
|
out = raw_dir_for_match(match_id)
|
||||||
|
out.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = out / GSI_JSONL
|
||||||
|
record = {"t": time.time(), "payload": payload}
|
||||||
|
with path.open("a", encoding="utf-8") as f:
|
||||||
|
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
def grab_frame(sct=None) -> np.ndarray:
|
def grab_frame(sct=None) -> np.ndarray:
|
||||||
"""Grab the primary monitor as a BGR image."""
|
"""Grab the primary monitor as a BGR image."""
|
||||||
if sct is None:
|
if sct is None:
|
||||||
|
|||||||
+2
-1
@@ -119,7 +119,8 @@
|
|||||||
"strategy_tail_polls": 8,
|
"strategy_tail_polls": 8,
|
||||||
"strategy_gsi_wait": 3.0,
|
"strategy_gsi_wait": 3.0,
|
||||||
"capture_interval": 1.0,
|
"capture_interval": 1.0,
|
||||||
"target_slots": 10
|
"target_slots": 10,
|
||||||
|
"dump_payloads": true
|
||||||
},
|
},
|
||||||
"calibrated_from": "draft_141704.png"
|
"calibrated_from": "draft_141704.png"
|
||||||
}
|
}
|
||||||
+27
-1
@@ -6,6 +6,8 @@ Usage:
|
|||||||
python gsi_watch.py --once # capture+recognize right now, no GSI
|
python gsi_watch.py --once # capture+recognize right now, no GSI
|
||||||
python gsi_watch.py --port 3223
|
python gsi_watch.py --port 3223
|
||||||
python gsi_watch.py --states HERO_SELECTION,STRATEGY_TIME
|
python gsi_watch.py --states HERO_SELECTION,STRATEGY_TIME
|
||||||
|
python gsi_watch.py --dump-gsi # force full GSI JSONL logging
|
||||||
|
python gsi_watch.py --no-dump-gsi # disable it
|
||||||
|
|
||||||
When the game enters hero selection the watcher follows the whole draft,
|
When the game enters hero selection the watcher follows the whole draft,
|
||||||
polling the screen and logging each pick as it is revealed (All Pick reveals
|
polling the screen and logging each pick as it is revealed (All Pick reveals
|
||||||
@@ -14,6 +16,9 @@ which lane role you queued for. The result lands in results/draft_<ts>.json.
|
|||||||
|
|
||||||
If config.json has no calibrated slots yet the watcher runs in capture-only
|
If config.json has no calibrated slots yet the watcher runs in capture-only
|
||||||
mode: it still saves frames to samples/raw/<matchid>/ so you can calibrate from them.
|
mode: it still saves frames to samples/raw/<matchid>/ so you can calibrate from them.
|
||||||
|
|
||||||
|
With gsi.dump_payloads (default on), every POST body is appended to
|
||||||
|
samples/raw/<matchid>/gsi.jsonl for later analysis.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -25,7 +30,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import mss
|
import mss
|
||||||
|
|
||||||
from capture import grab_frame, raw_dir_for_match, save_frame
|
from capture import append_gsi_payload, grab_frame, raw_dir_for_match, save_frame
|
||||||
from common import ROOT, load_config, load_template_library
|
from common import ROOT, load_config, load_template_library
|
||||||
from draft_session import HERO_SELECTION, DraftSession, describe, gsi_slot, loc
|
from draft_session import HERO_SELECTION, DraftSession, describe, gsi_slot, loc
|
||||||
from recognize import recognize_image
|
from recognize import recognize_image
|
||||||
@@ -52,6 +57,7 @@ class Watcher:
|
|||||||
gsi = cfg.get("gsi", {})
|
gsi = cfg.get("gsi", {})
|
||||||
self.trigger_states = set(gsi.get("trigger_states", ["DOTA_GAMERULES_STATE_STRATEGY_TIME"]))
|
self.trigger_states = set(gsi.get("trigger_states", ["DOTA_GAMERULES_STATE_STRATEGY_TIME"]))
|
||||||
self.interval = gsi.get("capture_interval", 1.0)
|
self.interval = gsi.get("capture_interval", 1.0)
|
||||||
|
self.dump_payloads = bool(gsi.get("dump_payloads", True))
|
||||||
|
|
||||||
self.library = load_template_library() if calibrated else []
|
self.library = load_template_library() if calibrated else []
|
||||||
self.last_state: str | None = None
|
self.last_state: str | None = None
|
||||||
@@ -59,7 +65,9 @@ class Watcher:
|
|||||||
self.handled_matches: set[str] = set()
|
self.handled_matches: set[str] = set()
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.busy = threading.Lock()
|
self.busy = threading.Lock()
|
||||||
|
self.dump_lock = threading.Lock()
|
||||||
self.self_info: dict = {}
|
self.self_info: dict = {}
|
||||||
|
self._dump_announced: set[str] = set()
|
||||||
|
|
||||||
def on_payload(self, payload: dict) -> None:
|
def on_payload(self, payload: dict) -> None:
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
@@ -79,6 +87,8 @@ class Watcher:
|
|||||||
state = (payload.get("map") or {}).get("game_state")
|
state = (payload.get("map") or {}).get("game_state")
|
||||||
match_id = str((payload.get("map") or {}).get("matchid") or "no-match")
|
match_id = str((payload.get("map") or {}).get("matchid") or "no-match")
|
||||||
self.last_match_id = match_id
|
self.last_match_id = match_id
|
||||||
|
if self.dump_payloads:
|
||||||
|
self._dump_gsi(match_id, payload)
|
||||||
if state is None:
|
if state is None:
|
||||||
# main menu / no active match
|
# main menu / no active match
|
||||||
if self.last_state is not None:
|
if self.last_state is not None:
|
||||||
@@ -108,6 +118,17 @@ class Watcher:
|
|||||||
return
|
return
|
||||||
threading.Thread(target=self.track, args=(match_id, state, key), daemon=True).start()
|
threading.Thread(target=self.track, args=(match_id, state, key), daemon=True).start()
|
||||||
|
|
||||||
|
def _dump_gsi(self, match_id: str, payload: dict) -> None:
|
||||||
|
"""Persist the full POST body; announce the path once per match folder."""
|
||||||
|
try:
|
||||||
|
with self.dump_lock:
|
||||||
|
path = append_gsi_payload(match_id, payload)
|
||||||
|
if match_id not in self._dump_announced:
|
||||||
|
self._dump_announced.add(match_id)
|
||||||
|
print(f"[gsi] dumping payloads -> {path}", flush=True)
|
||||||
|
except OSError as e:
|
||||||
|
print(f"[gsi] dump failed: {e}", flush=True)
|
||||||
|
|
||||||
def track(self, match_id: str, state: str, key: str) -> dict | None:
|
def track(self, match_id: str, state: str, key: str) -> dict | None:
|
||||||
"""Follow the draft from here to the end, recording every reveal."""
|
"""Follow the draft from here to the end, recording every reveal."""
|
||||||
if not self.busy.acquire(blocking=False):
|
if not self.busy.acquire(blocking=False):
|
||||||
@@ -225,6 +246,10 @@ def main() -> None:
|
|||||||
print()
|
print()
|
||||||
|
|
||||||
watcher = Watcher(cfg, calibrated, verbose)
|
watcher = Watcher(cfg, calibrated, verbose)
|
||||||
|
if "--dump-gsi" in args:
|
||||||
|
watcher.dump_payloads = True
|
||||||
|
if "--no-dump-gsi" in args:
|
||||||
|
watcher.dump_payloads = False
|
||||||
|
|
||||||
if "--states" in args:
|
if "--states" in args:
|
||||||
names = args[args.index("--states") + 1].split(",")
|
names = args[args.index("--states") + 1].split(",")
|
||||||
@@ -251,6 +276,7 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
print(f"listening on http://127.0.0.1:{port}/ (Ctrl+C to stop)")
|
print(f"listening on http://127.0.0.1:{port}/ (Ctrl+C to stop)")
|
||||||
print(f"trigger states: {', '.join(sorted(watcher.trigger_states))}")
|
print(f"trigger states: {', '.join(sorted(watcher.trigger_states))}")
|
||||||
|
print(f"gsi dump : {'on -> samples/raw/<matchid>/gsi.jsonl' if watcher.dump_payloads else 'off'}")
|
||||||
if calibrated:
|
if calibrated:
|
||||||
print(f"template library: {len(watcher.library)} entries")
|
print(f"template library: {len(watcher.library)} entries")
|
||||||
print("waiting for Dota 2 ... (needs -gamestateintegration launch option)")
|
print("waiting for Dota 2 ... (needs -gamestateintegration launch option)")
|
||||||
|
|||||||
Reference in New Issue
Block a user