bdo-toolkit 1.0.4__tar.gz → 1.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.5}/PKG-INFO +17 -6
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/README.md +16 -5
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/__init__.py +5 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_version.py +1 -1
- bdo_toolkit-1.0.5/src/bdo_toolkit/capture_diagnosis.py +246 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/cli.py +29 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5/src/bdo_toolkit.egg-info}/PKG-INFO +17 -6
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit.egg-info/SOURCES.txt +2 -0
- bdo_toolkit-1.0.5/tests/test_capture_diagnosis.py +215 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/LICENSE +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/setup.cfg +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_async_sessions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_async_utils.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/_formatting.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/_records.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/analysis.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/companions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/live.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/observations.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/persistence.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/progress.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_calibration/workflow.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_capture_backend.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_engine.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_framing.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/_records.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/assembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/formatting.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/inventory.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_item_state/storage.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_origin/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_origin/discovery.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_origin/manual.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_origin/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_origin/tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_profile_io.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_reassembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_record_geometry.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/character_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/item_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/origin_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/remote_profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_live_tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_replay_capture.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/async_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/solare/session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_async_sessions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration_conveniences.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration_lifecycle.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration_persistence.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration_progress.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_calibration_retention.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_capture_backend.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_capture_replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_character_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_decoder_diagnostics.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_direction.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_examples_and_writers.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_explicit_profile.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_fixture_catalog.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_fixture_regressions.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_framing.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_initial_load_storage.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_inventory_snapshots.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_item_state.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_live_capture_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_origin_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_package_layout.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_profile_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_reassembly.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_learning.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_real_captures.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_replay.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_solare_session.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_storage_hydration_tracker.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_storage_unknown_destination_calibration.py +0 -0
- {bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_toolkit_api.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bdo-toolkit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Passive, read-only BDO packet parsing toolkit for app developers.
|
|
5
5
|
Author: bdo-toolkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,16 +37,27 @@ Dynamic: license-file
|
|
|
37
37
|
Passive, read-only Python tooling that turns live or recorded Black Desert
|
|
38
38
|
traffic into structured, application-ready data.
|
|
39
39
|
|
|
40
|
-
[Documentation](https://ychwu.github.io/bdo-toolkit/)
|
|
41
|
-
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart)
|
|
42
|
-
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
43
|
-
[API index](https://ychwu.github.io/bdo-toolkit/#api-index)
|
|
40
|
+
[Documentation](https://ychwu.github.io/bdo-toolkit/) ·
|
|
41
|
+
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart) ·
|
|
42
|
+
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples) ·
|
|
43
|
+
[API index](https://ychwu.github.io/bdo-toolkit/#api-index) ·
|
|
44
44
|
[Report an issue](https://github.com/ychwu/bdo-toolkit/issues)
|
|
45
45
|
|
|
46
46
|
> **Passive, read-only boundary.** bdo-toolkit observes local traffic or saved
|
|
47
47
|
> captures. It does not send or modify packets, replay traffic to the game,
|
|
48
48
|
> automate gameplay, inspect process memory, or bypass anti-cheat software.
|
|
49
49
|
|
|
50
|
+
## Live-capture limitations
|
|
51
|
+
|
|
52
|
+
Live capture depends heavily on your network setup and on whether the capture
|
|
53
|
+
backend can see BDO traffic. VPNs and routing services such as ExitLag may require
|
|
54
|
+
manual interface, local IP, and port settings; automatic detection may select the
|
|
55
|
+
wrong connection. Traffic visible only as an encrypted tunnel cannot be decoded.
|
|
56
|
+
Follow [Diagnose capture when automatic selection fails](https://ychwu.github.io/bdo-toolkit/#capture-foundation/diagnose-network)
|
|
57
|
+
to inspect connections on Windows, select grouped capture settings, and verify
|
|
58
|
+
them with game activity. The guide covers VPNs, local proxies, and cases where
|
|
59
|
+
no usable adapter is found. Discovery does not apply settings or verify decoding.
|
|
60
|
+
|
|
50
61
|
## Capabilities
|
|
51
62
|
|
|
52
63
|
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
@@ -104,7 +115,7 @@ installed with the Python wheel.
|
|
|
104
115
|
| Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
|
|
105
116
|
| Capture inventory and town storage on character load | [`examples/live_character_load_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_character_load_snapshot.py) |
|
|
106
117
|
| Rebuild an item profile after a patch | [`examples/live_calibrate_profile.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibrate_profile.py) |
|
|
107
|
-
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py)
|
|
118
|
+
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
|
|
108
119
|
| Capture an Arena of Solare leaderboard load | [`examples/solare_live_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/solare_live_snapshot.py) |
|
|
109
120
|
|
|
110
121
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
@@ -10,16 +10,27 @@
|
|
|
10
10
|
Passive, read-only Python tooling that turns live or recorded Black Desert
|
|
11
11
|
traffic into structured, application-ready data.
|
|
12
12
|
|
|
13
|
-
[Documentation](https://ychwu.github.io/bdo-toolkit/)
|
|
14
|
-
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart)
|
|
15
|
-
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
16
|
-
[API index](https://ychwu.github.io/bdo-toolkit/#api-index)
|
|
13
|
+
[Documentation](https://ychwu.github.io/bdo-toolkit/) ·
|
|
14
|
+
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart) ·
|
|
15
|
+
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples) ·
|
|
16
|
+
[API index](https://ychwu.github.io/bdo-toolkit/#api-index) ·
|
|
17
17
|
[Report an issue](https://github.com/ychwu/bdo-toolkit/issues)
|
|
18
18
|
|
|
19
19
|
> **Passive, read-only boundary.** bdo-toolkit observes local traffic or saved
|
|
20
20
|
> captures. It does not send or modify packets, replay traffic to the game,
|
|
21
21
|
> automate gameplay, inspect process memory, or bypass anti-cheat software.
|
|
22
22
|
|
|
23
|
+
## Live-capture limitations
|
|
24
|
+
|
|
25
|
+
Live capture depends heavily on your network setup and on whether the capture
|
|
26
|
+
backend can see BDO traffic. VPNs and routing services such as ExitLag may require
|
|
27
|
+
manual interface, local IP, and port settings; automatic detection may select the
|
|
28
|
+
wrong connection. Traffic visible only as an encrypted tunnel cannot be decoded.
|
|
29
|
+
Follow [Diagnose capture when automatic selection fails](https://ychwu.github.io/bdo-toolkit/#capture-foundation/diagnose-network)
|
|
30
|
+
to inspect connections on Windows, select grouped capture settings, and verify
|
|
31
|
+
them with game activity. The guide covers VPNs, local proxies, and cases where
|
|
32
|
+
no usable adapter is found. Discovery does not apply settings or verify decoding.
|
|
33
|
+
|
|
23
34
|
## Capabilities
|
|
24
35
|
|
|
25
36
|
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
@@ -77,7 +88,7 @@ installed with the Python wheel.
|
|
|
77
88
|
| Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
|
|
78
89
|
| Capture inventory and town storage on character load | [`examples/live_character_load_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_character_load_snapshot.py) |
|
|
79
90
|
| Rebuild an item profile after a patch | [`examples/live_calibrate_profile.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibrate_profile.py) |
|
|
80
|
-
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py)
|
|
91
|
+
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
|
|
81
92
|
| Capture an Arena of Solare leaderboard load | [`examples/solare_live_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/solare_live_snapshot.py) |
|
|
82
93
|
|
|
83
94
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from ._async_sessions import AsyncCalibrationSession, AsyncLiveCaptureSession
|
|
4
4
|
from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
|
|
5
5
|
from ._capture_runtime import CaptureEndpoint
|
|
6
|
+
from .capture_diagnosis import CaptureCandidate, CaptureDiagnosis, CaptureSettingsProposal, diagnose_capture
|
|
6
7
|
from .capture import (
|
|
7
8
|
CaptureIntegrityError,
|
|
8
9
|
LiveCaptureHealth,
|
|
@@ -48,6 +49,10 @@ __all__ = [
|
|
|
48
49
|
"AsyncLiveCaptureSession",
|
|
49
50
|
"BDOEvent",
|
|
50
51
|
"CaptureEndpoint",
|
|
52
|
+
"CaptureCandidate",
|
|
53
|
+
"CaptureDiagnosis",
|
|
54
|
+
"CaptureSettingsProposal",
|
|
55
|
+
"diagnose_capture",
|
|
51
56
|
"CaptureIntegrityError",
|
|
52
57
|
"ConsoleEventWriter",
|
|
53
58
|
"DecoderDiagnostic",
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Read-only Windows connection discovery; candidates are not capture validation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
import ipaddress
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from ._capture_options import LiveCaptureOptions
|
|
14
|
+
from ._protocol import DEFAULT_SERVER_PORTS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class CaptureCandidate:
|
|
19
|
+
"""One observed game connection mapped to a capture adapter, if available."""
|
|
20
|
+
|
|
21
|
+
process_id: int
|
|
22
|
+
local_ip: str
|
|
23
|
+
local_port: int
|
|
24
|
+
remote_ip: str
|
|
25
|
+
remote_port: int
|
|
26
|
+
interface: str | None
|
|
27
|
+
peer_process: str | None
|
|
28
|
+
kind: Literal["local_proxy", "game_port", "other_tcp"]
|
|
29
|
+
explanation: str
|
|
30
|
+
|
|
31
|
+
def to_live_options(self) -> LiveCaptureOptions:
|
|
32
|
+
"""Build explicit options; reject candidates without a matching adapter."""
|
|
33
|
+
if self.interface is None:
|
|
34
|
+
raise ValueError("No matching capture adapter; manual investigation required")
|
|
35
|
+
return LiveCaptureOptions(
|
|
36
|
+
interface=self.interface, local_ip=self.local_ip,
|
|
37
|
+
ports=(self.remote_port,),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class CaptureSettingsProposal:
|
|
43
|
+
"""Compatible connections grouped for one explicit capture configuration."""
|
|
44
|
+
|
|
45
|
+
process_id: int
|
|
46
|
+
interface: str | None
|
|
47
|
+
local_ip: str
|
|
48
|
+
kind: Literal["local_proxy", "game_port", "other_tcp"]
|
|
49
|
+
ports: tuple[int, ...]
|
|
50
|
+
candidates: tuple[CaptureCandidate, ...]
|
|
51
|
+
|
|
52
|
+
def to_live_options(self) -> LiveCaptureOptions:
|
|
53
|
+
"""Build settings with every observed port; never starts capture."""
|
|
54
|
+
if self.interface is None:
|
|
55
|
+
raise ValueError("No matching capture adapter; manual investigation required")
|
|
56
|
+
return LiveCaptureOptions(
|
|
57
|
+
interface=self.interface, local_ip=self.local_ip, ports=self.ports,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict[str, Any]:
|
|
61
|
+
"""Return JSON-compatible settings and underlying connection evidence."""
|
|
62
|
+
return {
|
|
63
|
+
"process_id": self.process_id, "interface": self.interface,
|
|
64
|
+
"local_ip": self.local_ip, "kind": self.kind,
|
|
65
|
+
"ports": list(self.ports),
|
|
66
|
+
"candidates": [asdict(candidate) for candidate in self.candidates],
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class CaptureDiagnosis:
|
|
72
|
+
"""Snapshot of candidates and limitations; never automatically applied."""
|
|
73
|
+
|
|
74
|
+
status: Literal["candidates", "no_game", "no_connections", "unavailable"]
|
|
75
|
+
candidates: tuple[CaptureCandidate, ...] = ()
|
|
76
|
+
messages: tuple[str, ...] = ()
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def proposals(self) -> tuple[CaptureSettingsProposal, ...]:
|
|
80
|
+
"""Group by process, adapter, local address, and kind in evidence order.
|
|
81
|
+
|
|
82
|
+
Local proxies additionally match peer address and process name. When
|
|
83
|
+
ownership is unknown, different peer ports remain separate. Candidates
|
|
84
|
+
without matching adapters remain visible but cannot produce options.
|
|
85
|
+
"""
|
|
86
|
+
groups: dict[tuple[object, ...], list[CaptureCandidate]] = {}
|
|
87
|
+
for candidate in self.candidates:
|
|
88
|
+
key: tuple[object, ...] = (
|
|
89
|
+
candidate.process_id, candidate.interface,
|
|
90
|
+
candidate.local_ip, candidate.kind,
|
|
91
|
+
)
|
|
92
|
+
if candidate.kind == "local_proxy":
|
|
93
|
+
key += (candidate.remote_ip, candidate.peer_process)
|
|
94
|
+
if candidate.peer_process is None:
|
|
95
|
+
key += (candidate.remote_port,)
|
|
96
|
+
groups.setdefault(key, []).append(candidate)
|
|
97
|
+
return tuple(
|
|
98
|
+
CaptureSettingsProposal(
|
|
99
|
+
process_id=group[0].process_id, interface=group[0].interface,
|
|
100
|
+
local_ip=group[0].local_ip, kind=group[0].kind,
|
|
101
|
+
ports=tuple(sorted({candidate.remote_port for candidate in group})),
|
|
102
|
+
candidates=tuple(group),
|
|
103
|
+
)
|
|
104
|
+
for group in groups.values()
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
"""Return JSON-compatible diagnostic data, including local endpoints."""
|
|
109
|
+
return {
|
|
110
|
+
"status": self.status,
|
|
111
|
+
"candidates": [asdict(candidate) for candidate in self.candidates],
|
|
112
|
+
"messages": list(self.messages),
|
|
113
|
+
"proposals": [proposal.to_dict() for proposal in self.proposals],
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# Fixed script: no user text is interpolated into PowerShell. Read the connection
|
|
118
|
+
# table once, and correlate local proxies by the exact reversed TCP four-tuple.
|
|
119
|
+
_WINDOWS_SNAPSHOT = r"""
|
|
120
|
+
$ErrorActionPreference = 'Stop'
|
|
121
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
122
|
+
$processes = @(Get-Process)
|
|
123
|
+
$games = @($processes | Where-Object { $_.ProcessName -in @('BlackDesert64', 'BlackDesert') })
|
|
124
|
+
$connections = @(Get-NetTCPConnection)
|
|
125
|
+
$rows = @($connections | Where-Object { $_.State -eq 'Established' -and $_.OwningProcess -in $games.Id } | ForEach-Object {
|
|
126
|
+
$connection = $_
|
|
127
|
+
$peer = $connections | Where-Object {
|
|
128
|
+
$_.LocalAddress -eq $connection.RemoteAddress -and $_.LocalPort -eq $connection.RemotePort -and
|
|
129
|
+
$_.RemoteAddress -eq $connection.LocalAddress -and $_.RemotePort -eq $connection.LocalPort
|
|
130
|
+
} | Select-Object -First 1
|
|
131
|
+
$peerName = $null
|
|
132
|
+
if ($null -ne $peer) { $peerName = ($processes | Where-Object { $_.Id -eq $peer.OwningProcess } | Select-Object -First 1).ProcessName }
|
|
133
|
+
@{ process_id = [int]$connection.OwningProcess; local_ip = $connection.LocalAddress;
|
|
134
|
+
local_port = [int]$connection.LocalPort; remote_ip = $connection.RemoteAddress;
|
|
135
|
+
remote_port = [int]$connection.RemotePort; peer_process = $peerName }
|
|
136
|
+
})
|
|
137
|
+
@{ game_count = $games.Count; connections = $rows } | ConvertTo-Json -Depth 4 -Compress
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _windows_snapshot(timeout: float) -> dict[str, Any]:
|
|
142
|
+
result = subprocess.run(
|
|
143
|
+
["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", _WINDOWS_SNAPSHOT],
|
|
144
|
+
capture_output=True, encoding="utf-8", errors="replace", timeout=timeout,
|
|
145
|
+
# subprocess exposes this constant only on Windows. Discovery already
|
|
146
|
+
# gates this helper by platform; getattr also permits non-Windows typing.
|
|
147
|
+
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
148
|
+
)
|
|
149
|
+
if result.returncode:
|
|
150
|
+
raise RuntimeError("Windows connection inspection failed; check permissions and NetTCPIP availability")
|
|
151
|
+
value = json.loads(result.stdout.lstrip("\ufeff"))
|
|
152
|
+
if not isinstance(value, dict) or not isinstance(value.get("connections"), list):
|
|
153
|
+
raise ValueError("Unexpected Windows connection snapshot")
|
|
154
|
+
if type(value.get("game_count")) is not int or value["game_count"] < 0:
|
|
155
|
+
raise ValueError("Unexpected Windows process count")
|
|
156
|
+
for row in value["connections"]:
|
|
157
|
+
if not isinstance(row, dict):
|
|
158
|
+
raise ValueError("Unexpected Windows connection row")
|
|
159
|
+
for name in ("local_ip", "remote_ip"):
|
|
160
|
+
if not isinstance(row.get(name), str):
|
|
161
|
+
raise ValueError("Missing Windows connection address")
|
|
162
|
+
ipaddress.ip_address(row[name])
|
|
163
|
+
for name, maximum in (("process_id", 0xFFFFFFFF), ("local_port", 65535), ("remote_port", 65535)):
|
|
164
|
+
if type(row.get(name)) is not int or not 1 <= row[name] <= maximum:
|
|
165
|
+
raise ValueError("Invalid Windows connection identifier or port")
|
|
166
|
+
if "peer_process" not in row or not isinstance(row["peer_process"], (str, type(None))):
|
|
167
|
+
raise ValueError("Invalid Windows peer process name")
|
|
168
|
+
return value
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _capture_adapters() -> list[tuple[str, tuple[str, ...]]]:
|
|
172
|
+
from scapy.all import conf # type: ignore
|
|
173
|
+
|
|
174
|
+
conf.ifaces.reload()
|
|
175
|
+
return [
|
|
176
|
+
(str(adapter.network_name), tuple(str(ip) for ip in adapter.ips.get(4, [])))
|
|
177
|
+
for adapter in conf.ifaces.values()
|
|
178
|
+
]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def diagnose_capture(*, timeout: float = 15.0) -> CaptureDiagnosis:
|
|
182
|
+
"""Inspect Windows BDO TCP connections and capture interfaces.
|
|
183
|
+
|
|
184
|
+
``timeout`` bounds the Windows subprocess, not Scapy adapter enumeration.
|
|
185
|
+
Call from a worker in GUI applications. No packets are captured, no profile
|
|
186
|
+
is needed, and no settings are applied. IPv6 connections are reported as a
|
|
187
|
+
limitation. Other platforms return ``unavailable``. Operational inspection
|
|
188
|
+
failures are returned in messages; invalid timeout values raise ValueError.
|
|
189
|
+
Proxy ports and process IDs are valid only for the observed session.
|
|
190
|
+
"""
|
|
191
|
+
if isinstance(timeout, bool) or not math.isfinite(timeout) or timeout <= 0:
|
|
192
|
+
raise ValueError("timeout must be finite and greater than zero")
|
|
193
|
+
if sys.platform != "win32":
|
|
194
|
+
return CaptureDiagnosis("unavailable", messages=("Connection discovery currently supports Windows only.",))
|
|
195
|
+
try:
|
|
196
|
+
snapshot = _windows_snapshot(timeout)
|
|
197
|
+
except subprocess.TimeoutExpired:
|
|
198
|
+
return CaptureDiagnosis("unavailable", messages=(
|
|
199
|
+
f"Windows connection inspection timed out after {timeout:g} seconds; retry or increase timeout.",
|
|
200
|
+
))
|
|
201
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
202
|
+
return CaptureDiagnosis("unavailable", messages=(f"Could not inspect BDO connections: {exc}",))
|
|
203
|
+
if not snapshot["game_count"]:
|
|
204
|
+
return CaptureDiagnosis("no_game", messages=("Start Black Desert, enter the game, and retry.",))
|
|
205
|
+
messages = ["Candidates are unverified: test with known in-game activity. No settings were changed."]
|
|
206
|
+
try:
|
|
207
|
+
adapters = _capture_adapters()
|
|
208
|
+
except Exception as exc:
|
|
209
|
+
adapters = []
|
|
210
|
+
messages.append(f"Could not enumerate capture adapters: {exc}")
|
|
211
|
+
candidates: list[CaptureCandidate] = []
|
|
212
|
+
for row in snapshot["connections"]:
|
|
213
|
+
local = ipaddress.ip_address(row["local_ip"])
|
|
214
|
+
remote = ipaddress.ip_address(row["remote_ip"])
|
|
215
|
+
if local.version != 4 or remote.version != 4:
|
|
216
|
+
messages.append("An established BDO IPv6 connection was skipped: capture decoding supports IPv4 only.")
|
|
217
|
+
continue
|
|
218
|
+
interfaces = sorted({
|
|
219
|
+
name for name, ips in adapters
|
|
220
|
+
if str(local) in ips or (local.is_loopback and name == r"\Device\NPF_Loopback")
|
|
221
|
+
})
|
|
222
|
+
kind: Literal["local_proxy", "game_port", "other_tcp"]
|
|
223
|
+
if local.is_loopback and remote.is_loopback:
|
|
224
|
+
kind = "local_proxy"
|
|
225
|
+
explanation = "BDO connects through a local TCP peer; capture loopback using the peer port. Recheck after reconnecting."
|
|
226
|
+
elif row["remote_port"] in DEFAULT_SERVER_PORTS:
|
|
227
|
+
kind = "game_port"
|
|
228
|
+
explanation = "Connection uses a default BDO game port; verify traffic is visible on this adapter."
|
|
229
|
+
else:
|
|
230
|
+
kind = "other_tcp"
|
|
231
|
+
explanation = "Nonstandard TCP peer; may be web, authentication, or other traffic rather than gameplay."
|
|
232
|
+
if not interfaces:
|
|
233
|
+
explanation += " No capture adapter matches the local IPv4 address."
|
|
234
|
+
if len(interfaces) > 1:
|
|
235
|
+
explanation += " Multiple adapters match; test each candidate explicitly."
|
|
236
|
+
matches: list[str | None] = list(interfaces) if interfaces else [None]
|
|
237
|
+
for interface in matches:
|
|
238
|
+
candidates.append(CaptureCandidate(
|
|
239
|
+
process_id=row["process_id"], local_ip=str(local), local_port=row["local_port"],
|
|
240
|
+
remote_ip=str(remote), remote_port=row["remote_port"], interface=interface,
|
|
241
|
+
peer_process=row["peer_process"], kind=kind, explanation=explanation,
|
|
242
|
+
))
|
|
243
|
+
candidates.sort(key=lambda c: ({"local_proxy": 0, "game_port": 1, "other_tcp": 2}[c.kind], c.process_id, c.local_port, c.interface or ""))
|
|
244
|
+
if not candidates:
|
|
245
|
+
messages.append("No established IPv4 TCP candidates found. Enter a game server and retry.")
|
|
246
|
+
return CaptureDiagnosis("candidates" if candidates else "no_connections", tuple(candidates), tuple(dict.fromkeys(messages)))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
+
import json
|
|
6
7
|
import math
|
|
7
8
|
import sys
|
|
8
9
|
from pathlib import Path
|
|
@@ -11,6 +12,7 @@ from typing import Optional
|
|
|
11
12
|
from . import __version__
|
|
12
13
|
from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
|
|
13
14
|
from .capture import capture_live, replay_pcap
|
|
15
|
+
from .capture_diagnosis import diagnose_capture
|
|
14
16
|
from .diagnostics import DecoderDiagnostic
|
|
15
17
|
from ._protocol import DEFAULT_SERVER_PORTS
|
|
16
18
|
from .calibration import (
|
|
@@ -544,6 +546,27 @@ def _run_origin_promote(args: argparse.Namespace) -> int:
|
|
|
544
546
|
return 0
|
|
545
547
|
|
|
546
548
|
|
|
549
|
+
def _run_diagnose_capture(args: argparse.Namespace) -> int:
|
|
550
|
+
result = diagnose_capture(timeout=args.timeout)
|
|
551
|
+
if args.json:
|
|
552
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
553
|
+
else:
|
|
554
|
+
print(f"Capture discovery: {result.status}")
|
|
555
|
+
for message in result.messages:
|
|
556
|
+
print(message)
|
|
557
|
+
for number, proposal in enumerate(result.proposals, 1):
|
|
558
|
+
print(f"\n{number}. {proposal.kind} (BDO PID {proposal.process_id})")
|
|
559
|
+
print(f" Interface: {proposal.interface or '(no matching adapter)'}")
|
|
560
|
+
print(f" Local IP: {proposal.local_ip}; ports: {','.join(map(str, proposal.ports))}")
|
|
561
|
+
for explanation in dict.fromkeys(c.explanation for c in proposal.candidates):
|
|
562
|
+
print(f" {explanation}")
|
|
563
|
+
print(f" Connections: {len(proposal.candidates)}")
|
|
564
|
+
for candidate in proposal.candidates:
|
|
565
|
+
peer = f" ({candidate.peer_process})" if candidate.peer_process else ""
|
|
566
|
+
print(f" {candidate.local_ip}:{candidate.local_port} -> {candidate.remote_ip}:{candidate.remote_port}{peer}")
|
|
567
|
+
return 2 if result.status == "unavailable" else 0
|
|
568
|
+
|
|
569
|
+
|
|
547
570
|
def build_parser() -> argparse.ArgumentParser:
|
|
548
571
|
parser = argparse.ArgumentParser(
|
|
549
572
|
prog="bdo-toolkit",
|
|
@@ -557,6 +580,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
557
580
|
"--version", action="version", version=f"%(prog)s {__version__}"
|
|
558
581
|
)
|
|
559
582
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
583
|
+
diagnose = subparsers.add_parser(
|
|
584
|
+
"diagnose-capture", help="inspect Windows BDO connections and suggest capture settings",
|
|
585
|
+
)
|
|
586
|
+
diagnose.add_argument("--json", action="store_true", help="print structured JSON")
|
|
587
|
+
diagnose.add_argument("--timeout", type=_positive_float, default=15.0, help="Windows inspection timeout in seconds (default: 15)")
|
|
588
|
+
diagnose.set_defaults(func=_run_diagnose_capture)
|
|
560
589
|
|
|
561
590
|
profile = subparsers.add_parser(
|
|
562
591
|
"profile",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bdo-toolkit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.5
|
|
4
4
|
Summary: Passive, read-only BDO packet parsing toolkit for app developers.
|
|
5
5
|
Author: bdo-toolkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,16 +37,27 @@ Dynamic: license-file
|
|
|
37
37
|
Passive, read-only Python tooling that turns live or recorded Black Desert
|
|
38
38
|
traffic into structured, application-ready data.
|
|
39
39
|
|
|
40
|
-
[Documentation](https://ychwu.github.io/bdo-toolkit/)
|
|
41
|
-
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart)
|
|
42
|
-
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
43
|
-
[API index](https://ychwu.github.io/bdo-toolkit/#api-index)
|
|
40
|
+
[Documentation](https://ychwu.github.io/bdo-toolkit/) ·
|
|
41
|
+
[Quickstart](https://ychwu.github.io/bdo-toolkit/#quickstart) ·
|
|
42
|
+
[Examples](https://ychwu.github.io/bdo-toolkit/#item-examples) ·
|
|
43
|
+
[API index](https://ychwu.github.io/bdo-toolkit/#api-index) ·
|
|
44
44
|
[Report an issue](https://github.com/ychwu/bdo-toolkit/issues)
|
|
45
45
|
|
|
46
46
|
> **Passive, read-only boundary.** bdo-toolkit observes local traffic or saved
|
|
47
47
|
> captures. It does not send or modify packets, replay traffic to the game,
|
|
48
48
|
> automate gameplay, inspect process memory, or bypass anti-cheat software.
|
|
49
49
|
|
|
50
|
+
## Live-capture limitations
|
|
51
|
+
|
|
52
|
+
Live capture depends heavily on your network setup and on whether the capture
|
|
53
|
+
backend can see BDO traffic. VPNs and routing services such as ExitLag may require
|
|
54
|
+
manual interface, local IP, and port settings; automatic detection may select the
|
|
55
|
+
wrong connection. Traffic visible only as an encrypted tunnel cannot be decoded.
|
|
56
|
+
Follow [Diagnose capture when automatic selection fails](https://ychwu.github.io/bdo-toolkit/#capture-foundation/diagnose-network)
|
|
57
|
+
to inspect connections on Windows, select grouped capture settings, and verify
|
|
58
|
+
them with game activity. The guide covers VPNs, local proxies, and cases where
|
|
59
|
+
no usable adapter is found. Discovery does not apply settings or verify decoding.
|
|
60
|
+
|
|
50
61
|
## Capabilities
|
|
51
62
|
|
|
52
63
|
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
@@ -104,7 +115,7 @@ installed with the Python wheel.
|
|
|
104
115
|
| Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
|
|
105
116
|
| Capture inventory and town storage on character load | [`examples/live_character_load_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_character_load_snapshot.py) |
|
|
106
117
|
| Rebuild an item profile after a patch | [`examples/live_calibrate_profile.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibrate_profile.py) |
|
|
107
|
-
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py)
|
|
118
|
+
| Observe calibration progress and stop on completion | [`examples/live_calibration_progress.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_calibration_progress.py) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
|
|
108
119
|
| Capture an Arena of Solare leaderboard load | [`examples/solare_live_snapshot.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/solare_live_snapshot.py) |
|
|
109
120
|
|
|
110
121
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
@@ -21,6 +21,7 @@ src/bdo_toolkit/_storage_hydration.py
|
|
|
21
21
|
src/bdo_toolkit/_version.py
|
|
22
22
|
src/bdo_toolkit/calibration.py
|
|
23
23
|
src/bdo_toolkit/capture.py
|
|
24
|
+
src/bdo_toolkit/capture_diagnosis.py
|
|
24
25
|
src/bdo_toolkit/character_state.py
|
|
25
26
|
src/bdo_toolkit/cli.py
|
|
26
27
|
src/bdo_toolkit/diagnostics.py
|
|
@@ -88,6 +89,7 @@ tests/test_calibration_persistence.py
|
|
|
88
89
|
tests/test_calibration_progress.py
|
|
89
90
|
tests/test_calibration_retention.py
|
|
90
91
|
tests/test_capture_backend.py
|
|
92
|
+
tests/test_capture_diagnosis.py
|
|
91
93
|
tests/test_capture_replay.py
|
|
92
94
|
tests/test_capture_runtime.py
|
|
93
95
|
tests/test_character_state.py
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Synthetic network discovery: no game, capture driver, or network required."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from bdo_toolkit import CaptureDiagnosis, diagnose_capture, cli
|
|
9
|
+
from bdo_toolkit import capture_diagnosis as diagnosis
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def connection(**changes):
|
|
13
|
+
row = dict(process_id=12, local_ip="192.0.2.2", local_port=50000,
|
|
14
|
+
remote_ip="198.51.100.1", remote_port=8889, peer_process=None)
|
|
15
|
+
row.update(changes)
|
|
16
|
+
return row
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def snapshot(monkeypatch):
|
|
21
|
+
data = {"game_count": 1, "connections": [connection()]}
|
|
22
|
+
monkeypatch.setattr(diagnosis.sys, "platform", "win32")
|
|
23
|
+
monkeypatch.setattr(diagnosis, "_windows_snapshot", lambda timeout: data)
|
|
24
|
+
monkeypatch.setattr(diagnosis, "_capture_adapters", lambda: [
|
|
25
|
+
("ethernet", ("192.0.2.2",)), (r"\Device\NPF_Loopback", ("127.0.0.1",)),
|
|
26
|
+
])
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_direct_and_web_connections_are_distinguished(snapshot):
|
|
31
|
+
snapshot["connections"].append(connection(remote_port=443, local_port=50001))
|
|
32
|
+
result = diagnose_capture()
|
|
33
|
+
assert result.status == "candidates"
|
|
34
|
+
assert [c.kind for c in result.candidates] == ["game_port", "other_tcp"]
|
|
35
|
+
options = result.candidates[0].to_live_options()
|
|
36
|
+
assert (options.interface, options.local_ip, options.ports) == ("ethernet", "192.0.2.2", (8889,))
|
|
37
|
+
assert json.loads(json.dumps(result.to_dict()))["status"] == "candidates"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_proxy_uses_current_peer_port_and_loopback(snapshot):
|
|
41
|
+
snapshot["connections"] = [connection(local_ip="127.0.0.2", remote_ip="127.0.0.1",
|
|
42
|
+
remote_port=53123, peer_process="ExitLag")]
|
|
43
|
+
candidate, = diagnose_capture().candidates
|
|
44
|
+
assert candidate.kind == "local_proxy"
|
|
45
|
+
assert candidate.peer_process == "ExitLag"
|
|
46
|
+
options = candidate.to_live_options()
|
|
47
|
+
assert options.interface == r"\Device\NPF_Loopback"
|
|
48
|
+
assert options.ports == (53123,)
|
|
49
|
+
assert options.local_ip == "127.0.0.2"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_unidentified_proxy_still_has_evidence(snapshot):
|
|
53
|
+
snapshot["connections"] = [connection(local_ip="127.0.0.1", remote_ip="127.0.0.1", remote_port=53000)]
|
|
54
|
+
candidate, = diagnose_capture().candidates
|
|
55
|
+
assert candidate.peer_process is None
|
|
56
|
+
assert candidate.kind == "local_proxy"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_missing_adapter_does_not_fall_back_to_default(snapshot, monkeypatch):
|
|
60
|
+
monkeypatch.setattr(diagnosis, "_capture_adapters", lambda: [])
|
|
61
|
+
candidate, = diagnose_capture().candidates
|
|
62
|
+
assert candidate.interface is None
|
|
63
|
+
with pytest.raises(ValueError, match="No matching"):
|
|
64
|
+
candidate.to_live_options()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_ambiguous_adapters_are_all_returned(snapshot, monkeypatch):
|
|
68
|
+
monkeypatch.setattr(diagnosis, "_capture_adapters", lambda: [("a", ("192.0.2.2",)), ("b", ("192.0.2.2",))])
|
|
69
|
+
result = diagnose_capture()
|
|
70
|
+
assert [c.interface for c in result.candidates] == ["a", "b"]
|
|
71
|
+
assert all("Multiple adapters" in c.explanation for c in result.candidates)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_ipv6_is_not_suggested(snapshot):
|
|
75
|
+
snapshot["connections"] = [connection(local_ip="::1", remote_ip="::1")]
|
|
76
|
+
result = diagnose_capture()
|
|
77
|
+
assert result.status == "no_connections"
|
|
78
|
+
assert not result.candidates
|
|
79
|
+
assert any("IPv6" in m for m in result.messages)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_no_game_and_no_connections(snapshot):
|
|
83
|
+
snapshot["game_count"] = 0
|
|
84
|
+
assert diagnose_capture().status == "no_game"
|
|
85
|
+
snapshot.update(game_count=1, connections=[])
|
|
86
|
+
assert diagnose_capture().status == "no_connections"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize("error", [OSError("denied"), subprocess.TimeoutExpired("powershell", 1), ValueError("bad JSON")])
|
|
90
|
+
def test_inspection_failure_is_structured(snapshot, monkeypatch, error):
|
|
91
|
+
def fail(timeout):
|
|
92
|
+
raise error
|
|
93
|
+
monkeypatch.setattr(diagnosis, "_windows_snapshot", fail)
|
|
94
|
+
result = diagnose_capture()
|
|
95
|
+
assert result.status == "unavailable"
|
|
96
|
+
assert result.messages
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_adapter_failure_preserves_connection_evidence(snapshot, monkeypatch):
|
|
100
|
+
def fail():
|
|
101
|
+
raise RuntimeError("Npcap unavailable")
|
|
102
|
+
monkeypatch.setattr(diagnosis, "_capture_adapters", fail)
|
|
103
|
+
result = diagnose_capture()
|
|
104
|
+
assert result.candidates[0].interface is None
|
|
105
|
+
assert any("Npcap unavailable" in m for m in result.messages)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_other_platform_does_not_run_powershell(monkeypatch):
|
|
109
|
+
monkeypatch.setattr(diagnosis.sys, "platform", "linux")
|
|
110
|
+
assert diagnose_capture().status == "unavailable"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.mark.parametrize("timeout", [0, -1, float("inf"), float("nan"), True])
|
|
114
|
+
def test_invalid_timeout(timeout):
|
|
115
|
+
with pytest.raises(ValueError):
|
|
116
|
+
diagnose_capture(timeout=timeout)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_cli_json_and_text(snapshot, capsys):
|
|
120
|
+
assert cli.main(["diagnose-capture", "--json"]) == 0
|
|
121
|
+
assert json.loads(capsys.readouterr().out)["candidates"][0]["remote_port"] == 8889
|
|
122
|
+
assert cli.main(["diagnose-capture"]) == 0
|
|
123
|
+
output = capsys.readouterr().out
|
|
124
|
+
assert "Interface: ethernet" in output
|
|
125
|
+
assert "unverified" in output
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_cli_unavailable_and_timeout_validation(monkeypatch, capsys):
|
|
129
|
+
monkeypatch.setattr(cli, "diagnose_capture", lambda **kw: CaptureDiagnosis("unavailable"))
|
|
130
|
+
assert cli.main(["diagnose-capture", "--json"]) == 2
|
|
131
|
+
assert json.loads(capsys.readouterr().out)["status"] == "unavailable"
|
|
132
|
+
with pytest.raises(SystemExit):
|
|
133
|
+
cli.main(["diagnose-capture", "--timeout", "0"])
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_windows_collector_uses_fixed_script_timeout_and_hidden_window(monkeypatch):
|
|
137
|
+
monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", 0x08000000, raising=False)
|
|
138
|
+
def run(argv, **kwargs):
|
|
139
|
+
assert argv[-1] == diagnosis._WINDOWS_SNAPSHOT
|
|
140
|
+
assert kwargs["timeout"] == 3.0
|
|
141
|
+
assert kwargs["creationflags"] == 0x08000000
|
|
142
|
+
assert "shell" not in kwargs
|
|
143
|
+
return subprocess.CompletedProcess(argv, 0, '\ufeff' + json.dumps({"game_count": 1, "connections": [connection()]}))
|
|
144
|
+
monkeypatch.setattr(subprocess, "run", run)
|
|
145
|
+
assert diagnosis._windows_snapshot(3.0)["connections"][0]["remote_port"] == 8889
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@pytest.mark.parametrize("payload", ["invalid", "[]", '{"connections":[]}',
|
|
149
|
+
json.dumps({"game_count": 1, "connections": [connection(remote_port=0)]}),
|
|
150
|
+
json.dumps({"game_count": 1, "connections": [connection(local_ip="bad")]}),
|
|
151
|
+
])
|
|
152
|
+
def test_windows_collector_rejects_malformed_snapshot(monkeypatch, payload):
|
|
153
|
+
monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", 0x08000000, raising=False)
|
|
154
|
+
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: subprocess.CompletedProcess(a, 0, payload))
|
|
155
|
+
with pytest.raises(ValueError):
|
|
156
|
+
diagnosis._windows_snapshot(1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_windows_collector_failure_does_not_leak_command_output(monkeypatch):
|
|
160
|
+
monkeypatch.setattr(subprocess, "CREATE_NO_WINDOW", 0x08000000, raising=False)
|
|
161
|
+
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: subprocess.CompletedProcess(a, 1, "", "private command output"))
|
|
162
|
+
with pytest.raises(RuntimeError, match="inspection failed") as error:
|
|
163
|
+
diagnosis._windows_snapshot(1)
|
|
164
|
+
assert "private" not in str(error.value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_settings_proposals_merge_game_ports_but_not_web(snapshot):
|
|
168
|
+
snapshot["connections"] = [connection(remote_port=p, local_port=50000+i)
|
|
169
|
+
for i, p in enumerate([8889, 8885, 8884, 443, 443])]
|
|
170
|
+
result = diagnose_capture()
|
|
171
|
+
game, web = result.proposals
|
|
172
|
+
assert game.ports == (8884, 8885, 8889)
|
|
173
|
+
assert game.to_live_options().ports == game.ports
|
|
174
|
+
assert web.kind == "other_tcp" and web.ports == (443,)
|
|
175
|
+
assert len(game.candidates) == 3 and len(web.candidates) == 2
|
|
176
|
+
assert len(result.candidates) == 5
|
|
177
|
+
assert result.to_dict()["proposals"][0]["ports"] == [8884, 8885, 8889]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@pytest.mark.parametrize("change", [dict(process_id=13), dict(local_ip="192.0.2.3"),
|
|
181
|
+
dict(interface="another"), dict(kind="other_tcp")])
|
|
182
|
+
def test_proposal_group_boundaries(change):
|
|
183
|
+
from dataclasses import replace
|
|
184
|
+
from bdo_toolkit import CaptureCandidate
|
|
185
|
+
first = CaptureCandidate(**connection(), interface="ethernet", kind="game_port", explanation="test")
|
|
186
|
+
second = replace(first, **change)
|
|
187
|
+
assert len(CaptureDiagnosis("candidates", (first, second)).proposals) == 2
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def test_proxy_grouping_preserves_unknown_ownership_boundaries():
|
|
191
|
+
from dataclasses import replace
|
|
192
|
+
from bdo_toolkit import CaptureCandidate
|
|
193
|
+
first = CaptureCandidate(**connection(local_ip="127.0.0.1", remote_ip="127.0.0.1", peer_process="ExitLag"),
|
|
194
|
+
interface="loopback", kind="local_proxy", explanation="test")
|
|
195
|
+
second = replace(first, remote_port=53000)
|
|
196
|
+
assert len(CaptureDiagnosis("candidates", (first, second)).proposals) == 1
|
|
197
|
+
for other in [replace(second, peer_process="Other"), replace(second, remote_ip="127.0.0.2")]:
|
|
198
|
+
assert len(CaptureDiagnosis("candidates", (first, other)).proposals) == 2
|
|
199
|
+
unknown = (replace(first, peer_process=None), replace(second, peer_process=None))
|
|
200
|
+
assert len(CaptureDiagnosis("candidates", unknown).proposals) == 2
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_proposals_empty_and_missing_adapter(snapshot, monkeypatch):
|
|
204
|
+
assert CaptureDiagnosis("no_game").proposals == ()
|
|
205
|
+
monkeypatch.setattr(diagnosis, "_capture_adapters", lambda: [])
|
|
206
|
+
with pytest.raises(ValueError, match="No matching"):
|
|
207
|
+
diagnose_capture().proposals[0].to_live_options()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_cli_consolidates_ports(snapshot, capsys):
|
|
211
|
+
snapshot["connections"] = [connection(remote_port=port) for port in (8889, 8885, 8884)]
|
|
212
|
+
assert cli.main(["diagnose-capture"]) == 0
|
|
213
|
+
output = capsys.readouterr().out
|
|
214
|
+
assert output.count("Interface:") == 1
|
|
215
|
+
assert "ports: 8884,8885,8889" in output
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bdo_toolkit-1.0.4 → bdo_toolkit-1.0.5}/tests/test_storage_unknown_destination_calibration.py
RENAMED
|
File without changes
|
|
File without changes
|