bdo-toolkit 1.0.2__tar.gz → 1.0.4__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.2/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.4}/PKG-INFO +17 -39
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/README.md +16 -38
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_async_sessions.py +81 -23
- bdo_toolkit-1.0.4/src/bdo_toolkit/_async_utils.py +25 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/__init__.py +4 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_constants.py +42 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_formatting.py +65 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/_records.py +711 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/analysis.py +610 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/capture.py +691 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/companions.py +413 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/live.py +252 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/models.py +419 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/observations.py +122 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/persistence.py +326 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/progress.py +113 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/validation.py +94 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_calibration/workflow.py +140 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_backend.py +15 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_deposit_origin.py +16 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_framing.py +54 -45
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/__init__.py +8 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_constants.py +28 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/_records.py +211 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/assembly.py +496 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/formatting.py +241 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/inventory.py +609 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/models.py +669 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/session.py +405 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_item_state/storage.py +980 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/__init__.py +4 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/discovery.py +78 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/manual.py +587 -0
- bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/models.py +202 -0
- bdo_toolkit-1.0.2/src/bdo_toolkit/_deposit_origin.py → bdo_toolkit-1.0.4/src/bdo_toolkit/_origin/tracker.py +39 -783
- bdo_toolkit-1.0.4/src/bdo_toolkit/_profile_io.py +43 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_reassembly.py +12 -7
- bdo_toolkit-1.0.4/src/bdo_toolkit/_record_geometry.py +39 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_version.py +1 -1
- bdo_toolkit-1.0.4/src/bdo_toolkit/calibration.py +98 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/capture.py +119 -38
- bdo_toolkit-1.0.4/src/bdo_toolkit/character_state.py +56 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/item_state.py +3 -3
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/origin_learning.py +2 -43
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/remote_profiles.py +1 -13
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_live_tracker.py +3 -3
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_replay_capture.py +2 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/async_session.py +4 -16
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/session.py +9 -20
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4/src/bdo_toolkit.egg-info}/PKG-INFO +17 -39
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/SOURCES.txt +38 -1
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_async_sessions.py +3 -13
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration.py +436 -348
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_conveniences.py +5 -4
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_lifecycle.py +50 -20
- bdo_toolkit-1.0.4/tests/test_calibration_persistence.py +492 -0
- bdo_toolkit-1.0.4/tests/test_calibration_progress.py +758 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_calibration_retention.py +8 -6
- bdo_toolkit-1.0.4/tests/test_capture_replay.py +131 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_capture_runtime.py +2 -13
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_character_state.py +59 -52
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_decoder_diagnostics.py +2 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_deposit_origin.py +202 -18
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_direction.py +20 -20
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_dynamic_storage_calibration_matrix.py +32 -15
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_examples_and_writers.py +2 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_explicit_profile.py +3 -4
- bdo_toolkit-1.0.4/tests/test_fixture_catalog.py +92 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_fixture_regressions.py +2 -2
- bdo_toolkit-1.0.4/tests/test_framing.py +671 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_initial_load_storage.py +3 -3
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_inventory_snapshots.py +13 -9
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_item_state.py +16 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_live_capture_session.py +59 -16
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_origin_learning.py +2 -2
- bdo_toolkit-1.0.4/tests/test_package_layout.py +17 -0
- bdo_toolkit-1.0.4/tests/test_profile_validation.py +319 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_reassembly.py +101 -6
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_learning.py +15 -31
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_real_captures.py +28 -29
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_replay.py +4 -1
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_session.py +11 -23
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_destination_validation.py +5 -7
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_unknown_destination_calibration.py +2 -2
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_toolkit_api.py +43 -14
- bdo_toolkit-1.0.2/src/bdo_toolkit/calibration.py +0 -3221
- bdo_toolkit-1.0.2/src/bdo_toolkit/character_state.py +0 -3520
- bdo_toolkit-1.0.2/tests/test_hardening.py +0 -1567
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/LICENSE +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/setup.cfg +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/__init__.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_engine.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/cli.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_capture_backend.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.4}/tests/test_storage_hydration_tracker.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.4
|
|
4
4
|
Summary: Passive, read-only BDO packet parsing toolkit for app developers.
|
|
5
5
|
Author: bdo-toolkit contributors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -28,6 +28,7 @@ Dynamic: license-file
|
|
|
28
28
|
# bdo-toolkit
|
|
29
29
|
|
|
30
30
|
[](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
|
|
31
|
+
[](https://pypi.org/project/bdo-toolkit/)
|
|
31
32
|
[](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
|
|
32
33
|

|
|
33
34
|
[](https://ychwu.github.io/bdo-toolkit/#stability)
|
|
@@ -48,11 +49,21 @@ traffic into structured, application-ready data.
|
|
|
48
49
|
|
|
49
50
|
## Capabilities
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
53
|
+
replay a saved PCAP or PCAPNG file.
|
|
54
|
+
|
|
55
|
+
| Capability | What it provides | Guide | Status |
|
|
52
56
|
| --- | --- | --- | --- |
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
57
|
+
| Item activity | A continuing stream of typed `BDOEvent` objects for supported loot, gathering, inventory, and storage changes | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) | Stable |
|
|
58
|
+
| Inventory and town storage | A finite `ItemStateSnapshot` assembled from character-load traffic, with inventory, known balances, and observed town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) | Beta |
|
|
59
|
+
| Arena of Solare leaderboards | A finite `SolareCaptureResult` containing overall rankings, class tables, and player statistics when the capture is complete | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) | Beta |
|
|
60
|
+
|
|
61
|
+
These workflows include synchronous and
|
|
62
|
+
[asyncio](https://ychwu.github.io/bdo-toolkit/#asyncio) sessions, capture and
|
|
63
|
+
decoder health diagnostics, console and JSONL event writers, and a
|
|
64
|
+
[command-line interface](https://ychwu.github.io/bdo-toolkit/#cli). Exact
|
|
65
|
+
signatures, fields, lifecycle behavior, and failure contracts are in the
|
|
66
|
+
[API index](https://ychwu.github.io/bdo-toolkit/#api-index).
|
|
56
67
|
|
|
57
68
|
Testing and validation cover **NA/EU only**. Compatibility with other regional
|
|
58
69
|
services is unknown.
|
|
@@ -93,51 +104,18 @@ installed with the Python wheel.
|
|
|
93
104
|
| Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
|
|
94
105
|
| 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) |
|
|
95
106
|
| 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) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
|
|
96
108
|
| 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) |
|
|
97
109
|
|
|
98
110
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
99
111
|
for every script, its prerequisites, and the guide that explains it.
|
|
100
112
|
|
|
101
|
-
## Documentation
|
|
102
|
-
|
|
103
|
-
The documentation separates task-focused guides from the symbol-first API
|
|
104
|
-
reference. Exact signatures, fields, lifecycle behavior, and failure contracts
|
|
105
|
-
live there rather than in this README.
|
|
106
|
-
|
|
107
|
-
| Goal | Start here |
|
|
108
|
-
| --- | --- |
|
|
109
|
-
| Understand the package | [Overview](https://ychwu.github.io/bdo-toolkit/#overview) |
|
|
110
|
-
| Build with live item events | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
|
|
111
|
-
| Query character inventory and town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
|
|
112
|
-
| Rebuild item decoding after a patch | [Calibration](https://ychwu.github.io/bdo-toolkit/#calibration-workflow) |
|
|
113
|
-
| Capture and query an Arena of Solare leaderboard | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
|
|
114
|
-
| Integrate with an asyncio application | [Asyncio integration](https://ychwu.github.io/bdo-toolkit/#asyncio) |
|
|
115
|
-
| Look up a class, function, or model | [API index](https://ychwu.github.io/bdo-toolkit/#api-index) |
|
|
116
|
-
| Use the terminal interface | [Command line](https://ychwu.github.io/bdo-toolkit/#cli) |
|
|
117
|
-
| Diagnose a problem | [Troubleshooting](https://ychwu.github.io/bdo-toolkit/#errors) |
|
|
118
|
-
| Review data handling and project boundaries | [Safety & privacy](https://ychwu.github.io/bdo-toolkit/#stability) |
|
|
119
|
-
|
|
120
113
|
## Support
|
|
121
114
|
|
|
122
115
|
For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
|
|
123
116
|
|
|
124
117
|
For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
|
|
125
118
|
|
|
126
|
-
## Development
|
|
127
|
-
|
|
128
|
-
```powershell
|
|
129
|
-
git clone https://github.com/ychwu/bdo-toolkit.git
|
|
130
|
-
cd bdo-toolkit
|
|
131
|
-
python -m pip install -e ".[dev]"
|
|
132
|
-
python -m pytest -q -W error
|
|
133
|
-
python -m mypy src/bdo_toolkit
|
|
134
|
-
python -m pip wheel . --no-deps --wheel-dir dist
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
CI runs tests, type checking, wheel construction, and a CLI smoke test on
|
|
138
|
-
Ubuntu and Windows with Python 3.14. Tests that require private
|
|
139
|
-
game-session captures skip when those local fixtures are absent.
|
|
140
|
-
|
|
141
119
|
## License
|
|
142
120
|
|
|
143
121
|
bdo-toolkit is available under the
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# bdo-toolkit
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/bdo-toolkit/)
|
|
4
5
|
[](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
|
|
5
6
|

|
|
6
7
|
[](https://ychwu.github.io/bdo-toolkit/#stability)
|
|
@@ -21,11 +22,21 @@ traffic into structured, application-ready data.
|
|
|
21
22
|
|
|
22
23
|
## Capabilities
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
bdo-toolkit exposes three passive workflows. Each can observe live traffic or
|
|
26
|
+
replay a saved PCAP or PCAPNG file.
|
|
27
|
+
|
|
28
|
+
| Capability | What it provides | Guide | Status |
|
|
25
29
|
| --- | --- | --- | --- |
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
30
|
+
| Item activity | A continuing stream of typed `BDOEvent` objects for supported loot, gathering, inventory, and storage changes | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) | Stable |
|
|
31
|
+
| Inventory and town storage | A finite `ItemStateSnapshot` assembled from character-load traffic, with inventory, known balances, and observed town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) | Beta |
|
|
32
|
+
| Arena of Solare leaderboards | A finite `SolareCaptureResult` containing overall rankings, class tables, and player statistics when the capture is complete | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) | Beta |
|
|
33
|
+
|
|
34
|
+
These workflows include synchronous and
|
|
35
|
+
[asyncio](https://ychwu.github.io/bdo-toolkit/#asyncio) sessions, capture and
|
|
36
|
+
decoder health diagnostics, console and JSONL event writers, and a
|
|
37
|
+
[command-line interface](https://ychwu.github.io/bdo-toolkit/#cli). Exact
|
|
38
|
+
signatures, fields, lifecycle behavior, and failure contracts are in the
|
|
39
|
+
[API index](https://ychwu.github.io/bdo-toolkit/#api-index).
|
|
29
40
|
|
|
30
41
|
Testing and validation cover **NA/EU only**. Compatibility with other regional
|
|
31
42
|
services is unknown.
|
|
@@ -66,51 +77,18 @@ installed with the Python wheel.
|
|
|
66
77
|
| Observe live item activity | [`examples/live_transfer_log.py`](https://github.com/ychwu/bdo-toolkit/blob/main/examples/live_transfer_log.py) |
|
|
67
78
|
| 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) |
|
|
68
79
|
| 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) · [async](https://github.com/ychwu/bdo-toolkit/blob/main/examples/async_calibration_progress.py) |
|
|
69
81
|
| 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) |
|
|
70
82
|
|
|
71
83
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
72
84
|
for every script, its prerequisites, and the guide that explains it.
|
|
73
85
|
|
|
74
|
-
## Documentation
|
|
75
|
-
|
|
76
|
-
The documentation separates task-focused guides from the symbol-first API
|
|
77
|
-
reference. Exact signatures, fields, lifecycle behavior, and failure contracts
|
|
78
|
-
live there rather than in this README.
|
|
79
|
-
|
|
80
|
-
| Goal | Start here |
|
|
81
|
-
| --- | --- |
|
|
82
|
-
| Understand the package | [Overview](https://ychwu.github.io/bdo-toolkit/#overview) |
|
|
83
|
-
| Build with live item events | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
|
|
84
|
-
| Query character inventory and town storage | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
|
|
85
|
-
| Rebuild item decoding after a patch | [Calibration](https://ychwu.github.io/bdo-toolkit/#calibration-workflow) |
|
|
86
|
-
| Capture and query an Arena of Solare leaderboard | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
|
|
87
|
-
| Integrate with an asyncio application | [Asyncio integration](https://ychwu.github.io/bdo-toolkit/#asyncio) |
|
|
88
|
-
| Look up a class, function, or model | [API index](https://ychwu.github.io/bdo-toolkit/#api-index) |
|
|
89
|
-
| Use the terminal interface | [Command line](https://ychwu.github.io/bdo-toolkit/#cli) |
|
|
90
|
-
| Diagnose a problem | [Troubleshooting](https://ychwu.github.io/bdo-toolkit/#errors) |
|
|
91
|
-
| Review data handling and project boundaries | [Safety & privacy](https://ychwu.github.io/bdo-toolkit/#stability) |
|
|
92
|
-
|
|
93
86
|
## Support
|
|
94
87
|
|
|
95
88
|
For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
|
|
96
89
|
|
|
97
90
|
For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
|
|
98
91
|
|
|
99
|
-
## Development
|
|
100
|
-
|
|
101
|
-
```powershell
|
|
102
|
-
git clone https://github.com/ychwu/bdo-toolkit.git
|
|
103
|
-
cd bdo-toolkit
|
|
104
|
-
python -m pip install -e ".[dev]"
|
|
105
|
-
python -m pytest -q -W error
|
|
106
|
-
python -m mypy src/bdo_toolkit
|
|
107
|
-
python -m pip wheel . --no-deps --wheel-dir dist
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
CI runs tests, type checking, wheel construction, and a CLI smoke test on
|
|
111
|
-
Ubuntu and Windows with Python 3.14. Tests that require private
|
|
112
|
-
game-session captures skip when those local fixtures are absent.
|
|
113
|
-
|
|
114
92
|
## License
|
|
115
93
|
|
|
116
94
|
bdo-toolkit is available under the
|
|
@@ -9,6 +9,7 @@ thread and make cancellation deterministic.
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
11
|
import asyncio
|
|
12
|
+
import math
|
|
12
13
|
from collections import deque
|
|
13
14
|
from concurrent.futures import ThreadPoolExecutor
|
|
14
15
|
from functools import partial
|
|
@@ -16,12 +17,14 @@ from pathlib import Path
|
|
|
16
17
|
from types import TracebackType
|
|
17
18
|
from typing import AsyncIterator, Callable, Optional
|
|
18
19
|
|
|
20
|
+
from ._async_utils import _await_preserving_future, _wait_ignoring_cancellation
|
|
19
21
|
from ._capture_runtime import CaptureEndpoint, _attach_cleanup_owner
|
|
20
22
|
from ._capture_options import LiveCaptureOptions, PacketCaptureOptions
|
|
21
23
|
from .calibration import (
|
|
22
24
|
DEFAULT_CALIBRATION_MAX_RETAINED_BYTES,
|
|
23
25
|
DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES,
|
|
24
26
|
CalibrationResult,
|
|
27
|
+
CalibrationProgress,
|
|
25
28
|
CalibrationRetention,
|
|
26
29
|
CalibrationSession,
|
|
27
30
|
)
|
|
@@ -33,28 +36,6 @@ from .origin_learning import CompanionObservation
|
|
|
33
36
|
from .profiles import OpcodeProfile
|
|
34
37
|
|
|
35
38
|
|
|
36
|
-
async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
|
|
37
|
-
"""Wait for an already-started operation, even after caller cancellation."""
|
|
38
|
-
|
|
39
|
-
while not future.done():
|
|
40
|
-
try:
|
|
41
|
-
# asyncio.wait() never propagates cancellation into ``future`` and
|
|
42
|
-
# does not create a cancelled shield wrapper that may later log an
|
|
43
|
-
# otherwise-retrieved worker exception on Python 3.14.
|
|
44
|
-
await asyncio.wait((future,))
|
|
45
|
-
except asyncio.CancelledError:
|
|
46
|
-
# Cleanup must settle before the original cancellation escapes.
|
|
47
|
-
continue
|
|
48
|
-
return future.result()
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
|
|
52
|
-
"""Await without cancelling or wrapping the submitted worker future."""
|
|
53
|
-
|
|
54
|
-
await asyncio.wait((future,))
|
|
55
|
-
return future.result()
|
|
56
|
-
|
|
57
|
-
|
|
58
39
|
def _thread_task[T](function: Callable[[], T]) -> asyncio.Task[T]:
|
|
59
40
|
return asyncio.create_task(asyncio.to_thread(function))
|
|
60
41
|
|
|
@@ -390,6 +371,13 @@ class AsyncCalibrationSession:
|
|
|
390
371
|
session. Exiting the async context before ``stop()`` calls ``abort()`` and
|
|
391
372
|
discards the unfinished calibration, matching the synchronous context
|
|
392
373
|
manager's safety behavior.
|
|
374
|
+
|
|
375
|
+
``stop_on_complete=True`` enables automatic finalization. ``await wait()``
|
|
376
|
+
observes completion without stopping on timeout or cancellation. ``progress``,
|
|
377
|
+
``result``, ``stopped``, and ``stop_reason`` expose the synchronous owner's
|
|
378
|
+
state. ``on_update`` is synchronous on that owner's worker or finalizing
|
|
379
|
+
thread; use ``loop.call_soon_threadsafe`` for async/UI state.
|
|
380
|
+
``request_stop()`` is synchronous and callback-safe.
|
|
393
381
|
"""
|
|
394
382
|
|
|
395
383
|
def __init__(
|
|
@@ -403,6 +391,8 @@ class AsyncCalibrationSession:
|
|
|
403
391
|
min_confidence: float = 0.80,
|
|
404
392
|
max_retained_frames: int = DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES,
|
|
405
393
|
max_retained_bytes: int = DEFAULT_CALIBRATION_MAX_RETAINED_BYTES,
|
|
394
|
+
stop_on_complete: bool = False,
|
|
395
|
+
on_update: Callable[[CalibrationProgress], object] | None = None,
|
|
406
396
|
) -> None:
|
|
407
397
|
self._session = CalibrationSession(
|
|
408
398
|
item_id=item_id,
|
|
@@ -413,6 +403,8 @@ class AsyncCalibrationSession:
|
|
|
413
403
|
min_confidence=min_confidence,
|
|
414
404
|
max_retained_frames=max_retained_frames,
|
|
415
405
|
max_retained_bytes=max_retained_bytes,
|
|
406
|
+
stop_on_complete=stop_on_complete,
|
|
407
|
+
on_update=on_update,
|
|
416
408
|
)
|
|
417
409
|
self._active = False
|
|
418
410
|
self._terminal_action: str | None = None
|
|
@@ -470,7 +462,69 @@ class AsyncCalibrationSession:
|
|
|
470
462
|
def result(self) -> CalibrationResult | None:
|
|
471
463
|
"""Completed result, including one preserved across cancellation."""
|
|
472
464
|
|
|
473
|
-
|
|
465
|
+
if self._result is not None:
|
|
466
|
+
return self._result
|
|
467
|
+
if self._active and getattr(self._session, "stopped", False):
|
|
468
|
+
return self._session.result
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
@property
|
|
472
|
+
def progress(self) -> CalibrationProgress | None:
|
|
473
|
+
return self._session.progress
|
|
474
|
+
|
|
475
|
+
@property
|
|
476
|
+
def stopped(self) -> bool:
|
|
477
|
+
return self._session.stopped
|
|
478
|
+
|
|
479
|
+
@property
|
|
480
|
+
def stop_reason(self) -> str | None:
|
|
481
|
+
return self._session.stop_reason
|
|
482
|
+
|
|
483
|
+
def request_stop(self) -> None:
|
|
484
|
+
"""Callback-safe request; callbacks run on the synchronous worker."""
|
|
485
|
+
self._session.request_stop()
|
|
486
|
+
|
|
487
|
+
async def wait(self, timeout: float | None = None) -> CalibrationResult | None:
|
|
488
|
+
"""Await completion; timeout/cancellation leaves capture running.
|
|
489
|
+
|
|
490
|
+
Cancellation settles the one bounded pending wait before escaping.
|
|
491
|
+
An enclosing async context still performs its usual cleanup on exit.
|
|
492
|
+
"""
|
|
493
|
+
if timeout is not None and (
|
|
494
|
+
isinstance(timeout, bool) or not isinstance(timeout, (int, float))
|
|
495
|
+
or not math.isfinite(timeout) or timeout < 0
|
|
496
|
+
):
|
|
497
|
+
raise ValueError("timeout must be finite and non-negative")
|
|
498
|
+
loop = asyncio.get_running_loop()
|
|
499
|
+
deadline = None if timeout is None else loop.time() + timeout
|
|
500
|
+
terminal_poll = False
|
|
501
|
+
while True:
|
|
502
|
+
remaining = None if deadline is None else max(0.0, deadline - loop.time())
|
|
503
|
+
wait_seconds = 0.2 if remaining is None else min(0.2, remaining)
|
|
504
|
+
if terminal_poll:
|
|
505
|
+
wait_seconds = 0.0
|
|
506
|
+
task = _thread_task(partial(self._session.wait, wait_seconds))
|
|
507
|
+
try:
|
|
508
|
+
result = await _await_preserving_future(task)
|
|
509
|
+
except asyncio.CancelledError:
|
|
510
|
+
try:
|
|
511
|
+
await _wait_ignoring_cancellation(task)
|
|
512
|
+
except BaseException:
|
|
513
|
+
pass
|
|
514
|
+
raise
|
|
515
|
+
if result is not None:
|
|
516
|
+
self._result = result
|
|
517
|
+
return result
|
|
518
|
+
if terminal_poll:
|
|
519
|
+
return None # Terminal discard, without a result or error.
|
|
520
|
+
if self.stopped:
|
|
521
|
+
# Completion may have raced the empty poll's delivery. Re-read
|
|
522
|
+
# through wait() to retrieve the final result or raise its error,
|
|
523
|
+
# using the same cancellation-safe worker path as ordinary polls.
|
|
524
|
+
terminal_poll = True
|
|
525
|
+
continue
|
|
526
|
+
if deadline is not None and loop.time() >= deadline:
|
|
527
|
+
return None
|
|
474
528
|
|
|
475
529
|
async def start(self) -> None:
|
|
476
530
|
"""Begin calibration capture without blocking the event loop."""
|
|
@@ -581,6 +635,10 @@ class AsyncCalibrationSession:
|
|
|
581
635
|
self._active = self._session.cleanup_incomplete
|
|
582
636
|
raise
|
|
583
637
|
else:
|
|
638
|
+
# Automatic finalization may have won the race with context exit.
|
|
639
|
+
# Preserve that completed result even when nobody awaited wait().
|
|
640
|
+
if getattr(self._session, "stopped", False):
|
|
641
|
+
self._result = self._session.result
|
|
584
642
|
self._active = False
|
|
585
643
|
|
|
586
644
|
async def abort(self) -> None:
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Cancellation-safe waits shared by asynchronous session facades."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def _wait_ignoring_cancellation[T](future: asyncio.Future[T]) -> T:
|
|
7
|
+
"""Wait for an already-started operation, even after caller cancellation."""
|
|
8
|
+
|
|
9
|
+
while not future.done():
|
|
10
|
+
try:
|
|
11
|
+
# asyncio.wait() never propagates cancellation into ``future`` and
|
|
12
|
+
# does not create a cancelled shield wrapper that may later log an
|
|
13
|
+
# otherwise-retrieved worker exception on Python 3.14.
|
|
14
|
+
await asyncio.wait((future,))
|
|
15
|
+
except asyncio.CancelledError:
|
|
16
|
+
# Cleanup must settle before the original cancellation escapes.
|
|
17
|
+
continue
|
|
18
|
+
return future.result()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def _await_preserving_future[T](future: asyncio.Future[T]) -> T:
|
|
22
|
+
"""Await without cancelling or wrapping the submitted worker future."""
|
|
23
|
+
|
|
24
|
+
await asyncio.wait((future,))
|
|
25
|
+
return future.result()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Private calibration constants implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .._protocol import (
|
|
6
|
+
CHARACTER_LOAD_CONTEXT,
|
|
7
|
+
SOURCE_CONTEXT_LABELS,
|
|
8
|
+
STORAGE_DELTA_CONTEXTS,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
CALIBRATION_ACTIONS = (
|
|
13
|
+
"loot-preview",
|
|
14
|
+
"storage-to-inventory",
|
|
15
|
+
"inventory-to-storage",
|
|
16
|
+
)
|
|
17
|
+
DEFAULT_CALIBRATION_MAX_RETAINED_FRAMES = 50_000
|
|
18
|
+
DEFAULT_CALIBRATION_MAX_RETAINED_BYTES = 64 * 1024 * 1024
|
|
19
|
+
_CALIBRATION_MAX_ACTIVE_FLOWS = 64
|
|
20
|
+
OPCODE_PROFILE_EVENTS = (
|
|
21
|
+
"LOOT_PREVIEW",
|
|
22
|
+
"INVENTORY_TRANSFER",
|
|
23
|
+
"SOURCE_CONTAINER_DECREMENT",
|
|
24
|
+
"SOURCE_STACK_DECREMENT",
|
|
25
|
+
"SOURCE_ITEM_REFERENCE",
|
|
26
|
+
"STORAGE_ITEM_DELTA",
|
|
27
|
+
)
|
|
28
|
+
_FAMILY_LABELS = {
|
|
29
|
+
"into_inventory": "storage->inventory",
|
|
30
|
+
"into_storage": "inventory->storage",
|
|
31
|
+
}
|
|
32
|
+
REFERENCE_FRAME_MAX_LENGTH = 128
|
|
33
|
+
SOURCE_DECREMENT_FRAME_MAX_LENGTH = 512
|
|
34
|
+
_HIGH_ENTROPY_CONTEXTS = tuple(
|
|
35
|
+
value
|
|
36
|
+
for value in SOURCE_CONTEXT_LABELS
|
|
37
|
+
if value != CHARACTER_LOAD_CONTEXT and value not in STORAGE_DELTA_CONTEXTS
|
|
38
|
+
)
|
|
39
|
+
_EXPECTED_FAMILY = {
|
|
40
|
+
"storage-to-inventory": "into_inventory",
|
|
41
|
+
"inventory-to-storage": "into_storage",
|
|
42
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Private calibration formatting implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
from ._constants import OPCODE_PROFILE_EVENTS
|
|
8
|
+
from ._records import _Options
|
|
9
|
+
from .models import MessageSpec
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _dedupe_message_specs(specs: Iterable[MessageSpec]) -> list[MessageSpec]:
|
|
13
|
+
output: list[MessageSpec] = []
|
|
14
|
+
seen: set[tuple[object, ...]] = set()
|
|
15
|
+
for spec in specs:
|
|
16
|
+
key = spec.dedupe_key()
|
|
17
|
+
if key in seen:
|
|
18
|
+
continue
|
|
19
|
+
seen.add(key)
|
|
20
|
+
output.append(spec)
|
|
21
|
+
return output
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _confidence_label(score: float) -> str:
|
|
25
|
+
level = "high" if score >= 0.90 else "medium"
|
|
26
|
+
return f"calibrated-{level}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _calibration_source(options: _Options, action: str) -> str:
|
|
30
|
+
parts = [f"calibrate {action}", f"item_id={options.item_id}"]
|
|
31
|
+
if options.quantity is not None:
|
|
32
|
+
parts.append(f"qty={options.quantity}")
|
|
33
|
+
return " ".join(parts)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _iso_timestamp(timestamp: float) -> str:
|
|
37
|
+
return (
|
|
38
|
+
dt.datetime.fromtimestamp(timestamp, tz=dt.UTC)
|
|
39
|
+
.isoformat(timespec="seconds")
|
|
40
|
+
.replace("+00:00", "Z")
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _utc_now_text() -> str:
|
|
45
|
+
return (
|
|
46
|
+
dt.datetime.now(tz=dt.UTC)
|
|
47
|
+
.isoformat(timespec="seconds")
|
|
48
|
+
.replace("+00:00", "Z")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _events_for_action(action: str) -> tuple[str, ...]:
|
|
53
|
+
if action == "loot-preview":
|
|
54
|
+
return ("LOOT_PREVIEW",)
|
|
55
|
+
if action == "storage-to-inventory":
|
|
56
|
+
return ("INVENTORY_TRANSFER", "SOURCE_CONTAINER_DECREMENT")
|
|
57
|
+
if action == "inventory-to-storage":
|
|
58
|
+
return (
|
|
59
|
+
"SOURCE_STACK_DECREMENT",
|
|
60
|
+
"SOURCE_ITEM_REFERENCE",
|
|
61
|
+
"STORAGE_ITEM_DELTA",
|
|
62
|
+
)
|
|
63
|
+
# ``auto`` observes both transfer directions but never owns the separate
|
|
64
|
+
# loot-preview workflow.
|
|
65
|
+
return tuple(event for event in OPCODE_PROFILE_EVENTS if event != "LOOT_PREVIEW")
|