bdo-toolkit 1.0.2__tar.gz → 1.0.3__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.3}/PKG-INFO +16 -39
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/README.md +15 -38
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_reassembly.py +12 -7
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_version.py +1 -1
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/calibration.py +50 -2
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_live_tracker.py +3 -3
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_replay_capture.py +2 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/session.py +7 -14
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3/src/bdo_toolkit.egg-info}/PKG-INFO +16 -39
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration.py +28 -9
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_character_state.py +3 -3
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_reassembly.py +63 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_replay.py +4 -1
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_session.py +11 -23
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/LICENSE +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/pyproject.toml +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/setup.cfg +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/__init__.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_async_sessions.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_backend.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_options.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_engine.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_framing.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_profile_runtime.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_protocol.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_specs.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_storage_hydration.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/capture.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/character_state.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/cli.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/diagnostics.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/events.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/filters.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/item_state.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/origin_learning.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/profiles.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/py.typed +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/remote_profiles.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/__init__.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_constants.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_details.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_discovery.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_result.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_scanner.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_validation.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/async_session.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/models.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/replay.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/writers.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/SOURCES.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/requires.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_async_sessions.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_conveniences.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_lifecycle.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_retention.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_capture_backend.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_capture_runtime.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_cli.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_decoder_diagnostics.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_deposit_origin.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_direction.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_engine.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_examples_and_writers.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_explicit_profile.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_fixture_regressions.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_hardening.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_initial_load_storage.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_inventory_snapshots.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_item_state.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_live_capture_session.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_origin_learning.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_remote_profiles.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_api.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_details.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_learning.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_learning_adversarial.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_real_captures.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_destination_validation.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_events.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_hydration_tracker.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_unknown_destination_calibration.py +0 -0
- {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/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.3
|
|
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.
|
|
@@ -98,46 +109,12 @@ installed with the Python wheel.
|
|
|
98
109
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
99
110
|
for every script, its prerequisites, and the guide that explains it.
|
|
100
111
|
|
|
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
112
|
## Support
|
|
121
113
|
|
|
122
114
|
For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
|
|
123
115
|
|
|
124
116
|
For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
|
|
125
117
|
|
|
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
118
|
## License
|
|
142
119
|
|
|
143
120
|
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.
|
|
@@ -71,46 +82,12 @@ installed with the Python wheel.
|
|
|
71
82
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
72
83
|
for every script, its prerequisites, and the guide that explains it.
|
|
73
84
|
|
|
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
85
|
## Support
|
|
94
86
|
|
|
95
87
|
For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
|
|
96
88
|
|
|
97
89
|
For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
|
|
98
90
|
|
|
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
91
|
## License
|
|
115
92
|
|
|
116
93
|
bdo-toolkit is available under the
|
|
@@ -47,6 +47,7 @@ class TCPFlowState:
|
|
|
47
47
|
scanner: StreamScanner
|
|
48
48
|
max_pending_segments: int = MAX_PENDING_SEGMENTS
|
|
49
49
|
max_pending_bytes: Optional[int] = None
|
|
50
|
+
defer_gap_timeouts: bool = False
|
|
50
51
|
next_sequence: Optional[int] = None
|
|
51
52
|
pending: dict[int, PendingSegment] = field(default_factory=dict)
|
|
52
53
|
unanchored: dict[int, PendingSegment] = field(default_factory=dict)
|
|
@@ -280,7 +281,7 @@ class TCPFlowState:
|
|
|
280
281
|
# capture loss through the ordinary gap-reset accounting.
|
|
281
282
|
while self._pending_capacity_exceeded(self.pending):
|
|
282
283
|
self._resume_after_gap()
|
|
283
|
-
|
|
284
|
+
elif not self.defer_gap_timeouts:
|
|
284
285
|
self.service_gaps(context.timestamp)
|
|
285
286
|
return
|
|
286
287
|
|
|
@@ -411,6 +412,7 @@ class FlowManager:
|
|
|
411
412
|
track_flow_generations: bool = False,
|
|
412
413
|
max_pending_segments: int = MAX_PENDING_SEGMENTS,
|
|
413
414
|
max_pending_bytes: Optional[int] = None,
|
|
415
|
+
defer_gap_timeouts: bool = False,
|
|
414
416
|
) -> None:
|
|
415
417
|
if max_flows is not None and max_flows <= 0:
|
|
416
418
|
raise ValueError("max_flows must be positive or None")
|
|
@@ -430,6 +432,8 @@ class FlowManager:
|
|
|
430
432
|
raise TypeError("max_pending_bytes must be an integer or None")
|
|
431
433
|
if max_pending_bytes is not None and max_pending_bytes <= 0:
|
|
432
434
|
raise ValueError("max_pending_bytes must be positive or None")
|
|
435
|
+
if not isinstance(defer_gap_timeouts, bool):
|
|
436
|
+
raise TypeError("defer_gap_timeouts must be a boolean")
|
|
433
437
|
self.server_ports = frozenset(server_ports)
|
|
434
438
|
self._scanner_factory = scanner_factory
|
|
435
439
|
self._max_flows = max_flows
|
|
@@ -440,6 +444,7 @@ class FlowManager:
|
|
|
440
444
|
self._track_flow_generations = track_flow_generations
|
|
441
445
|
self._max_pending_segments = max_pending_segments
|
|
442
446
|
self._max_pending_bytes = max_pending_bytes
|
|
447
|
+
self._defer_gap_timeouts = defer_gap_timeouts
|
|
443
448
|
self._next_flow_generation = 0
|
|
444
449
|
self._flows: OrderedDict[FlowKey, TCPFlowState] = OrderedDict()
|
|
445
450
|
self._tcp_gap_resets = 0
|
|
@@ -458,6 +463,7 @@ class FlowManager:
|
|
|
458
463
|
scanner=self._scanner_factory(),
|
|
459
464
|
max_pending_segments=self._max_pending_segments,
|
|
460
465
|
max_pending_bytes=self._max_pending_bytes,
|
|
466
|
+
defer_gap_timeouts=self._defer_gap_timeouts,
|
|
461
467
|
generation=generation,
|
|
462
468
|
on_gap_reset=lambda resume_sequence: self._record_gap_reset(
|
|
463
469
|
flow,
|
|
@@ -484,13 +490,12 @@ class FlowManager:
|
|
|
484
490
|
return self._tcp_gap_resets
|
|
485
491
|
|
|
486
492
|
def service_gaps(self, now: float) -> int:
|
|
487
|
-
"""Advance pending-gap timers using a caller-supplied
|
|
493
|
+
"""Advance pending-gap timers using a caller-supplied clock.
|
|
488
494
|
|
|
489
|
-
``FlowManager`` does not own a timer thread.
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
cumulative.
|
|
495
|
+
``FlowManager`` does not own a timer thread. Callers that defer
|
|
496
|
+
per-packet timeouts may invoke this at a lifecycle boundary they know
|
|
497
|
+
is safe. The return value is the number of new resets performed during
|
|
498
|
+
this call; ``tcp_gap_resets`` is cumulative.
|
|
494
499
|
"""
|
|
495
500
|
with self._lock:
|
|
496
501
|
before = self.tcp_gap_resets
|
|
@@ -2354,9 +2354,29 @@ def _discover_source_container_decrement(
|
|
|
2354
2354
|
)
|
|
2355
2355
|
|
|
2356
2356
|
for quantity_offset in quantity_offsets:
|
|
2357
|
-
#
|
|
2358
|
-
#
|
|
2357
|
+
# Known layouts vary whether context is before or after instance.
|
|
2358
|
+
# Historical layouts put context before quantity. A newer layout
|
|
2359
|
+
# puts quantity first, so admit a trailing context only when the
|
|
2360
|
+
# same frame has one exact receipt-instance anchor; quantity and a
|
|
2361
|
+
# context label alone are too weak.
|
|
2359
2362
|
context_offset = _discover_context_offset(frame, quantity_offset)
|
|
2363
|
+
if context_offset is None and exact_instance_offset is not None:
|
|
2364
|
+
trailing_context_offset = (
|
|
2365
|
+
_discover_source_container_trailing_context_offset(
|
|
2366
|
+
frame,
|
|
2367
|
+
quantity_offset + 4,
|
|
2368
|
+
)
|
|
2369
|
+
)
|
|
2370
|
+
if (
|
|
2371
|
+
trailing_context_offset is not None
|
|
2372
|
+
and not _ranges_overlap(
|
|
2373
|
+
exact_instance_offset,
|
|
2374
|
+
8,
|
|
2375
|
+
trailing_context_offset,
|
|
2376
|
+
4,
|
|
2377
|
+
)
|
|
2378
|
+
):
|
|
2379
|
+
context_offset = trailing_context_offset
|
|
2360
2380
|
if context_offset is None:
|
|
2361
2381
|
continue
|
|
2362
2382
|
if exact_instance_offset is not None and _ranges_overlap(
|
|
@@ -2785,6 +2805,34 @@ def _discover_context_offset(frame: BDOFrame, before_offset: int) -> Optional[in
|
|
|
2785
2805
|
return best_offset
|
|
2786
2806
|
|
|
2787
2807
|
|
|
2808
|
+
def _discover_source_container_trailing_context_offset(
|
|
2809
|
+
frame: BDOFrame,
|
|
2810
|
+
after_offset: int,
|
|
2811
|
+
) -> Optional[int]:
|
|
2812
|
+
"""Return one unique source context after a container quantity field.
|
|
2813
|
+
|
|
2814
|
+
This is deliberately narrower than ``_discover_context_offset``. The
|
|
2815
|
+
caller requires an exact receipt-instance match before using this fallback,
|
|
2816
|
+
and ambiguity among trailing context labels fails closed.
|
|
2817
|
+
"""
|
|
2818
|
+
|
|
2819
|
+
offsets: set[int] = set()
|
|
2820
|
+
for context_bytes in SOURCE_CONTEXT_LABELS:
|
|
2821
|
+
if (
|
|
2822
|
+
context_bytes == CHARACTER_LOAD_CONTEXT
|
|
2823
|
+
or context_bytes in STORAGE_DELTA_CONTEXTS
|
|
2824
|
+
):
|
|
2825
|
+
continue
|
|
2826
|
+
search_at = max(5, after_offset)
|
|
2827
|
+
while True:
|
|
2828
|
+
offset = frame.message.find(context_bytes, search_at)
|
|
2829
|
+
if offset < 0:
|
|
2830
|
+
break
|
|
2831
|
+
offsets.add(offset)
|
|
2832
|
+
search_at = offset + 1
|
|
2833
|
+
return next(iter(offsets)) if len(offsets) == 1 else None
|
|
2834
|
+
|
|
2835
|
+
|
|
2788
2836
|
def _discover_storage_context_offset(
|
|
2789
2837
|
frame: BDOFrame,
|
|
2790
2838
|
before_offset: int,
|
|
@@ -291,12 +291,12 @@ class LiveSolareDiscoveryTracker:
|
|
|
291
291
|
prefix rejection—remain authoritative at this boundary.
|
|
292
292
|
"""
|
|
293
293
|
|
|
294
|
+
last_candidate_activity_at = self._last_candidate_activity_at
|
|
294
295
|
if (
|
|
295
296
|
self.complete
|
|
296
297
|
or self._tail_finalized
|
|
297
|
-
or
|
|
298
|
-
or now
|
|
299
|
-
< LIVE_CANDIDATE_IDLE_SECONDS
|
|
298
|
+
or last_candidate_activity_at is None
|
|
299
|
+
or now < last_candidate_activity_at + LIVE_CANDIDATE_IDLE_SECONDS
|
|
300
300
|
):
|
|
301
301
|
return False
|
|
302
302
|
self.refresh()
|
|
@@ -36,6 +36,7 @@ class SolareFrameCollector:
|
|
|
36
36
|
on_traffic: Optional[Callable[[], None]] = None,
|
|
37
37
|
*,
|
|
38
38
|
retain_frames: bool = True,
|
|
39
|
+
defer_gap_timeouts: bool = False,
|
|
39
40
|
) -> None:
|
|
40
41
|
self.ports = validate_server_ports(ports)
|
|
41
42
|
self.frames: list[BDOFrame] = []
|
|
@@ -57,6 +58,7 @@ class SolareFrameCollector:
|
|
|
57
58
|
max_flows=SOLARE_MAX_ACTIVE_FLOWS,
|
|
58
59
|
max_pending_segments=SOLARE_MAX_PENDING_SEGMENTS,
|
|
59
60
|
max_pending_bytes=SOLARE_MAX_PENDING_BYTES,
|
|
61
|
+
defer_gap_timeouts=defer_gap_timeouts,
|
|
60
62
|
on_flow_eviction=self._count_flow_eviction,
|
|
61
63
|
track_flow_generations=True,
|
|
62
64
|
)
|
|
@@ -198,6 +198,7 @@ class LiveSolareSession:
|
|
|
198
198
|
on_frame=tracker.observe,
|
|
199
199
|
on_traffic=self._announce_traffic,
|
|
200
200
|
retain_frames=False,
|
|
201
|
+
defer_gap_timeouts=True,
|
|
201
202
|
)
|
|
202
203
|
capture = LivePacketCapture(
|
|
203
204
|
capture_options=self._capture_options,
|
|
@@ -709,23 +710,15 @@ class LiveSolareSession:
|
|
|
709
710
|
self._latch_confirmation()
|
|
710
711
|
|
|
711
712
|
def _service_drained_clocks(self) -> None:
|
|
712
|
-
"""Advance
|
|
713
|
+
"""Advance candidate-idle time only after packet work is caught up.
|
|
713
714
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
finalization
|
|
715
|
+
Live Solare deliberately does not perform timeout-based TCP gap
|
|
716
|
+
recovery while capture remains active. Native callback batching can
|
|
717
|
+
pause longer than the generic deadline even when the missing prefix is
|
|
718
|
+
still buffered. Reorder pressure remains bounded and immediate, and
|
|
719
|
+
finalization resolves any genuinely unfinished gap fail-closed.
|
|
719
720
|
"""
|
|
720
721
|
|
|
721
|
-
if not self._packet_queue.empty():
|
|
722
|
-
return
|
|
723
|
-
|
|
724
|
-
collector = self._collector
|
|
725
|
-
if collector is not None:
|
|
726
|
-
collector.service_gaps(time.time())
|
|
727
|
-
self._announce_tcp_gap_loss()
|
|
728
|
-
|
|
729
722
|
if not self._packet_queue.empty():
|
|
730
723
|
return
|
|
731
724
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bdo-toolkit
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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.
|
|
@@ -98,46 +109,12 @@ installed with the Python wheel.
|
|
|
98
109
|
See the [Examples index](https://ychwu.github.io/bdo-toolkit/#item-examples)
|
|
99
110
|
for every script, its prerequisites, and the guide that explains it.
|
|
100
111
|
|
|
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
112
|
## Support
|
|
121
113
|
|
|
122
114
|
For questions, contact me on Discord: `._.__.__._._.__._____.__._.___.`
|
|
123
115
|
|
|
124
116
|
For bugs and feature requests, [open a GitHub issue](https://github.com/ychwu/bdo-toolkit/issues).
|
|
125
117
|
|
|
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
118
|
## License
|
|
142
119
|
|
|
143
120
|
bdo-toolkit is available under the
|
|
@@ -643,17 +643,18 @@ def test_one_session_1_4_deposits_and_5_withdrawal_keep_all_strides():
|
|
|
643
643
|
|
|
644
644
|
|
|
645
645
|
@pytest.mark.parametrize(
|
|
646
|
-
("layout", "
|
|
646
|
+
("layout", "expected_offsets"),
|
|
647
647
|
[
|
|
648
|
-
("
|
|
649
|
-
("
|
|
648
|
+
("instance-context-quantity", (13, 5, 17)),
|
|
649
|
+
("context-instance-quantity-different-instance", (7, 23, 32)),
|
|
650
|
+
("quantity-instance-context", (34, 21, 5)),
|
|
651
|
+
("trailing-context-without-exact-instance", None),
|
|
652
|
+
("ambiguous-trailing-context", None),
|
|
650
653
|
],
|
|
651
654
|
)
|
|
652
|
-
def
|
|
655
|
+
def test_container_companion_handles_known_field_orders_safely(
|
|
653
656
|
layout,
|
|
654
|
-
|
|
655
|
-
expected_instance,
|
|
656
|
-
expected_quantity,
|
|
657
|
+
expected_offsets,
|
|
657
658
|
):
|
|
658
659
|
from bdo_toolkit._protocol import BDOFrame, FlowKey, PacketContext
|
|
659
660
|
from bdo_toolkit.calibration import (
|
|
@@ -679,17 +680,30 @@ def test_container_companion_discovers_both_known_field_orders(
|
|
|
679
680
|
receipt_instance = bytes.fromhex("1122334455667788")
|
|
680
681
|
source_context = bytes.fromhex("d0f205a3")
|
|
681
682
|
|
|
682
|
-
if layout == "
|
|
683
|
+
if layout == "instance-context-quantity":
|
|
683
684
|
companion_message = bytearray(42)
|
|
684
685
|
companion_message[5:13] = receipt_instance
|
|
685
686
|
companion_message[13:17] = source_context
|
|
686
687
|
companion_message[17:21] = quantity.to_bytes(4, "little")
|
|
687
|
-
|
|
688
|
+
elif layout == "context-instance-quantity-different-instance":
|
|
688
689
|
companion_message = bytearray(40)
|
|
689
690
|
companion_message[7:11] = source_context
|
|
690
691
|
companion_message[23:31] = bytes.fromhex("0102030405060708")
|
|
691
692
|
companion_message[31] = 0x02
|
|
692
693
|
companion_message[32:36] = quantity.to_bytes(4, "little")
|
|
694
|
+
else:
|
|
695
|
+
companion_message = bytearray(
|
|
696
|
+
48 if layout == "ambiguous-trailing-context" else 40
|
|
697
|
+
)
|
|
698
|
+
companion_message[5:9] = quantity.to_bytes(4, "little")
|
|
699
|
+
companion_message[21:29] = (
|
|
700
|
+
bytes.fromhex("0102030405060708")
|
|
701
|
+
if layout == "trailing-context-without-exact-instance"
|
|
702
|
+
else receipt_instance
|
|
703
|
+
)
|
|
704
|
+
companion_message[34:38] = source_context
|
|
705
|
+
if layout == "ambiguous-trailing-context":
|
|
706
|
+
companion_message[40:44] = bytes.fromhex("0471ee0e")
|
|
693
707
|
companion = frame(0, 0xBEEF, companion_message)
|
|
694
708
|
|
|
695
709
|
receipt_frame = frame(1, 0xCAFE, bytearray(80))
|
|
@@ -709,6 +723,11 @@ def test_container_companion_discovers_both_known_field_orders(
|
|
|
709
723
|
[companion, receipt_frame], receipt, options
|
|
710
724
|
)
|
|
711
725
|
|
|
726
|
+
if expected_offsets is None:
|
|
727
|
+
assert spec is None
|
|
728
|
+
return
|
|
729
|
+
|
|
730
|
+
expected_context, expected_instance, expected_quantity = expected_offsets
|
|
712
731
|
assert spec is not None
|
|
713
732
|
assert (spec.opcode, spec.length) == (0xBEEF, len(companion_message))
|
|
714
733
|
assert spec.context_offset == expected_context
|
|
@@ -99,7 +99,7 @@ def test_july17_character_state_report_recovers_inventory_and_storage():
|
|
|
99
99
|
container.confidence == "provisional"
|
|
100
100
|
for container in state.inventory.containers
|
|
101
101
|
)
|
|
102
|
-
assert state.inventory.currency("Silver").quantity ==
|
|
102
|
+
assert state.inventory.currency("Silver").quantity == 637_487_451_027
|
|
103
103
|
assert state.inventory.currency(6).quantity == 363
|
|
104
104
|
assert state.inventory.currency("Loyalties").quantity == 17_600
|
|
105
105
|
assert state.inventory.currency(10).quantity == 105_742
|
|
@@ -137,7 +137,7 @@ def test_july17_character_state_report_recovers_inventory_and_storage():
|
|
|
137
137
|
assert "capacity: unavailable" in output
|
|
138
138
|
assert "Main Inventory [0x00, provisional]: 74 item stacks" in output
|
|
139
139
|
assert "2 empty wrappers: unclassified" in output
|
|
140
|
-
assert "Silver:
|
|
140
|
+
assert "Silver: 637,487,451,027" in output
|
|
141
141
|
assert "/192" not in output
|
|
142
142
|
assert "initial login vs character switch" in output
|
|
143
143
|
|
|
@@ -223,7 +223,7 @@ def test_july17_character_switch_classifies_exact_inventory_state():
|
|
|
223
223
|
)
|
|
224
224
|
for balance in inventory.currency_balances
|
|
225
225
|
} == {
|
|
226
|
-
1: ("Silver",
|
|
226
|
+
1: ("Silver", 637_487_451_027, 3, 0x18),
|
|
227
227
|
6: ("Pearl", 363, 0, 0x10),
|
|
228
228
|
7: ("Loyalties", 17_900, 1, 0x10),
|
|
229
229
|
10: ("Crow Coin", 105_742, 0, 0x18),
|
|
@@ -371,6 +371,69 @@ def test_wall_clock_service_releases_complete_frame_behind_idle_gap():
|
|
|
371
371
|
assert manager.tcp_gap_resets == 1
|
|
372
372
|
|
|
373
373
|
|
|
374
|
+
def test_deferred_live_gap_timeout_does_not_skip_a_queued_prefix():
|
|
375
|
+
scanner = _RecordingScanner()
|
|
376
|
+
manager = FlowManager(
|
|
377
|
+
server_ports=(8889,),
|
|
378
|
+
scanner_factory=lambda: scanner,
|
|
379
|
+
defer_gap_timeouts=True,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
_segment(
|
|
383
|
+
manager,
|
|
384
|
+
sequence=99,
|
|
385
|
+
timestamp=100.0,
|
|
386
|
+
syn=True,
|
|
387
|
+
)
|
|
388
|
+
_segment(
|
|
389
|
+
manager,
|
|
390
|
+
sequence=100,
|
|
391
|
+
payload=b"a",
|
|
392
|
+
timestamp=100.0,
|
|
393
|
+
)
|
|
394
|
+
_segment(
|
|
395
|
+
manager,
|
|
396
|
+
sequence=102,
|
|
397
|
+
payload=b"c",
|
|
398
|
+
timestamp=100.1,
|
|
399
|
+
)
|
|
400
|
+
# Processing a later queued packet after the deadline must not recover the
|
|
401
|
+
# gap before its already-queued prefix gets a chance to run.
|
|
402
|
+
_segment(
|
|
403
|
+
manager,
|
|
404
|
+
sequence=103,
|
|
405
|
+
payload=b"d",
|
|
406
|
+
timestamp=102.0,
|
|
407
|
+
)
|
|
408
|
+
_segment(
|
|
409
|
+
manager,
|
|
410
|
+
sequence=101,
|
|
411
|
+
payload=b"b",
|
|
412
|
+
timestamp=102.1,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
assert b"".join(scanner.feeds) == b"abcd"
|
|
416
|
+
assert scanner.resets == 0
|
|
417
|
+
assert manager.tcp_gap_resets == 0
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def test_deferred_live_gap_timeout_still_resets_after_explicit_service():
|
|
421
|
+
scanner = _RecordingScanner()
|
|
422
|
+
manager = FlowManager(
|
|
423
|
+
server_ports=(8889,),
|
|
424
|
+
scanner_factory=lambda: scanner,
|
|
425
|
+
defer_gap_timeouts=True,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
_segment(manager, sequence=99, timestamp=100.0, syn=True)
|
|
429
|
+
_segment(manager, sequence=100, payload=b"a", timestamp=100.0)
|
|
430
|
+
_segment(manager, sequence=102, payload=b"c", timestamp=100.1)
|
|
431
|
+
|
|
432
|
+
assert manager.service_gaps(100.1 + GAP_RESET_SECONDS) == 1
|
|
433
|
+
assert b"".join(scanner.feeds) == b"ac"
|
|
434
|
+
assert manager.tcp_gap_resets == 1
|
|
435
|
+
|
|
436
|
+
|
|
374
437
|
@pytest.mark.parametrize("syn_sequence", [1000, 0xFFFFFFF8])
|
|
375
438
|
def test_empty_syn_anchor_reassembles_second_half_first_and_deduplicates(
|
|
376
439
|
syn_sequence: int,
|
|
@@ -884,7 +884,10 @@ def test_candidate_idle_closes_overall_tail_amid_unrelated_traffic() -> None:
|
|
|
884
884
|
# the global packet queue busy.
|
|
885
885
|
assert not tracker.complete
|
|
886
886
|
assert tracker._last_candidate_activity_at is not None
|
|
887
|
-
|
|
887
|
+
# This monotonic-clock value reproduces a Linux float boundary where
|
|
888
|
+
# ``(last_candidate + 1.5) - last_candidate`` rounds just below 1.5.
|
|
889
|
+
last_candidate = 63.021708486
|
|
890
|
+
tracker._last_candidate_activity_at = last_candidate
|
|
888
891
|
assert not tracker.service_candidate_idle(
|
|
889
892
|
last_candidate + LIVE_CANDIDATE_IDLE_SECONDS - 0.001
|
|
890
893
|
)
|
|
@@ -598,16 +598,7 @@ def test_live_session_auto_stops_when_candidate_idle_confirms_final_window(
|
|
|
598
598
|
assert session.stop_reason == "complete-snapshot"
|
|
599
599
|
|
|
600
600
|
|
|
601
|
-
def
|
|
602
|
-
class RecordingCollector:
|
|
603
|
-
def __init__(self) -> None:
|
|
604
|
-
self.service_calls = 0
|
|
605
|
-
self.tcp_gap_resets = 0
|
|
606
|
-
|
|
607
|
-
def service_gaps(self, _now: float) -> int:
|
|
608
|
-
self.service_calls += 1
|
|
609
|
-
return 0
|
|
610
|
-
|
|
601
|
+
def test_drained_candidate_clock_waits_for_decoder_backlog() -> None:
|
|
611
602
|
class RecordingTracker:
|
|
612
603
|
complete = False
|
|
613
604
|
|
|
@@ -619,29 +610,26 @@ def test_drained_clock_service_waits_for_decoder_backlog() -> None:
|
|
|
619
610
|
return False
|
|
620
611
|
|
|
621
612
|
session = LiveSolareSession()
|
|
622
|
-
collector = RecordingCollector()
|
|
623
613
|
tracker = RecordingTracker()
|
|
624
|
-
session._collector = collector # type: ignore[assignment]
|
|
625
614
|
session._tracker = tracker # type: ignore[assignment]
|
|
626
615
|
session._packet_queue.put_nowait(object())
|
|
627
616
|
|
|
628
617
|
session._service_drained_clocks()
|
|
629
618
|
|
|
630
|
-
assert collector.service_calls == 0
|
|
631
619
|
assert tracker.service_calls == 0
|
|
632
620
|
|
|
633
621
|
session._packet_queue.get_nowait()
|
|
634
622
|
session._service_drained_clocks()
|
|
635
623
|
|
|
636
|
-
assert collector.service_calls == 1
|
|
637
624
|
assert tracker.service_calls == 1
|
|
638
625
|
|
|
639
626
|
|
|
640
|
-
def
|
|
641
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
642
|
-
) -> None:
|
|
627
|
+
def test_live_clock_leaves_quiet_tcp_gap_for_finalization() -> None:
|
|
643
628
|
session = LiveSolareSession()
|
|
644
|
-
collector = session_module.SolareFrameCollector(
|
|
629
|
+
collector = session_module.SolareFrameCollector(
|
|
630
|
+
(8889,),
|
|
631
|
+
defer_gap_timeouts=True,
|
|
632
|
+
)
|
|
645
633
|
session._collector = collector
|
|
646
634
|
|
|
647
635
|
collector.process_tcp_segment(
|
|
@@ -663,14 +651,14 @@ def test_drained_clock_services_quiet_tcp_gap_and_warns_fail_closed(
|
|
|
663
651
|
payload=b"x",
|
|
664
652
|
timestamp=100.1,
|
|
665
653
|
)
|
|
666
|
-
monkeypatch.setattr(session_module.time, "time", lambda: 102.0)
|
|
667
|
-
|
|
668
654
|
session._service_drained_clocks()
|
|
669
655
|
|
|
656
|
+
assert collector.tcp_gap_resets == 0
|
|
657
|
+
assert session._update_queue.empty()
|
|
658
|
+
|
|
659
|
+
collector.finish()
|
|
660
|
+
|
|
670
661
|
assert collector.tcp_gap_resets == 1
|
|
671
|
-
warning = session._update_queue.get_nowait()
|
|
672
|
-
assert warning.kind is SolareUpdateKind.WARNING
|
|
673
|
-
assert "remain fail-closed" in warning.message
|
|
674
662
|
|
|
675
663
|
|
|
676
664
|
def test_live_session_refuses_to_overwrite_existing_capture(
|
|
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.2 → bdo_toolkit-1.0.3}/tests/test_storage_unknown_destination_calibration.py
RENAMED
|
File without changes
|
|
File without changes
|