bdo-toolkit 1.0.0__py3-none-any.whl
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/__init__.py +87 -0
- bdo_toolkit/_async_sessions.py +651 -0
- bdo_toolkit/_capture_backend.py +194 -0
- bdo_toolkit/_capture_options.py +68 -0
- bdo_toolkit/_capture_runtime.py +626 -0
- bdo_toolkit/_deposit_origin.py +1599 -0
- bdo_toolkit/_engine.py +327 -0
- bdo_toolkit/_framing.py +904 -0
- bdo_toolkit/_profile_runtime.py +157 -0
- bdo_toolkit/_protocol.py +386 -0
- bdo_toolkit/_reassembly.py +654 -0
- bdo_toolkit/_specs.py +285 -0
- bdo_toolkit/_storage_destination_validation.py +167 -0
- bdo_toolkit/_storage_hydration.py +241 -0
- bdo_toolkit/_version.py +3 -0
- bdo_toolkit/calibration.py +3223 -0
- bdo_toolkit/capture.py +1713 -0
- bdo_toolkit/character_state.py +3506 -0
- bdo_toolkit/cli.py +948 -0
- bdo_toolkit/diagnostics.py +51 -0
- bdo_toolkit/events.py +214 -0
- bdo_toolkit/filters.py +105 -0
- bdo_toolkit/item_state.py +48 -0
- bdo_toolkit/origin_learning.py +779 -0
- bdo_toolkit/profiles.py +370 -0
- bdo_toolkit/py.typed +1 -0
- bdo_toolkit/remote_profiles.py +358 -0
- bdo_toolkit/solare/__init__.py +50 -0
- bdo_toolkit/solare/_constants.py +94 -0
- bdo_toolkit/solare/_detail_learning.py +1437 -0
- bdo_toolkit/solare/_details.py +796 -0
- bdo_toolkit/solare/_discovery.py +1212 -0
- bdo_toolkit/solare/_live_tracker.py +472 -0
- bdo_toolkit/solare/_replay_capture.py +182 -0
- bdo_toolkit/solare/_result.py +441 -0
- bdo_toolkit/solare/_scanner.py +203 -0
- bdo_toolkit/solare/_validation.py +11 -0
- bdo_toolkit/solare/async_session.py +444 -0
- bdo_toolkit/solare/models.py +806 -0
- bdo_toolkit/solare/replay.py +62 -0
- bdo_toolkit/solare/session.py +1051 -0
- bdo_toolkit/writers.py +30 -0
- bdo_toolkit-1.0.0.dist-info/METADATA +143 -0
- bdo_toolkit-1.0.0.dist-info/RECORD +48 -0
- bdo_toolkit-1.0.0.dist-info/WHEEL +5 -0
- bdo_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
- bdo_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- bdo_toolkit-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
"""Turn opcode-agnostic structural discovery into immutable public models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from bdo_toolkit._protocol import BDOFrame
|
|
9
|
+
|
|
10
|
+
from ._constants import class_name
|
|
11
|
+
from ._detail_learning import learn_unknown_solare_details
|
|
12
|
+
from ._details import (
|
|
13
|
+
_overall_layout_for,
|
|
14
|
+
_rich_layout_for,
|
|
15
|
+
decode_solare_details,
|
|
16
|
+
decode_solare_overall_details,
|
|
17
|
+
)
|
|
18
|
+
from ._discovery import (
|
|
19
|
+
DiscoveredSolareFamily,
|
|
20
|
+
SolareDiscoveryResult,
|
|
21
|
+
discover_solare,
|
|
22
|
+
family_frames,
|
|
23
|
+
)
|
|
24
|
+
from ._validation import validate_retain_raw_extensions
|
|
25
|
+
from .models import (
|
|
26
|
+
SolareCaptureHealth,
|
|
27
|
+
SolareCaptureResult,
|
|
28
|
+
SolareClass,
|
|
29
|
+
SolareDetectionStatus,
|
|
30
|
+
SolareEvidence,
|
|
31
|
+
SolareFamilyLayout,
|
|
32
|
+
SolareLeaderboardSnapshot,
|
|
33
|
+
SolareOverallEntry,
|
|
34
|
+
SolarePlayer,
|
|
35
|
+
solare_snapshot_id,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _family_layout(
|
|
40
|
+
family: Optional[DiscoveredSolareFamily],
|
|
41
|
+
*,
|
|
42
|
+
detail_layout_id: Optional[str] = None,
|
|
43
|
+
) -> Optional[SolareFamilyLayout]:
|
|
44
|
+
if family is None:
|
|
45
|
+
return None
|
|
46
|
+
return SolareFamilyLayout(
|
|
47
|
+
role=family.role,
|
|
48
|
+
opcode=family.opcode,
|
|
49
|
+
message_length=family.frame_length,
|
|
50
|
+
message_count=family.frame_count,
|
|
51
|
+
record_stride=family.record_stride,
|
|
52
|
+
name_offset=family.name_offset,
|
|
53
|
+
rank_offset=family.rank_offset,
|
|
54
|
+
class_offset=family.class_offset,
|
|
55
|
+
detail_layout_id=detail_layout_id,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _ranking_players(rich: DiscoveredSolareFamily) -> tuple[SolarePlayer, ...]:
|
|
60
|
+
players = (
|
|
61
|
+
SolarePlayer(
|
|
62
|
+
name=name,
|
|
63
|
+
global_rank=rank,
|
|
64
|
+
primary_class=SolareClass(code=class_code, name=class_name(class_code)),
|
|
65
|
+
)
|
|
66
|
+
for name, rank, class_code in zip(
|
|
67
|
+
rich.names,
|
|
68
|
+
rich.ranks,
|
|
69
|
+
rich.class_codes,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return tuple(sorted(players, key=lambda player: player.global_rank))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _overall_entries(
|
|
76
|
+
overall: DiscoveredSolareFamily,
|
|
77
|
+
) -> tuple[SolareOverallEntry, ...]:
|
|
78
|
+
return tuple(
|
|
79
|
+
SolareOverallEntry(
|
|
80
|
+
name=name,
|
|
81
|
+
global_rank=rank,
|
|
82
|
+
)
|
|
83
|
+
for name, rank in zip(overall.names, overall.ranks)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _details_match_discovery(
|
|
88
|
+
players: tuple[SolarePlayer, ...],
|
|
89
|
+
rich: DiscoveredSolareFamily,
|
|
90
|
+
) -> bool:
|
|
91
|
+
discovered = tuple(
|
|
92
|
+
sorted(
|
|
93
|
+
zip(rich.ranks, rich.names, rich.class_codes),
|
|
94
|
+
key=lambda row: row[0],
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
decoded = tuple(
|
|
98
|
+
(player.global_rank, player.name, player.primary_class.code)
|
|
99
|
+
for player in sorted(players, key=lambda player: player.global_rank)
|
|
100
|
+
)
|
|
101
|
+
return decoded == discovered
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _overall_details_match_discovery(
|
|
105
|
+
entries: tuple[SolareOverallEntry, ...],
|
|
106
|
+
overall: DiscoveredSolareFamily,
|
|
107
|
+
) -> bool:
|
|
108
|
+
discovered = tuple(
|
|
109
|
+
sorted(
|
|
110
|
+
zip(overall.ranks, overall.names),
|
|
111
|
+
key=lambda row: row[0],
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
decoded = tuple(
|
|
115
|
+
(entry.global_rank, entry.name)
|
|
116
|
+
for entry in sorted(entries, key=lambda entry: entry.global_rank)
|
|
117
|
+
)
|
|
118
|
+
return decoded == discovered
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _message_for_status(status: SolareDetectionStatus) -> str:
|
|
122
|
+
messages = {
|
|
123
|
+
SolareDetectionStatus.COMPLETE: (
|
|
124
|
+
"structurally confirmed Solare leaderboard without a known opcode"
|
|
125
|
+
),
|
|
126
|
+
SolareDetectionStatus.DETECTED_INCOMPLETE: (
|
|
127
|
+
"leaderboard traffic was recognized, but the complete snapshot "
|
|
128
|
+
"was not captured"
|
|
129
|
+
),
|
|
130
|
+
SolareDetectionStatus.RICH_CANDIDATE: (
|
|
131
|
+
"a complete class-balanced ranked table was found, but the "
|
|
132
|
+
"independent overall top-100 cross-check was not captured"
|
|
133
|
+
),
|
|
134
|
+
SolareDetectionStatus.RANKED_PARTIAL: (
|
|
135
|
+
"ranked leaderboard structure was found, but the class-balanced "
|
|
136
|
+
"table and overall cross-check were incomplete"
|
|
137
|
+
),
|
|
138
|
+
SolareDetectionStatus.MENU_CONTEXT: (
|
|
139
|
+
"Solare menu/history-like traffic was found, not a leaderboard"
|
|
140
|
+
),
|
|
141
|
+
SolareDetectionStatus.INCONCLUSIVE: (
|
|
142
|
+
"no complete structural Solare signature was found"
|
|
143
|
+
),
|
|
144
|
+
SolareDetectionStatus.NO_TRAFFIC: (
|
|
145
|
+
"no inbound payload from the selected game-server ports was found"
|
|
146
|
+
),
|
|
147
|
+
}
|
|
148
|
+
return messages[status]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resource_loss_summary(health: SolareCaptureHealth) -> Optional[str]:
|
|
152
|
+
"""Describe bounded live-pipeline losses that invalidate acquisition.
|
|
153
|
+
|
|
154
|
+
This deliberately covers only losses introduced by the toolkit's bounded
|
|
155
|
+
packet/reassembly pipeline. Pcap drops and TCP gaps keep their broader
|
|
156
|
+
capture-integrity wording below.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
losses: list[str] = []
|
|
160
|
+
if health.packet_queue_overflows:
|
|
161
|
+
count = health.packet_queue_overflows
|
|
162
|
+
losses.append(
|
|
163
|
+
"packet queue overflow discarded "
|
|
164
|
+
f"{count} captured packet{'s' if count != 1 else ''} before decoding"
|
|
165
|
+
)
|
|
166
|
+
if health.flow_state_evictions:
|
|
167
|
+
count = health.flow_state_evictions
|
|
168
|
+
losses.append(
|
|
169
|
+
"forced flow-state eviction removed "
|
|
170
|
+
f"{count} active TCP reassembly state{'s' if count != 1 else ''}"
|
|
171
|
+
)
|
|
172
|
+
if not losses:
|
|
173
|
+
return None
|
|
174
|
+
if len(losses) == 1:
|
|
175
|
+
return losses[0]
|
|
176
|
+
return f"{losses[0]} and {losses[1]}"
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _capture_loss_summary(health: SolareCaptureHealth) -> Optional[str]:
|
|
180
|
+
"""Name every observed acquisition loss, including pre-confirmation gaps."""
|
|
181
|
+
|
|
182
|
+
losses: list[str] = []
|
|
183
|
+
resource_loss = _resource_loss_summary(health)
|
|
184
|
+
if resource_loss is not None:
|
|
185
|
+
losses.append(resource_loss)
|
|
186
|
+
if health.tcp_gap_resets:
|
|
187
|
+
count = health.tcp_gap_resets
|
|
188
|
+
losses.append(
|
|
189
|
+
"TCP reassembly reset "
|
|
190
|
+
f"{count} time{'s' if count != 1 else ''} after an unresolved "
|
|
191
|
+
"sequence gap"
|
|
192
|
+
)
|
|
193
|
+
if health.pcap_dropped not in (None, 0):
|
|
194
|
+
backend_drop_count = health.pcap_dropped
|
|
195
|
+
assert backend_drop_count is not None
|
|
196
|
+
losses.append(
|
|
197
|
+
"the capture backend reported "
|
|
198
|
+
f"{backend_drop_count} dropped "
|
|
199
|
+
f"packet{'s' if backend_drop_count != 1 else ''}"
|
|
200
|
+
)
|
|
201
|
+
if health.pcap_interface_dropped not in (None, 0):
|
|
202
|
+
interface_drop_count = health.pcap_interface_dropped
|
|
203
|
+
assert interface_drop_count is not None
|
|
204
|
+
losses.append(
|
|
205
|
+
"the capture interface reported "
|
|
206
|
+
f"{interface_drop_count} dropped "
|
|
207
|
+
f"packet{'s' if interface_drop_count != 1 else ''}"
|
|
208
|
+
)
|
|
209
|
+
if not losses:
|
|
210
|
+
return None
|
|
211
|
+
if len(losses) == 1:
|
|
212
|
+
return losses[0]
|
|
213
|
+
return "; ".join(losses[:-1]) + f"; and {losses[-1]}"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _noncomplete_message(
|
|
217
|
+
status: SolareDetectionStatus,
|
|
218
|
+
health: SolareCaptureHealth,
|
|
219
|
+
*,
|
|
220
|
+
integrity_rejected: bool,
|
|
221
|
+
) -> str:
|
|
222
|
+
capture_loss = _capture_loss_summary(health)
|
|
223
|
+
if capture_loss is not None:
|
|
224
|
+
retry = (
|
|
225
|
+
"Retry after reducing capture load"
|
|
226
|
+
if _resource_loss_summary(health) is not None
|
|
227
|
+
else "Retry with a fresh capture"
|
|
228
|
+
)
|
|
229
|
+
if integrity_rejected:
|
|
230
|
+
return (
|
|
231
|
+
"the structural tables matched, but acquisition integrity was "
|
|
232
|
+
f"lost because {capture_loss}; snapshot publication was "
|
|
233
|
+
f"withheld. {retry}"
|
|
234
|
+
)
|
|
235
|
+
return (
|
|
236
|
+
f"{_message_for_status(status)}; acquisition integrity was lost "
|
|
237
|
+
f"because {capture_loss}. {retry}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if integrity_rejected:
|
|
241
|
+
return (
|
|
242
|
+
"the structural tables matched, but capture gaps or reported "
|
|
243
|
+
"packet drops made acquisition integrity uncertain; snapshot "
|
|
244
|
+
"publication was withheld"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
message = _message_for_status(status)
|
|
248
|
+
if health.candidate_history_rolled_over and health.capture_is_clean:
|
|
249
|
+
return (
|
|
250
|
+
f"{message}; the bounded candidate history rolled over after older "
|
|
251
|
+
"frames were evaluated and discarded. Retry while opening the "
|
|
252
|
+
"leaderboard promptly"
|
|
253
|
+
)
|
|
254
|
+
return message
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _evidence(
|
|
258
|
+
discovery: SolareDiscoveryResult,
|
|
259
|
+
health: SolareCaptureHealth,
|
|
260
|
+
*,
|
|
261
|
+
class_detail_layout_id: Optional[str] = None,
|
|
262
|
+
overall_detail_layout_id: Optional[str] = None,
|
|
263
|
+
) -> SolareEvidence:
|
|
264
|
+
ranked = discovery.ranked_candidate
|
|
265
|
+
overall = discovery.overall or discovery.partial_overall
|
|
266
|
+
rich = discovery.rich
|
|
267
|
+
return SolareEvidence(
|
|
268
|
+
scanned_messages=discovery.scanned_frames,
|
|
269
|
+
candidate_families=discovery.candidate_families,
|
|
270
|
+
ranked_players=ranked.record_count if ranked is not None else 0,
|
|
271
|
+
class_group_counts=rich.class_counts if rich is not None else (),
|
|
272
|
+
overall_players=overall.record_count if overall is not None else 0,
|
|
273
|
+
exact_cross_check=discovery.exact_cross_check,
|
|
274
|
+
rich_layout=_family_layout(
|
|
275
|
+
rich,
|
|
276
|
+
detail_layout_id=class_detail_layout_id,
|
|
277
|
+
),
|
|
278
|
+
overall_layout=_family_layout(
|
|
279
|
+
overall,
|
|
280
|
+
detail_layout_id=overall_detail_layout_id,
|
|
281
|
+
),
|
|
282
|
+
health=health,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def build_solare_result(
|
|
287
|
+
frames: Iterable[BDOFrame],
|
|
288
|
+
health: SolareCaptureHealth,
|
|
289
|
+
*,
|
|
290
|
+
retain_raw_extensions: bool = False,
|
|
291
|
+
_discovery: Optional[SolareDiscoveryResult] = None,
|
|
292
|
+
) -> SolareCaptureResult:
|
|
293
|
+
"""Classify collected generic frames and construct a fail-closed result.
|
|
294
|
+
|
|
295
|
+
A snapshot is returned only after the rich 620-player class table and an
|
|
296
|
+
independent complete overall top 100 agree on every shared rank and name.
|
|
297
|
+
Deeper detail decoding is optional: an unfamiliar geometry may use bounded
|
|
298
|
+
ephemeral inference after confirmation, while every unsupported or
|
|
299
|
+
ambiguous capability is still omitted from the ranking snapshot.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
retain_raw_extensions = validate_retain_raw_extensions(
|
|
303
|
+
retain_raw_extensions
|
|
304
|
+
)
|
|
305
|
+
frame_tuple = tuple(frames)
|
|
306
|
+
discovery = _discovery or discover_solare(frame_tuple)
|
|
307
|
+
integrity_rejected = discovery.confirmed and not health.capture_is_clean
|
|
308
|
+
if not frame_tuple and health.payload_segments == 0:
|
|
309
|
+
status = SolareDetectionStatus.NO_TRAFFIC
|
|
310
|
+
elif integrity_rejected:
|
|
311
|
+
status = SolareDetectionStatus.DETECTED_INCOMPLETE
|
|
312
|
+
else:
|
|
313
|
+
status = SolareDetectionStatus(discovery.status)
|
|
314
|
+
|
|
315
|
+
if status is not SolareDetectionStatus.COMPLETE:
|
|
316
|
+
evidence = _evidence(discovery, health)
|
|
317
|
+
message = _noncomplete_message(
|
|
318
|
+
status,
|
|
319
|
+
health,
|
|
320
|
+
integrity_rejected=integrity_rejected,
|
|
321
|
+
)
|
|
322
|
+
return SolareCaptureResult(
|
|
323
|
+
status=status,
|
|
324
|
+
evidence=evidence,
|
|
325
|
+
message=message,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
rich = discovery.rich
|
|
329
|
+
overall = discovery.overall
|
|
330
|
+
assert rich is not None
|
|
331
|
+
assert overall is not None
|
|
332
|
+
|
|
333
|
+
players = _ranking_players(rich)
|
|
334
|
+
overall_entries = _overall_entries(overall)
|
|
335
|
+
class_table_capabilities = {"rankings"}
|
|
336
|
+
overall_capabilities = {"rankings"}
|
|
337
|
+
class_detail_layout_id: Optional[str] = None
|
|
338
|
+
overall_detail_layout_id: Optional[str] = None
|
|
339
|
+
rich_frames = family_frames(frame_tuple, rich)
|
|
340
|
+
overall_frames = family_frames(frame_tuple, overall)
|
|
341
|
+
class_details = decode_solare_details(
|
|
342
|
+
rich_frames,
|
|
343
|
+
rich,
|
|
344
|
+
retain_raw_extensions=retain_raw_extensions,
|
|
345
|
+
)
|
|
346
|
+
overall_details = decode_solare_overall_details(
|
|
347
|
+
overall_frames,
|
|
348
|
+
overall,
|
|
349
|
+
retain_raw_extensions=retain_raw_extensions,
|
|
350
|
+
)
|
|
351
|
+
learn_rich = _rich_layout_for(rich) is None
|
|
352
|
+
learn_overall = _overall_layout_for(overall) is None
|
|
353
|
+
if learn_rich or learn_overall:
|
|
354
|
+
known_rich_elos: Optional[dict[str, int]] = None
|
|
355
|
+
if (
|
|
356
|
+
not learn_rich
|
|
357
|
+
and class_details is not None
|
|
358
|
+
and "elo" in class_details.capabilities
|
|
359
|
+
and all(player.elo is not None for player in class_details.players)
|
|
360
|
+
):
|
|
361
|
+
known_rich_elos = {
|
|
362
|
+
player.name: player.elo
|
|
363
|
+
for player in class_details.players
|
|
364
|
+
if player.elo is not None
|
|
365
|
+
}
|
|
366
|
+
known_overall_elos: Optional[dict[str, int]] = None
|
|
367
|
+
if (
|
|
368
|
+
not learn_overall
|
|
369
|
+
and overall_details is not None
|
|
370
|
+
and "elo" in overall_details.capabilities
|
|
371
|
+
and all(entry.elo is not None for entry in overall_details.entries)
|
|
372
|
+
):
|
|
373
|
+
known_overall_elos = {
|
|
374
|
+
entry.name: entry.elo
|
|
375
|
+
for entry in overall_details.entries
|
|
376
|
+
if entry.elo is not None
|
|
377
|
+
}
|
|
378
|
+
learned_class, learned_overall = learn_unknown_solare_details(
|
|
379
|
+
rich_frames,
|
|
380
|
+
rich,
|
|
381
|
+
overall_frames,
|
|
382
|
+
overall,
|
|
383
|
+
learn_rich=learn_rich,
|
|
384
|
+
learn_overall=learn_overall,
|
|
385
|
+
retain_raw_extensions=retain_raw_extensions,
|
|
386
|
+
known_rich_elos=known_rich_elos,
|
|
387
|
+
known_overall_elos=known_overall_elos,
|
|
388
|
+
)
|
|
389
|
+
if class_details is None and learn_rich:
|
|
390
|
+
class_details = learned_class
|
|
391
|
+
if overall_details is None and learn_overall:
|
|
392
|
+
overall_details = learned_overall
|
|
393
|
+
|
|
394
|
+
if class_details is not None and _details_match_discovery(
|
|
395
|
+
class_details.players,
|
|
396
|
+
rich,
|
|
397
|
+
):
|
|
398
|
+
players = tuple(
|
|
399
|
+
sorted(class_details.players, key=lambda player: player.global_rank)
|
|
400
|
+
)
|
|
401
|
+
class_detail_layout_id = class_details.layout_id
|
|
402
|
+
class_table_capabilities.update(class_details.capabilities)
|
|
403
|
+
|
|
404
|
+
if overall_details is not None and _overall_details_match_discovery(
|
|
405
|
+
overall_details.entries,
|
|
406
|
+
overall,
|
|
407
|
+
):
|
|
408
|
+
overall_entries = tuple(
|
|
409
|
+
sorted(
|
|
410
|
+
overall_details.entries,
|
|
411
|
+
key=lambda entry: entry.global_rank,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
overall_detail_layout_id = overall_details.layout_id
|
|
415
|
+
overall_capabilities.update(overall_details.capabilities)
|
|
416
|
+
|
|
417
|
+
evidence = _evidence(
|
|
418
|
+
discovery,
|
|
419
|
+
health,
|
|
420
|
+
class_detail_layout_id=class_detail_layout_id,
|
|
421
|
+
overall_detail_layout_id=overall_detail_layout_id,
|
|
422
|
+
)
|
|
423
|
+
snapshot = SolareLeaderboardSnapshot(
|
|
424
|
+
snapshot_id=solare_snapshot_id(
|
|
425
|
+
players,
|
|
426
|
+
overall_top_100=overall_entries,
|
|
427
|
+
),
|
|
428
|
+
observed_at=max(
|
|
429
|
+
frame.context.timestamp for frame in (*rich_frames, *overall_frames)
|
|
430
|
+
),
|
|
431
|
+
players=players,
|
|
432
|
+
overall_top_100=overall_entries,
|
|
433
|
+
class_table_capabilities=frozenset(class_table_capabilities),
|
|
434
|
+
overall_capabilities=frozenset(overall_capabilities),
|
|
435
|
+
)
|
|
436
|
+
return SolareCaptureResult(
|
|
437
|
+
status=status,
|
|
438
|
+
evidence=evidence,
|
|
439
|
+
snapshot=snapshot,
|
|
440
|
+
message=_message_for_status(status),
|
|
441
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Midstream-safe generic BDO framing used by Solare structural discovery."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Optional
|
|
6
|
+
|
|
7
|
+
from bdo_toolkit._protocol import BDOFrame, PacketContext
|
|
8
|
+
|
|
9
|
+
from ._constants import (
|
|
10
|
+
BDO_HEADER_SIZE,
|
|
11
|
+
BDO_MAX_FRAME_SIZE,
|
|
12
|
+
DISCOVERY_MAX_FRAME_LENGTH,
|
|
13
|
+
DISCOVERY_MIN_FRAME_LENGTH,
|
|
14
|
+
DISCOVERY_SYNC_BUFFER_LIMIT,
|
|
15
|
+
DISCOVERY_SYNC_HEADERS,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _u16le(data: bytes | bytearray, offset: int) -> int:
|
|
20
|
+
return int.from_bytes(data[offset : offset + 2], "little")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class SolareDiscoveryStreamScanner:
|
|
24
|
+
"""Emit generic frames without trusting a known Solare opcode.
|
|
25
|
+
|
|
26
|
+
Live capture can attach in the middle of an application message. The
|
|
27
|
+
scanner therefore requires either two equal large-family headers or three
|
|
28
|
+
consecutive generic flag-zero headers before it declares synchronization.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
callback: Callable[[BDOFrame], None],
|
|
34
|
+
reset_callback: Optional[Callable[[], None]] = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._callback = callback
|
|
37
|
+
self._reset_callback = reset_callback
|
|
38
|
+
self._buffer = bytearray()
|
|
39
|
+
self._buffer_start_sequence: Optional[int] = None
|
|
40
|
+
self._locked = False
|
|
41
|
+
self._frame_index = 0
|
|
42
|
+
|
|
43
|
+
def reset(self) -> None:
|
|
44
|
+
self._buffer.clear()
|
|
45
|
+
self._buffer_start_sequence = None
|
|
46
|
+
self._locked = False
|
|
47
|
+
if self._reset_callback is not None:
|
|
48
|
+
self._reset_callback()
|
|
49
|
+
|
|
50
|
+
def can_anchor_at_start(self, data: bytes) -> bool:
|
|
51
|
+
"""Return whether byte zero already satisfies the discovery sync proof."""
|
|
52
|
+
|
|
53
|
+
def header_length(offset: int) -> Optional[int]:
|
|
54
|
+
if offset + BDO_HEADER_SIZE > len(data):
|
|
55
|
+
return None
|
|
56
|
+
message_length = _u16le(data, offset)
|
|
57
|
+
if not BDO_HEADER_SIZE <= message_length <= BDO_MAX_FRAME_SIZE:
|
|
58
|
+
return -1
|
|
59
|
+
if data[offset + 2] != 0:
|
|
60
|
+
return -1
|
|
61
|
+
return message_length
|
|
62
|
+
|
|
63
|
+
position = 0
|
|
64
|
+
generic_chain = True
|
|
65
|
+
for index in range(DISCOVERY_SYNC_HEADERS):
|
|
66
|
+
message_length = header_length(position)
|
|
67
|
+
if message_length is None or message_length < BDO_HEADER_SIZE:
|
|
68
|
+
generic_chain = False
|
|
69
|
+
break
|
|
70
|
+
if index != DISCOVERY_SYNC_HEADERS - 1:
|
|
71
|
+
if position + message_length > len(data):
|
|
72
|
+
generic_chain = False
|
|
73
|
+
break
|
|
74
|
+
position += message_length
|
|
75
|
+
if generic_chain:
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
first_length = header_length(0)
|
|
79
|
+
if first_length is None or not (
|
|
80
|
+
DISCOVERY_MIN_FRAME_LENGTH
|
|
81
|
+
<= first_length
|
|
82
|
+
<= DISCOVERY_MAX_FRAME_LENGTH
|
|
83
|
+
):
|
|
84
|
+
return False
|
|
85
|
+
second_offset = first_length
|
|
86
|
+
return (
|
|
87
|
+
header_length(second_offset) == first_length
|
|
88
|
+
and second_offset + BDO_HEADER_SIZE <= len(data)
|
|
89
|
+
and _u16le(data, 3) == _u16le(data, second_offset + 3)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def feed(self, data: bytes, context: PacketContext) -> None:
|
|
93
|
+
if not data:
|
|
94
|
+
return
|
|
95
|
+
if not self._buffer:
|
|
96
|
+
self._buffer_start_sequence = context.stream_start
|
|
97
|
+
self._buffer.extend(data)
|
|
98
|
+
self._scan(context)
|
|
99
|
+
|
|
100
|
+
def scan_standalone(self, data: bytes, context: PacketContext) -> None:
|
|
101
|
+
# FlowManager invokes this for overlap/retransmission bytes already
|
|
102
|
+
# represented in the primary stream. Ignoring them prevents duplicate
|
|
103
|
+
# discovery frames.
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
def _discard_prefix(self, byte_count: int) -> None:
|
|
107
|
+
if byte_count <= 0:
|
|
108
|
+
return
|
|
109
|
+
del self._buffer[:byte_count]
|
|
110
|
+
if self._buffer_start_sequence is not None:
|
|
111
|
+
self._buffer_start_sequence += byte_count
|
|
112
|
+
if not self._buffer:
|
|
113
|
+
self._buffer_start_sequence = None
|
|
114
|
+
|
|
115
|
+
def _header_length(self, offset: int) -> Optional[int]:
|
|
116
|
+
if offset + BDO_HEADER_SIZE > len(self._buffer):
|
|
117
|
+
return None
|
|
118
|
+
message_length = _u16le(self._buffer, offset)
|
|
119
|
+
if not BDO_HEADER_SIZE <= message_length <= BDO_MAX_FRAME_SIZE:
|
|
120
|
+
return -1
|
|
121
|
+
if self._buffer[offset + 2] != 0:
|
|
122
|
+
return -1
|
|
123
|
+
return message_length
|
|
124
|
+
|
|
125
|
+
def _has_sync_chain(self, offset: int) -> Optional[bool]:
|
|
126
|
+
position = offset
|
|
127
|
+
for index in range(DISCOVERY_SYNC_HEADERS):
|
|
128
|
+
message_length = self._header_length(position)
|
|
129
|
+
if message_length is None:
|
|
130
|
+
return None
|
|
131
|
+
if message_length < BDO_HEADER_SIZE:
|
|
132
|
+
return False
|
|
133
|
+
if index == DISCOVERY_SYNC_HEADERS - 1:
|
|
134
|
+
return True
|
|
135
|
+
if position + message_length > len(self._buffer):
|
|
136
|
+
return None
|
|
137
|
+
position += message_length
|
|
138
|
+
return True
|
|
139
|
+
|
|
140
|
+
def _find_sync_offset(self) -> Optional[int]:
|
|
141
|
+
final_offset = len(self._buffer) - BDO_HEADER_SIZE
|
|
142
|
+
for offset in range(max(0, final_offset + 1)):
|
|
143
|
+
if self._has_sync_chain(offset) is True:
|
|
144
|
+
return offset
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _find_repeated_large_family_offset(self) -> Optional[int]:
|
|
148
|
+
final_offset = len(self._buffer) - BDO_HEADER_SIZE
|
|
149
|
+
for offset in range(max(0, final_offset + 1)):
|
|
150
|
+
message_length = self._header_length(offset)
|
|
151
|
+
if message_length is None or not (
|
|
152
|
+
DISCOVERY_MIN_FRAME_LENGTH
|
|
153
|
+
<= message_length
|
|
154
|
+
<= DISCOVERY_MAX_FRAME_LENGTH
|
|
155
|
+
):
|
|
156
|
+
continue
|
|
157
|
+
next_offset = offset + message_length
|
|
158
|
+
next_length = self._header_length(next_offset)
|
|
159
|
+
if next_length != message_length:
|
|
160
|
+
continue
|
|
161
|
+
if _u16le(self._buffer, offset + 3) != _u16le(
|
|
162
|
+
self._buffer, next_offset + 3
|
|
163
|
+
):
|
|
164
|
+
continue
|
|
165
|
+
return offset
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
def _scan(self, context: PacketContext) -> None:
|
|
169
|
+
while True:
|
|
170
|
+
if not self._locked:
|
|
171
|
+
sync_offset = self._find_repeated_large_family_offset()
|
|
172
|
+
if sync_offset is None:
|
|
173
|
+
sync_offset = self._find_sync_offset()
|
|
174
|
+
if sync_offset is None:
|
|
175
|
+
if len(self._buffer) > DISCOVERY_SYNC_BUFFER_LIMIT:
|
|
176
|
+
self._discard_prefix(
|
|
177
|
+
len(self._buffer) - DISCOVERY_SYNC_BUFFER_LIMIT
|
|
178
|
+
)
|
|
179
|
+
return
|
|
180
|
+
if sync_offset:
|
|
181
|
+
self._discard_prefix(sync_offset)
|
|
182
|
+
self._locked = True
|
|
183
|
+
|
|
184
|
+
message_length = self._header_length(0)
|
|
185
|
+
if message_length is None:
|
|
186
|
+
return
|
|
187
|
+
if message_length < BDO_HEADER_SIZE:
|
|
188
|
+
self._locked = False
|
|
189
|
+
self._discard_prefix(1)
|
|
190
|
+
continue
|
|
191
|
+
if message_length > len(self._buffer):
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
message = bytes(self._buffer[:message_length])
|
|
195
|
+
frame = BDOFrame(
|
|
196
|
+
index=self._frame_index,
|
|
197
|
+
message=message,
|
|
198
|
+
context=context,
|
|
199
|
+
stream_sequence=self._buffer_start_sequence,
|
|
200
|
+
)
|
|
201
|
+
self._frame_index += 1
|
|
202
|
+
self._callback(frame)
|
|
203
|
+
self._discard_prefix(message_length)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Shared validation for public Arena of Solare acquisition options."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_retain_raw_extensions(value: object) -> bool:
|
|
7
|
+
"""Return a validated raw-retention flag with one consistent contract."""
|
|
8
|
+
|
|
9
|
+
if not isinstance(value, bool):
|
|
10
|
+
raise TypeError("retain_raw_extensions must be a boolean")
|
|
11
|
+
return value
|