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.
Files changed (90) hide show
  1. {bdo_toolkit-1.0.2/src/bdo_toolkit.egg-info → bdo_toolkit-1.0.3}/PKG-INFO +16 -39
  2. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/README.md +15 -38
  3. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_reassembly.py +12 -7
  4. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_version.py +1 -1
  5. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/calibration.py +50 -2
  6. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_live_tracker.py +3 -3
  7. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_replay_capture.py +2 -0
  8. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/session.py +7 -14
  9. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3/src/bdo_toolkit.egg-info}/PKG-INFO +16 -39
  10. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration.py +28 -9
  11. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_character_state.py +3 -3
  12. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_reassembly.py +63 -0
  13. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_replay.py +4 -1
  14. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_session.py +11 -23
  15. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/LICENSE +0 -0
  16. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/pyproject.toml +0 -0
  17. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/setup.cfg +0 -0
  18. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/__init__.py +0 -0
  19. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_async_sessions.py +0 -0
  20. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_backend.py +0 -0
  21. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_options.py +0 -0
  22. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_capture_runtime.py +0 -0
  23. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_deposit_origin.py +0 -0
  24. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_engine.py +0 -0
  25. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_framing.py +0 -0
  26. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_profile_runtime.py +0 -0
  27. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_protocol.py +0 -0
  28. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_specs.py +0 -0
  29. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_storage_destination_validation.py +0 -0
  30. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/_storage_hydration.py +0 -0
  31. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/capture.py +0 -0
  32. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/character_state.py +0 -0
  33. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/cli.py +0 -0
  34. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/diagnostics.py +0 -0
  35. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/events.py +0 -0
  36. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/filters.py +0 -0
  37. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/item_state.py +0 -0
  38. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/origin_learning.py +0 -0
  39. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/profiles.py +0 -0
  40. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/py.typed +0 -0
  41. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/remote_profiles.py +0 -0
  42. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/__init__.py +0 -0
  43. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_constants.py +0 -0
  44. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_detail_learning.py +0 -0
  45. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_details.py +0 -0
  46. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_discovery.py +0 -0
  47. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_result.py +0 -0
  48. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_scanner.py +0 -0
  49. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/_validation.py +0 -0
  50. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/async_session.py +0 -0
  51. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/models.py +0 -0
  52. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/solare/replay.py +0 -0
  53. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit/writers.py +0 -0
  54. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/SOURCES.txt +0 -0
  55. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/dependency_links.txt +0 -0
  56. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/entry_points.txt +0 -0
  57. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/requires.txt +0 -0
  58. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/src/bdo_toolkit.egg-info/top_level.txt +0 -0
  59. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_async_sessions.py +0 -0
  60. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_conveniences.py +0 -0
  61. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_lifecycle.py +0 -0
  62. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_calibration_retention.py +0 -0
  63. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_capture_backend.py +0 -0
  64. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_capture_runtime.py +0 -0
  65. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_cli.py +0 -0
  66. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_decoder_diagnostics.py +0 -0
  67. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_deposit_origin.py +0 -0
  68. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_direction.py +0 -0
  69. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_dynamic_storage_calibration_matrix.py +0 -0
  70. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_engine.py +0 -0
  71. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_examples_and_writers.py +0 -0
  72. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_explicit_profile.py +0 -0
  73. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_fixture_regressions.py +0 -0
  74. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_hardening.py +0 -0
  75. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_initial_load_storage.py +0 -0
  76. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_inventory_snapshots.py +0 -0
  77. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_item_state.py +0 -0
  78. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_live_capture_session.py +0 -0
  79. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_origin_learning.py +0 -0
  80. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_remote_profiles.py +0 -0
  81. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_api.py +0 -0
  82. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_details.py +0 -0
  83. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_learning.py +0 -0
  84. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_learning_adversarial.py +0 -0
  85. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_solare_real_captures.py +0 -0
  86. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_destination_validation.py +0 -0
  87. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_events.py +0 -0
  88. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_hydration_tracker.py +0 -0
  89. {bdo_toolkit-1.0.2 → bdo_toolkit-1.0.3}/tests/test_storage_unknown_destination_calibration.py +0 -0
  90. {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.2
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
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
31
+ [![PyPI version](https://img.shields.io/pypi/v/bdo-toolkit?style=flat-square&label=version)](https://pypi.org/project/bdo-toolkit/)
31
32
  [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
32
33
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
33
34
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](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
- | In-game task | Python result | Status | Guide |
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
- | Watch supported item changes such as loot, gathering, and storage activity | A continuing stream of typed `BDOEvent` objects | Stable | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
54
- | Log in or switch characters | One observational `ItemStateSnapshot` of inventory, known balances, and town storage | Beta | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
55
- | Load the Arena of Solare Leaderboard | One `SolareCaptureResult`; a complete result contains a leaderboard snapshot | Beta | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
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
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
4
+ [![PyPI version](https://img.shields.io/pypi/v/bdo-toolkit?style=flat-square&label=version)](https://pypi.org/project/bdo-toolkit/)
4
5
  [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
5
6
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
6
7
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](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
- | In-game task | Python result | Status | Guide |
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
- | Watch supported item changes such as loot, gathering, and storage activity | A continuing stream of typed `BDOEvent` objects | Stable | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
27
- | Log in or switch characters | One observational `ItemStateSnapshot` of inventory, known balances, and town storage | Beta | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
28
- | Load the Arena of Solare Leaderboard | One `SolareCaptureResult`; a complete result contains a leaderboard snapshot | Beta | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
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
- else:
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 wall clock.
493
+ """Advance pending-gap timers using a caller-supplied clock.
488
494
 
489
- ``FlowManager`` does not own a timer thread. Live capture roots that
490
- require wall-clock release should call this during their normal
491
- poll/tick even when no packets arrived. The return value is the number
492
- of new resets performed during this call; ``tcp_gap_resets`` is
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
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the BDO Toolkit release version."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.0.3"
@@ -2354,9 +2354,29 @@ def _discover_source_container_decrement(
2354
2354
  )
2355
2355
 
2356
2356
  for quantity_offset in quantity_offsets:
2357
- # The current layout places context after instance; older layouts
2358
- # place it before instance. Both put context before the quantity.
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 self._last_candidate_activity_at is None
298
- or now - self._last_candidate_activity_at
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 live-only clocks only after queued packet work is caught up.
713
+ """Advance candidate-idle time only after packet work is caught up.
713
714
 
714
- Wall-clock TCP-gap recovery or candidate-tail closure while packets
715
- remain queued could skip a delayed prefix or a later same-family
716
- frame that Npcap already delivered. Waiting for an empty decode queue
717
- preserves that evidence. The second check protects candidate
718
- finalization from packets enqueued while gap service was running.
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.2
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
  [![CI](https://img.shields.io/github/actions/workflow/status/ychwu/bdo-toolkit/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/ychwu/bdo-toolkit/actions/workflows/ci.yml)
31
+ [![PyPI version](https://img.shields.io/pypi/v/bdo-toolkit?style=flat-square&label=version)](https://pypi.org/project/bdo-toolkit/)
31
32
  [![Python 3.14+](https://img.shields.io/badge/python-3.14%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://github.com/ychwu/bdo-toolkit/blob/main/pyproject.toml)
32
33
  ![Tested on NA/EU](https://img.shields.io/badge/tested-NA%2FEU-5b61a8?style=flat-square)
33
34
  [![Package: Stable](https://img.shields.io/badge/package-stable-2f855a?style=flat-square)](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
- | In-game task | Python result | Status | Guide |
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
- | Watch supported item changes such as loot, gathering, and storage activity | A continuing stream of typed `BDOEvent` objects | Stable | [Item events](https://ychwu.github.io/bdo-toolkit/#item-overview) |
54
- | Log in or switch characters | One observational `ItemStateSnapshot` of inventory, known balances, and town storage | Beta | [Inventory & town storage](https://ychwu.github.io/bdo-toolkit/#item-state-overview) |
55
- | Load the Arena of Solare Leaderboard | One `SolareCaptureResult`; a complete result contains a leaderboard snapshot | Beta | [Arena of Solare](https://ychwu.github.io/bdo-toolkit/#solare-overview) |
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", "expected_context", "expected_instance", "expected_quantity"),
646
+ ("layout", "expected_offsets"),
647
647
  [
648
- ("current", 13, 5, 17),
649
- ("legacy-different-instance", 7, 23, 32),
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 test_container_companion_discovers_both_known_field_orders(
655
+ def test_container_companion_handles_known_field_orders_safely(
653
656
  layout,
654
- expected_context,
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 == "current":
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
- else:
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 == 1_832_291_219
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: 1,832,291,219" in output
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", 1_832_291_219, 3, 0x18),
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
- last_candidate = tracker._last_candidate_activity_at
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 test_drained_clock_service_waits_for_decoder_backlog() -> None:
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 test_drained_clock_services_quiet_tcp_gap_and_warns_fail_closed(
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((8889,))
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