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.
Files changed (48) hide show
  1. bdo_toolkit/__init__.py +87 -0
  2. bdo_toolkit/_async_sessions.py +651 -0
  3. bdo_toolkit/_capture_backend.py +194 -0
  4. bdo_toolkit/_capture_options.py +68 -0
  5. bdo_toolkit/_capture_runtime.py +626 -0
  6. bdo_toolkit/_deposit_origin.py +1599 -0
  7. bdo_toolkit/_engine.py +327 -0
  8. bdo_toolkit/_framing.py +904 -0
  9. bdo_toolkit/_profile_runtime.py +157 -0
  10. bdo_toolkit/_protocol.py +386 -0
  11. bdo_toolkit/_reassembly.py +654 -0
  12. bdo_toolkit/_specs.py +285 -0
  13. bdo_toolkit/_storage_destination_validation.py +167 -0
  14. bdo_toolkit/_storage_hydration.py +241 -0
  15. bdo_toolkit/_version.py +3 -0
  16. bdo_toolkit/calibration.py +3223 -0
  17. bdo_toolkit/capture.py +1713 -0
  18. bdo_toolkit/character_state.py +3506 -0
  19. bdo_toolkit/cli.py +948 -0
  20. bdo_toolkit/diagnostics.py +51 -0
  21. bdo_toolkit/events.py +214 -0
  22. bdo_toolkit/filters.py +105 -0
  23. bdo_toolkit/item_state.py +48 -0
  24. bdo_toolkit/origin_learning.py +779 -0
  25. bdo_toolkit/profiles.py +370 -0
  26. bdo_toolkit/py.typed +1 -0
  27. bdo_toolkit/remote_profiles.py +358 -0
  28. bdo_toolkit/solare/__init__.py +50 -0
  29. bdo_toolkit/solare/_constants.py +94 -0
  30. bdo_toolkit/solare/_detail_learning.py +1437 -0
  31. bdo_toolkit/solare/_details.py +796 -0
  32. bdo_toolkit/solare/_discovery.py +1212 -0
  33. bdo_toolkit/solare/_live_tracker.py +472 -0
  34. bdo_toolkit/solare/_replay_capture.py +182 -0
  35. bdo_toolkit/solare/_result.py +441 -0
  36. bdo_toolkit/solare/_scanner.py +203 -0
  37. bdo_toolkit/solare/_validation.py +11 -0
  38. bdo_toolkit/solare/async_session.py +444 -0
  39. bdo_toolkit/solare/models.py +806 -0
  40. bdo_toolkit/solare/replay.py +62 -0
  41. bdo_toolkit/solare/session.py +1051 -0
  42. bdo_toolkit/writers.py +30 -0
  43. bdo_toolkit-1.0.0.dist-info/METADATA +143 -0
  44. bdo_toolkit-1.0.0.dist-info/RECORD +48 -0
  45. bdo_toolkit-1.0.0.dist-info/WHEEL +5 -0
  46. bdo_toolkit-1.0.0.dist-info/entry_points.txt +2 -0
  47. bdo_toolkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  48. bdo_toolkit-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1437 @@
1
+ """Bounded, fail-closed learning for unregistered Solare detail layouts.
2
+
3
+ Structural discovery has already proved the two leaderboard responses before
4
+ this module is called. The learner works only from those complete response
5
+ records and never trusts an opcode. It does not persist layouts.
6
+
7
+ The class-table and overall-table records remain independent data sources.
8
+ Comparisons between them may disambiguate a candidate, but decoded values are
9
+ always read from the response that owns the public row.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import itertools
16
+ from collections import Counter
17
+ from dataclasses import dataclass
18
+ from typing import Mapping, Optional, Sequence
19
+
20
+ from bdo_toolkit._protocol import BDOFrame
21
+
22
+ from ._constants import EMPTY_CLASS_CODE, EMPTY_SPEC_CODE, class_name
23
+ from ._details import (
24
+ _ADDON_SLOT_SIZE,
25
+ _CLASS_SLOT_COUNT,
26
+ _GEAR_SLOT_SIZE,
27
+ _HISTORY_SLOT_SIZE,
28
+ _MAX_PLAUSIBLE_MATCHES,
29
+ _OverallDetailLayout,
30
+ _RichDetailLayout,
31
+ _decode_history_region,
32
+ _decode_overall_aggregate_performance,
33
+ _decode_performance_slots,
34
+ _read_name,
35
+ _record_locations,
36
+ _u32le,
37
+ SolareDetailDecode,
38
+ SolareOverallDetailDecode,
39
+ )
40
+ from ._discovery import DiscoveredSolareFamily
41
+ from .models import (
42
+ SolareClass,
43
+ SolareClassPerformance,
44
+ SolareOverallEntry,
45
+ SolarePlayer,
46
+ )
47
+
48
+
49
+ _EXPECTED_RICH_RECORDS = 620
50
+ _EXPECTED_RICH_FRAMES = _EXPECTED_RICH_RECORDS // 2
51
+ _EXPECTED_OVERALL_RECORDS = 100
52
+ _EXPECTED_OVERALL_FRAMES = _EXPECTED_OVERALL_RECORDS // 2
53
+ _MIN_ELO_DISTINCT = 20
54
+ _MAX_LEARNED_ELO = 0xFFFF
55
+ _MAX_LEARNED_COUNTER = 0xFFFF
56
+ _MAX_MATCH_CANDIDATES = 2
57
+ _MAX_COUNTER_CANDIDATES = 64
58
+ _MAX_PREFILTER_CANDIDATES = 128
59
+ _MAX_ACTIVE_OUTCOME_PREFILTER_CANDIDATES = 4096
60
+ _MAX_AGGREGATE_VECTORS = 256
61
+ _MAX_LEARNED_RECORD_END = 32 * 1024
62
+ _MIN_HISTORY_DISCRIMINATORS = 5
63
+ _PREFILTER_ROWS = 32
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class _RecordTable:
68
+ family: DiscoveredSolareFamily
69
+ rows: tuple[bytes, ...]
70
+ names: tuple[str, ...]
71
+ ranks: tuple[int, ...]
72
+
73
+ @property
74
+ def record_end(self) -> int:
75
+ return self.family.frame_length - self.family.record_stride
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class _PerformanceOffsets:
80
+ class_offset: int
81
+ spec_offset: int
82
+ matches_offset: int
83
+ wins_offset: int
84
+ draws_offset: int
85
+ losses_offset: int
86
+ history_offset: int
87
+ occupancies: tuple[int, ...]
88
+
89
+
90
+ @dataclass(frozen=True)
91
+ class _RawOffsets:
92
+ gear_offset: int
93
+ addons_offset: int
94
+
95
+
96
+ @dataclass(frozen=True)
97
+ class _AggregateOffsets:
98
+ wins_offset: int
99
+ draws_offset: int
100
+ losses_offset: int
101
+
102
+
103
+ @dataclass(frozen=True)
104
+ class _LearnedTable:
105
+ records: _RecordTable
106
+ elo_offset: Optional[int]
107
+ performance: Optional[_PerformanceOffsets]
108
+ raw: Optional[_RawOffsets] = None
109
+ aggregate: Optional[_AggregateOffsets] = None
110
+
111
+
112
+ def _extract_records(
113
+ frames: Sequence[BDOFrame],
114
+ family: DiscoveredSolareFamily,
115
+ *,
116
+ expected_frames: int,
117
+ expected_records: int,
118
+ ) -> Optional[_RecordTable]:
119
+ record_end = family.frame_length - family.record_stride
120
+ if not 0 < record_end <= _MAX_LEARNED_RECORD_END:
121
+ return None
122
+ locations = _record_locations(
123
+ frames,
124
+ expected_frames=expected_frames,
125
+ frame_length=family.frame_length,
126
+ record_stride=family.record_stride,
127
+ required_record_end=record_end,
128
+ )
129
+ if locations is None or len(locations) != expected_records:
130
+ return None
131
+ rows = tuple(bytes(data[base : base + record_end]) for data, base in locations)
132
+ if len(family.names) != expected_records or len(family.ranks) != expected_records:
133
+ return None
134
+ for ordinal, row in enumerate(rows):
135
+ if (
136
+ _read_name(row, family.name_offset) != family.names[ordinal]
137
+ or _u32le(row, family.rank_offset) != family.ranks[ordinal]
138
+ ):
139
+ return None
140
+ return _RecordTable(family, rows, family.names, family.ranks)
141
+
142
+
143
+ def _class_occupancies(
144
+ rows: Sequence[bytes],
145
+ offset: int,
146
+ ) -> Optional[tuple[int, ...]]:
147
+ if not rows or offset < 0 or offset + _CLASS_SLOT_COUNT > len(rows[0]):
148
+ return None
149
+ occupancies: list[int] = []
150
+ observed_codes: set[int] = set()
151
+ for row in rows:
152
+ codes = tuple(row[offset + slot] for slot in range(_CLASS_SLOT_COUNT))
153
+ occupied = 0
154
+ seen_empty = False
155
+ row_codes: set[int] = set()
156
+ for code in codes:
157
+ if code == EMPTY_CLASS_CODE:
158
+ seen_empty = True
159
+ continue
160
+ if (
161
+ seen_empty
162
+ or class_name(code) is None
163
+ or code in row_codes
164
+ ):
165
+ return None
166
+ row_codes.add(code)
167
+ observed_codes.add(code)
168
+ occupied += 1
169
+ if occupied == 0:
170
+ return None
171
+ occupancies.append(occupied)
172
+ if len(observed_codes) < 5:
173
+ return None
174
+ return tuple(occupancies)
175
+
176
+
177
+ def _learn_overall_class_offset(
178
+ records: _RecordTable,
179
+ ) -> Optional[tuple[int, tuple[int, ...]]]:
180
+ candidates: list[tuple[int, tuple[int, ...]]] = []
181
+ for offset in range(records.record_end - _CLASS_SLOT_COUNT + 1):
182
+ occupancies = _class_occupancies(records.rows, offset)
183
+ if occupancies is not None:
184
+ candidates.append((offset, occupancies))
185
+ if len(candidates) > 1:
186
+ return None
187
+ return candidates[0] if len(candidates) == 1 else None
188
+
189
+
190
+ def _learn_spec_offset(
191
+ rows: Sequence[bytes],
192
+ occupancies: Sequence[int],
193
+ ) -> Optional[int]:
194
+ candidates: list[int] = []
195
+ record_end = len(rows[0])
196
+ for offset in range(record_end - _CLASS_SLOT_COUNT + 1):
197
+ valid = True
198
+ observed_occupied: set[int] = set()
199
+ for row, occupied in zip(rows, occupancies):
200
+ for slot in range(_CLASS_SLOT_COUNT):
201
+ code = row[offset + slot]
202
+ if slot < occupied:
203
+ if code not in (1, 2):
204
+ valid = False
205
+ break
206
+ observed_occupied.add(code)
207
+ elif code != EMPTY_SPEC_CODE:
208
+ valid = False
209
+ break
210
+ if not valid:
211
+ break
212
+ if valid and observed_occupied:
213
+ candidates.append(offset)
214
+ if len(candidates) > 1:
215
+ return None
216
+ return candidates[0] if len(candidates) == 1 else None
217
+
218
+
219
+ def _elo_candidates(records: _RecordTable) -> tuple[int, ...]:
220
+ sample = _sample_ordinals(len(records.rows))
221
+ prefiltered: list[int] = []
222
+ for offset in range(records.record_end - 3):
223
+ sample_values = tuple(
224
+ _u32le(records.rows[ordinal], offset) for ordinal in sample
225
+ )
226
+ if (
227
+ not all(0 < value <= _MAX_LEARNED_ELO for value in sample_values)
228
+ or not all(
229
+ left >= right
230
+ for left, right in zip(sample_values, sample_values[1:])
231
+ )
232
+ ):
233
+ continue
234
+ prefiltered.append(offset)
235
+ if len(prefiltered) > _MAX_PREFILTER_CANDIDATES:
236
+ return ()
237
+
238
+ candidates: list[int] = []
239
+ for offset in prefiltered:
240
+ values = tuple(_u32le(row, offset) for row in records.rows)
241
+ if (
242
+ all(0 < value <= _MAX_LEARNED_ELO for value in values)
243
+ and all(left >= right for left, right in zip(values, values[1:]))
244
+ and len(set(values)) >= _MIN_ELO_DISTINCT
245
+ ):
246
+ candidates.append(offset)
247
+ return tuple(candidates)
248
+
249
+
250
+ def _choose_elo_offsets(
251
+ rich: _RecordTable,
252
+ overall: _RecordTable,
253
+ ) -> tuple[Optional[int], Optional[int]]:
254
+ rich_candidates = _elo_candidates(rich)
255
+ overall_candidates = _elo_candidates(overall)
256
+
257
+ rich_ordinals = {name: ordinal for ordinal, name in enumerate(rich.names)}
258
+ shared = tuple(name for name in overall.names if name in rich_ordinals)
259
+ if len(shared) < 20:
260
+ return None, None
261
+ overall_ordinals = {name: ordinal for ordinal, name in enumerate(overall.names)}
262
+ pairs: list[tuple[int, int]] = []
263
+ for rich_offset in rich_candidates:
264
+ for overall_offset in overall_candidates:
265
+ if all(
266
+ _u32le(rich.rows[rich_ordinals[name]], rich_offset)
267
+ == _u32le(overall.rows[overall_ordinals[name]], overall_offset)
268
+ for name in shared
269
+ ):
270
+ pairs.append((rich_offset, overall_offset))
271
+ if len(pairs) == 1:
272
+ return pairs[0]
273
+ return None, None
274
+
275
+
276
+ def _choose_anchored_elo_offset(
277
+ records: _RecordTable,
278
+ authoritative_by_name: Optional[Mapping[str, int]],
279
+ ) -> Optional[int]:
280
+ if authoritative_by_name is None:
281
+ return None
282
+ ordinals = {name: ordinal for ordinal, name in enumerate(records.names)}
283
+ shared = tuple(
284
+ name for name in authoritative_by_name if name in ordinals
285
+ )
286
+ if len(shared) < 20:
287
+ return None
288
+ candidates = tuple(
289
+ offset
290
+ for offset in _elo_candidates(records)
291
+ if all(
292
+ _u32le(records.rows[ordinals[name]], offset)
293
+ == authoritative_by_name[name]
294
+ for name in shared
295
+ )
296
+ )
297
+ return candidates[0] if len(candidates) == 1 else None
298
+
299
+
300
+ def _counter_vector(rows: Sequence[bytes], offset: int) -> tuple[int, ...]:
301
+ return tuple(
302
+ _u32le(row, offset + slot * 4)
303
+ for row in rows
304
+ for slot in range(_CLASS_SLOT_COUNT)
305
+ )
306
+
307
+
308
+ def _sample_ordinals(
309
+ count: int,
310
+ occupancies: Optional[Sequence[int]] = None,
311
+ ) -> tuple[int, ...]:
312
+ if count <= _PREFILTER_ROWS:
313
+ return tuple(range(count))
314
+ selected = {
315
+ round(index * (count - 1) / (_PREFILTER_ROWS - 1))
316
+ for index in range(_PREFILTER_ROWS)
317
+ }
318
+ if occupancies is not None:
319
+ for occupied in range(1, _CLASS_SLOT_COUNT + 1):
320
+ matching = (
321
+ ordinal
322
+ for ordinal, value in enumerate(occupancies)
323
+ if value == occupied
324
+ )
325
+ selected.update(itertools.islice(matching, 4))
326
+ return tuple(sorted(selected))
327
+
328
+
329
+ def _matches_offset_valid(
330
+ rows: Sequence[bytes],
331
+ occupancies: Sequence[int],
332
+ offset: int,
333
+ ordinals: Sequence[int],
334
+ ) -> bool:
335
+ for ordinal in ordinals:
336
+ row = rows[ordinal]
337
+ occupied = occupancies[ordinal]
338
+ for slot in range(_CLASS_SLOT_COUNT):
339
+ value = _u32le(row, offset + slot * 4)
340
+ if slot < occupied:
341
+ if not 0 < value <= _MAX_LEARNED_COUNTER:
342
+ return False
343
+ elif value != 0:
344
+ return False
345
+ return True
346
+
347
+
348
+ def _matches_candidates(
349
+ rows: Sequence[bytes],
350
+ occupancies: Sequence[int],
351
+ ) -> tuple[tuple[int, tuple[int, ...]], ...]:
352
+ sample = _sample_ordinals(len(rows), occupancies)
353
+ prefiltered: list[int] = []
354
+ for offset in range(len(rows[0]) - 11):
355
+ if _matches_offset_valid(rows, occupancies, offset, sample):
356
+ prefiltered.append(offset)
357
+ if len(prefiltered) > _MAX_PREFILTER_CANDIDATES:
358
+ return ()
359
+
360
+ candidates: list[tuple[int, tuple[int, ...]]] = []
361
+ all_ordinals = tuple(range(len(rows)))
362
+ for offset in prefiltered:
363
+ if not _matches_offset_valid(
364
+ rows,
365
+ occupancies,
366
+ offset,
367
+ all_ordinals,
368
+ ):
369
+ continue
370
+ vector = _counter_vector(rows, offset)
371
+ candidates.append((offset, vector))
372
+ # Every match candidate requires a separate bounded scan for its
373
+ # component columns. More than this small set is ambiguous evidence
374
+ # as well as disproportionate work, so fail closed before expanding
375
+ # the search.
376
+ if len(candidates) > _MAX_MATCH_CANDIDATES:
377
+ return ()
378
+ return tuple(candidates)
379
+
380
+
381
+ def _outcome_offset_valid(
382
+ rows: Sequence[bytes],
383
+ occupancies: Sequence[int],
384
+ matches: Sequence[int],
385
+ offset: int,
386
+ ordinals: Sequence[int],
387
+ *,
388
+ require_positive: bool,
389
+ ) -> bool:
390
+ observed_positive = False
391
+ for ordinal in ordinals:
392
+ row = rows[ordinal]
393
+ occupied = occupancies[ordinal]
394
+ base = ordinal * _CLASS_SLOT_COUNT
395
+ for slot in range(_CLASS_SLOT_COUNT):
396
+ value = _u32le(row, offset + slot * 4)
397
+ match_value = matches[base + slot]
398
+ if slot < occupied:
399
+ if value > match_value:
400
+ return False
401
+ observed_positive = observed_positive or value > 0
402
+ elif value != 0:
403
+ return False
404
+ return observed_positive or not require_positive
405
+
406
+
407
+ def _outcome_candidates(
408
+ rows: Sequence[bytes],
409
+ occupancies: Sequence[int],
410
+ matches: Sequence[int],
411
+ activity_masks: Sequence[bytes],
412
+ ) -> tuple[tuple[int, tuple[int, ...]], ...]:
413
+ sample = _sample_ordinals(len(rows), occupancies)
414
+ prefiltered: list[int] = []
415
+ for offset in range(len(rows[0]) - 11):
416
+ if _outcome_offset_valid(
417
+ rows,
418
+ occupancies,
419
+ matches,
420
+ offset,
421
+ sample,
422
+ require_positive=False,
423
+ ):
424
+ if not any(
425
+ any(
426
+ activity_masks[slot][
427
+ offset + slot * 4 : offset + slot * 4 + 4
428
+ ]
429
+ )
430
+ for slot in range(_CLASS_SLOT_COUNT)
431
+ ):
432
+ continue
433
+ prefiltered.append(offset)
434
+ if (
435
+ len(prefiltered)
436
+ > _MAX_ACTIVE_OUTCOME_PREFILTER_CANDIDATES
437
+ ):
438
+ return ()
439
+
440
+ candidates: list[tuple[int, tuple[int, ...]]] = []
441
+ all_ordinals = tuple(range(len(rows)))
442
+ for offset in prefiltered:
443
+ if not _outcome_offset_valid(
444
+ rows,
445
+ occupancies,
446
+ matches,
447
+ offset,
448
+ all_ordinals,
449
+ require_positive=True,
450
+ ):
451
+ continue
452
+ vector = _counter_vector(rows, offset)
453
+ candidates.append((offset, vector))
454
+ if len(candidates) > _MAX_COUNTER_CANDIDATES:
455
+ return ()
456
+ return tuple(candidates)
457
+
458
+
459
+ def _balanced_counter_solution(
460
+ rows: Sequence[bytes],
461
+ occupancies: Sequence[int],
462
+ ) -> Optional[tuple[int, tuple[int, int, int]]]:
463
+ record_size = len(rows[0])
464
+ occupied_activity = [0] * _CLASS_SLOT_COUNT
465
+ for row, occupied in zip(rows, occupancies):
466
+ row_bits = int.from_bytes(row, "little")
467
+ for slot in range(occupied):
468
+ occupied_activity[slot] |= row_bits
469
+ activity_masks = tuple(
470
+ value.to_bytes(record_size, "little")
471
+ for value in occupied_activity
472
+ )
473
+
474
+ solutions: set[tuple[int, tuple[int, int, int]]] = set()
475
+ for matches_offset, matches in _matches_candidates(rows, occupancies):
476
+ outcomes = _outcome_candidates(
477
+ rows,
478
+ occupancies,
479
+ matches,
480
+ activity_masks,
481
+ )
482
+ if not outcomes:
483
+ continue
484
+ by_vector: dict[tuple[int, ...], list[int]] = {}
485
+ for offset, vector in outcomes:
486
+ by_vector.setdefault(vector, []).append(offset)
487
+ unique = tuple(by_vector)
488
+ if len(unique) > _MAX_COUNTER_CANDIDATES:
489
+ continue
490
+ for left_index, left in enumerate(unique):
491
+ for right in unique[left_index:]:
492
+ remainder = tuple(
493
+ match - first - second
494
+ for match, first, second in zip(matches, left, right)
495
+ )
496
+ if any(value < 0 for value in remainder):
497
+ continue
498
+ third_offsets = by_vector.get(remainder)
499
+ if third_offsets is None:
500
+ continue
501
+ for left_offset in by_vector[left]:
502
+ for right_offset in by_vector[right]:
503
+ for third_offset in third_offsets:
504
+ bases = (
505
+ matches_offset,
506
+ left_offset,
507
+ right_offset,
508
+ third_offset,
509
+ )
510
+ if any(
511
+ abs(left_base - right_base) < 12
512
+ for index, left_base in enumerate(bases)
513
+ for right_base in bases[index + 1 :]
514
+ ):
515
+ continue
516
+ ordered = sorted(
517
+ (left_offset, right_offset, third_offset)
518
+ )
519
+ solutions.add(
520
+ (
521
+ matches_offset,
522
+ (ordered[0], ordered[1], ordered[2]),
523
+ )
524
+ )
525
+ if len(solutions) > 1:
526
+ return None
527
+ return next(iter(solutions)) if len(solutions) == 1 else None
528
+
529
+
530
+ def _history_candidates(
531
+ rows: Sequence[bytes],
532
+ occupancies: Sequence[int],
533
+ matches_offset: int,
534
+ ) -> tuple[int, ...]:
535
+ required = _CLASS_SLOT_COUNT * _HISTORY_SLOT_SIZE
536
+ sample = _sample_ordinals(len(rows), occupancies)
537
+
538
+ def valid_for(offset: int, ordinals: Sequence[int]) -> bool:
539
+ for ordinal in ordinals:
540
+ row = rows[ordinal]
541
+ occupied = occupancies[ordinal]
542
+ for slot in range(_CLASS_SLOT_COUNT):
543
+ start = offset + slot * _HISTORY_SLOT_SIZE
544
+ decoded = _decode_history_region(
545
+ row[start : start + _HISTORY_SLOT_SIZE]
546
+ )
547
+ if decoded is None:
548
+ return False
549
+ raw, codes = decoded
550
+ matches = _u32le(row, matches_offset + slot * 4)
551
+ if slot < occupied:
552
+ if (
553
+ not raw
554
+ or len(codes) != 2 * min(matches, 5)
555
+ ):
556
+ return False
557
+ elif raw or codes:
558
+ return False
559
+ return True
560
+
561
+ prefiltered: list[int] = []
562
+ for offset in range(len(rows[0]) - required + 1):
563
+ if valid_for(offset, sample):
564
+ prefiltered.append(offset)
565
+ if len(prefiltered) > _MAX_PREFILTER_CANDIDATES:
566
+ return ()
567
+
568
+ all_ordinals = tuple(range(len(rows)))
569
+ candidates: list[int] = []
570
+ for offset in prefiltered:
571
+ if valid_for(offset, all_ordinals):
572
+ candidates.append(offset)
573
+ if len(candidates) > 1:
574
+ return ()
575
+ return tuple(candidates)
576
+
577
+
578
+ def _orient_outcomes(
579
+ rows: Sequence[bytes],
580
+ occupancies: Sequence[int],
581
+ matches_offset: int,
582
+ outcome_offsets: Sequence[int],
583
+ history_offset: int,
584
+ ) -> Optional[tuple[int, int, int]]:
585
+ valid_permutations: list[tuple[int, int, int]] = []
586
+ for wins_offset, draws_offset, losses_offset in itertools.permutations(
587
+ outcome_offsets
588
+ ):
589
+ discriminators = 0
590
+ valid = True
591
+ for row, occupied in zip(rows, occupancies):
592
+ for slot in range(occupied):
593
+ matches = _u32le(row, matches_offset + slot * 4)
594
+ if matches > 5:
595
+ continue
596
+ decoded = _decode_history_region(
597
+ row[
598
+ history_offset
599
+ + slot * _HISTORY_SLOT_SIZE : history_offset
600
+ + (slot + 1) * _HISTORY_SLOT_SIZE
601
+ ]
602
+ )
603
+ if decoded is None:
604
+ valid = False
605
+ break
606
+ _raw, codes = decoded
607
+ equal = sum(
608
+ codes[index] == codes[index + 1]
609
+ for index in range(0, len(codes), 2)
610
+ )
611
+ unequal = matches - equal
612
+ wins = _u32le(row, wins_offset + slot * 4)
613
+ draws = _u32le(row, draws_offset + slot * 4)
614
+ losses = _u32le(row, losses_offset + slot * 4)
615
+ if draws != 0 or wins != equal or losses != unequal:
616
+ valid = False
617
+ break
618
+ if wins != losses:
619
+ discriminators += 1
620
+ if not valid:
621
+ break
622
+ if valid and discriminators >= _MIN_HISTORY_DISCRIMINATORS:
623
+ valid_permutations.append(
624
+ (wins_offset, draws_offset, losses_offset)
625
+ )
626
+ return valid_permutations[0] if len(valid_permutations) == 1 else None
627
+
628
+
629
+ def _learn_performance_offsets(
630
+ records: _RecordTable,
631
+ class_offset: int,
632
+ occupancies: tuple[int, ...],
633
+ ) -> Optional[_PerformanceOffsets]:
634
+ spec_offset = _learn_spec_offset(records.rows, occupancies)
635
+ counters = _balanced_counter_solution(records.rows, occupancies)
636
+ if spec_offset is None or counters is None:
637
+ return None
638
+ matches_offset, outcome_offsets = counters
639
+ histories = _history_candidates(
640
+ records.rows,
641
+ occupancies,
642
+ matches_offset,
643
+ )
644
+ if len(histories) != 1:
645
+ return None
646
+ history_offset = histories[0]
647
+ oriented = _orient_outcomes(
648
+ records.rows,
649
+ occupancies,
650
+ matches_offset,
651
+ outcome_offsets,
652
+ history_offset,
653
+ )
654
+ if oriented is None:
655
+ return None
656
+ wins_offset, draws_offset, losses_offset = oriented
657
+ return _PerformanceOffsets(
658
+ class_offset=class_offset,
659
+ spec_offset=spec_offset,
660
+ matches_offset=matches_offset,
661
+ wins_offset=wins_offset,
662
+ draws_offset=draws_offset,
663
+ losses_offset=losses_offset,
664
+ history_offset=history_offset,
665
+ occupancies=occupancies,
666
+ )
667
+
668
+
669
+ def _csv_integer_count(
670
+ row: bytes,
671
+ offset: int,
672
+ *,
673
+ maximum_length: int = 512,
674
+ ) -> Optional[tuple[int, int]]:
675
+ if offset < 0 or offset >= len(row):
676
+ return None
677
+ if not 48 <= row[offset] <= 57:
678
+ return None
679
+ if offset > 0 and (48 <= row[offset - 1] <= 57 or row[offset - 1] == 44):
680
+ return None
681
+ end_limit = min(len(row), offset + maximum_length + 1)
682
+ nul = row.find(b"\x00", offset, end_limit)
683
+ if nul < 0:
684
+ return None
685
+ encoded = row[offset:nul]
686
+ try:
687
+ text = encoded.decode("ascii")
688
+ except UnicodeDecodeError:
689
+ return None
690
+ parts = text.split(",")
691
+ if not parts or any(not part.isdigit() for part in parts):
692
+ return None
693
+ return len(parts), len(encoded)
694
+
695
+
696
+ def _raw_base_candidates(
697
+ rows: Sequence[bytes],
698
+ occupancies: Sequence[int],
699
+ *,
700
+ value_count: int,
701
+ slot_size: int,
702
+ minimum_text_length: int,
703
+ maximum_text_length: int,
704
+ ) -> tuple[int, ...]:
705
+ first = rows[0]
706
+ candidates: list[int] = []
707
+ required_end = _CLASS_SLOT_COUNT * slot_size
708
+ for offset in range(len(first) - required_end + 1):
709
+ first_shape = _csv_integer_count(first, offset)
710
+ if (
711
+ first_shape is None
712
+ or first_shape[0] != value_count
713
+ or not minimum_text_length <= first_shape[1] <= maximum_text_length
714
+ ):
715
+ continue
716
+ valid = True
717
+ for row in rows:
718
+ shape = _csv_integer_count(row, offset)
719
+ if (
720
+ shape is None
721
+ or shape[0] != value_count
722
+ or not minimum_text_length
723
+ <= shape[1]
724
+ <= maximum_text_length
725
+ ):
726
+ valid = False
727
+ break
728
+ if not valid:
729
+ continue
730
+ # Secondary/tertiary sections provide periodicity evidence. A tiny
731
+ # minority of observed occupied addon slots carry a shorter legitimate
732
+ # form, so require overwhelming support instead of fabricating an
733
+ # all-record grammar that the wire does not have.
734
+ for slot in range(1, _CLASS_SLOT_COUNT):
735
+ eligible = tuple(
736
+ row
737
+ for row, occupied in zip(rows, occupancies)
738
+ if occupied > slot
739
+ )
740
+ if not eligible:
741
+ valid = False
742
+ break
743
+ matching = 0
744
+ for row in eligible:
745
+ shape = _csv_integer_count(row, offset + slot * slot_size)
746
+ if (
747
+ shape is not None
748
+ and shape[0] == value_count
749
+ and minimum_text_length
750
+ <= shape[1]
751
+ <= maximum_text_length
752
+ ):
753
+ matching += 1
754
+ if matching * 100 < len(eligible) * 95:
755
+ valid = False
756
+ break
757
+ if valid:
758
+ candidates.append(offset)
759
+ if len(candidates) > 1:
760
+ return ()
761
+ return tuple(candidates)
762
+
763
+
764
+ def _learn_raw_offsets(
765
+ records: _RecordTable,
766
+ performance: _PerformanceOffsets,
767
+ ) -> Optional[_RawOffsets]:
768
+ gear = _raw_base_candidates(
769
+ records.rows,
770
+ performance.occupancies,
771
+ value_count=126,
772
+ slot_size=_GEAR_SLOT_SIZE,
773
+ minimum_text_length=300,
774
+ maximum_text_length=500,
775
+ )
776
+ addons = _raw_base_candidates(
777
+ records.rows,
778
+ performance.occupancies,
779
+ value_count=12,
780
+ slot_size=_ADDON_SLOT_SIZE,
781
+ minimum_text_length=20,
782
+ maximum_text_length=100,
783
+ )
784
+ if len(gear) != 1 or len(addons) != 1:
785
+ return None
786
+ return _RawOffsets(gear[0], addons[0])
787
+
788
+
789
+ def _ranges_overlap(left: int, right: int, size: int = 4) -> bool:
790
+ return left < right + size and right < left + size
791
+
792
+
793
+ def _aggregate_candidates(
794
+ records: _RecordTable,
795
+ performance: _PerformanceOffsets,
796
+ ) -> Optional[tuple[int, int, int]]:
797
+ protected = (
798
+ performance.matches_offset,
799
+ performance.wins_offset,
800
+ performance.draws_offset,
801
+ performance.losses_offset,
802
+ )
803
+ exposed = tuple(
804
+ sum(
805
+ _u32le(row, performance.matches_offset + slot * 4)
806
+ for slot in range(occupied)
807
+ )
808
+ for row, occupied in zip(records.rows, performance.occupancies)
809
+ )
810
+ exact_ordinals = tuple(
811
+ ordinal
812
+ for ordinal, occupied in enumerate(performance.occupancies)
813
+ if occupied < _CLASS_SLOT_COUNT
814
+ )
815
+ if not exact_ordinals or not any(
816
+ occupied == _CLASS_SLOT_COUNT
817
+ for occupied in performance.occupancies
818
+ ):
819
+ return None
820
+
821
+ by_vector: dict[tuple[int, ...], list[int]] = {}
822
+ for offset in range(records.record_end - 3):
823
+ if any(
824
+ _ranges_overlap(offset, base + slot * 4)
825
+ for base in protected
826
+ for slot in range(_CLASS_SLOT_COUNT)
827
+ ):
828
+ continue
829
+ values = tuple(_u32le(row, offset) for row in records.rows)
830
+ if (
831
+ not any(values)
832
+ or any(value > _MAX_LEARNED_COUNTER for value in values)
833
+ or any(values[index] > exposed[index] for index in exact_ordinals)
834
+ ):
835
+ continue
836
+ by_vector.setdefault(values, []).append(offset)
837
+ if len(by_vector) > _MAX_AGGREGATE_VECTORS:
838
+ return None
839
+
840
+ vectors = tuple(by_vector)
841
+ subset_lookup: dict[tuple[int, ...], list[tuple[int, ...]]] = {}
842
+ for vector in vectors:
843
+ key = tuple(vector[index] for index in exact_ordinals)
844
+ subset_lookup.setdefault(key, []).append(vector)
845
+
846
+ target = tuple(exposed[index] for index in exact_ordinals)
847
+ solutions: set[tuple[int, int, int]] = set()
848
+ for left_index, left in enumerate(vectors):
849
+ for right in vectors[left_index:]:
850
+ remainder = tuple(
851
+ target_value - left[index] - right[index]
852
+ for index, target_value in zip(exact_ordinals, target)
853
+ )
854
+ if any(value < 0 for value in remainder):
855
+ continue
856
+ for third in subset_lookup.get(remainder, ()):
857
+ totals = tuple(
858
+ first + second + last
859
+ for first, second, last in zip(left, right, third)
860
+ )
861
+ if any(
862
+ total < minimum
863
+ or total <= 0
864
+ or total > _MAX_PLAUSIBLE_MATCHES
865
+ or (
866
+ performance.occupancies[ordinal] < _CLASS_SLOT_COUNT
867
+ and total != minimum
868
+ )
869
+ for ordinal, (total, minimum) in enumerate(
870
+ zip(totals, exposed)
871
+ )
872
+ ):
873
+ continue
874
+ if not any(
875
+ performance.occupancies[ordinal] == _CLASS_SLOT_COUNT
876
+ and total > exposed[ordinal]
877
+ for ordinal, total in enumerate(totals)
878
+ ):
879
+ continue
880
+ for left_offset in by_vector[left]:
881
+ for right_offset in by_vector[right]:
882
+ for third_offset in by_vector[third]:
883
+ offsets = (
884
+ left_offset,
885
+ right_offset,
886
+ third_offset,
887
+ )
888
+ if len(set(offsets)) != 3:
889
+ continue
890
+ ordered = sorted(offsets)
891
+ solutions.add(
892
+ (ordered[0], ordered[1], ordered[2])
893
+ )
894
+ if len(solutions) > 8:
895
+ return None
896
+ return next(iter(solutions)) if len(solutions) == 1 else None
897
+
898
+
899
+ def _orient_aggregate(
900
+ records: _RecordTable,
901
+ performance: _PerformanceOffsets,
902
+ candidates: Sequence[int],
903
+ ) -> Optional[_AggregateOffsets]:
904
+ one_slot = tuple(
905
+ ordinal
906
+ for ordinal, occupied in enumerate(performance.occupancies)
907
+ if occupied == 1
908
+ )
909
+ if len(one_slot) < 5:
910
+ return None
911
+ scored: list[tuple[int, int, tuple[int, int, int]]] = []
912
+ for wins_offset, draws_offset, losses_offset in itertools.permutations(
913
+ candidates
914
+ ):
915
+ exact = 0
916
+ distance = 0
917
+ for ordinal, row in enumerate(records.rows):
918
+ exposed_wins = sum(
919
+ _u32le(row, performance.wins_offset + slot * 4)
920
+ for slot in range(performance.occupancies[ordinal])
921
+ )
922
+ exposed_draws = sum(
923
+ _u32le(row, performance.draws_offset + slot * 4)
924
+ for slot in range(performance.occupancies[ordinal])
925
+ )
926
+ exposed_losses = sum(
927
+ _u32le(row, performance.losses_offset + slot * 4)
928
+ for slot in range(performance.occupancies[ordinal])
929
+ )
930
+ aggregate = (
931
+ _u32le(row, wins_offset),
932
+ _u32le(row, draws_offset),
933
+ _u32le(row, losses_offset),
934
+ )
935
+ exposed_values = (exposed_wins, exposed_draws, exposed_losses)
936
+ distance += sum(
937
+ abs(value - expected)
938
+ for value, expected in zip(aggregate, exposed_values)
939
+ )
940
+ if ordinal in one_slot and aggregate == exposed_values:
941
+ exact += 1
942
+ scored.append(
943
+ (distance, -exact, (wins_offset, draws_offset, losses_offset))
944
+ )
945
+ scored.sort()
946
+ if len(scored) < 2:
947
+ return None
948
+ best_distance, best_negative_exact, best = scored[0]
949
+ second_distance = scored[1][0]
950
+ minimum_exact = max(5, (len(one_slot) * 3) // 4)
951
+ if (
952
+ -best_negative_exact < minimum_exact
953
+ or second_distance <= best_distance * 4
954
+ ):
955
+ return None
956
+ return _AggregateOffsets(*best)
957
+
958
+
959
+ def _learn_aggregate(
960
+ records: _RecordTable,
961
+ performance: _PerformanceOffsets,
962
+ ) -> Optional[_AggregateOffsets]:
963
+ candidates = _aggregate_candidates(records, performance)
964
+ if candidates is None:
965
+ return None
966
+ return _orient_aggregate(records, performance, candidates)
967
+
968
+
969
+ def _raw_sections_agree(
970
+ rich: _LearnedTable,
971
+ overall: _LearnedTable,
972
+ ) -> bool:
973
+ if (
974
+ rich.raw is None
975
+ or overall.raw is None
976
+ or rich.performance is None
977
+ or overall.performance is None
978
+ ):
979
+ return False
980
+ rich_ordinals = {
981
+ name: ordinal for ordinal, name in enumerate(rich.records.names)
982
+ }
983
+ compared = 0
984
+ for overall_ordinal, name in enumerate(overall.records.names):
985
+ rich_ordinal = rich_ordinals.get(name)
986
+ if rich_ordinal is None:
987
+ continue
988
+ rich_row = rich.records.rows[rich_ordinal]
989
+ overall_row = overall.records.rows[overall_ordinal]
990
+ rich_occupied = rich.performance.occupancies[rich_ordinal]
991
+ overall_occupied = overall.performance.occupancies[overall_ordinal]
992
+ for slot in range(min(rich_occupied, overall_occupied)):
993
+ rich_class = rich_row[rich.performance.class_offset + slot]
994
+ overall_class = overall_row[overall.performance.class_offset + slot]
995
+ if rich_class != overall_class:
996
+ continue
997
+ rich_gear = rich_row[
998
+ rich.raw.gear_offset
999
+ + slot * _GEAR_SLOT_SIZE : rich.raw.gear_offset
1000
+ + (slot + 1) * _GEAR_SLOT_SIZE
1001
+ ]
1002
+ overall_gear = overall_row[
1003
+ overall.raw.gear_offset
1004
+ + slot * _GEAR_SLOT_SIZE : overall.raw.gear_offset
1005
+ + (slot + 1) * _GEAR_SLOT_SIZE
1006
+ ]
1007
+ rich_addons = rich_row[
1008
+ rich.raw.addons_offset
1009
+ + slot * _ADDON_SLOT_SIZE : rich.raw.addons_offset
1010
+ + (slot + 1) * _ADDON_SLOT_SIZE
1011
+ ]
1012
+ overall_addons = overall_row[
1013
+ overall.raw.addons_offset
1014
+ + slot * _ADDON_SLOT_SIZE : overall.raw.addons_offset
1015
+ + (slot + 1) * _ADDON_SLOT_SIZE
1016
+ ]
1017
+ if rich_gear != overall_gear or rich_addons != overall_addons:
1018
+ return False
1019
+ compared += 1
1020
+ return compared >= 20
1021
+
1022
+
1023
+ def _layout_id(
1024
+ role: str,
1025
+ records: _RecordTable,
1026
+ elo_offset: Optional[int],
1027
+ performance: Optional[_PerformanceOffsets],
1028
+ raw: Optional[_RawOffsets] = None,
1029
+ aggregate: Optional[_AggregateOffsets] = None,
1030
+ ) -> str:
1031
+ fields: tuple[object, ...] = (
1032
+ "solare-ephemeral-detail-v1",
1033
+ role,
1034
+ records.family.frame_length,
1035
+ records.family.record_stride,
1036
+ records.family.name_offset,
1037
+ records.family.rank_offset,
1038
+ elo_offset,
1039
+ (
1040
+ None
1041
+ if performance is None
1042
+ else (
1043
+ performance.class_offset,
1044
+ performance.spec_offset,
1045
+ performance.matches_offset,
1046
+ performance.wins_offset,
1047
+ performance.draws_offset,
1048
+ performance.losses_offset,
1049
+ performance.history_offset,
1050
+ )
1051
+ ),
1052
+ (
1053
+ None
1054
+ if aggregate is None
1055
+ else (
1056
+ aggregate.wins_offset,
1057
+ aggregate.draws_offset,
1058
+ aggregate.losses_offset,
1059
+ )
1060
+ ),
1061
+ (
1062
+ None
1063
+ if raw is None
1064
+ else (
1065
+ raw.gear_offset,
1066
+ raw.addons_offset,
1067
+ )
1068
+ ),
1069
+ )
1070
+ digest = hashlib.sha256(repr(fields).encode("ascii")).hexdigest()[:16]
1071
+ return f"solare-{role}-ephemeral-v1-{digest}"
1072
+
1073
+
1074
+ def _performance_layout(
1075
+ learned: _LearnedTable,
1076
+ layout_id: str,
1077
+ ) -> Optional[_RichDetailLayout | _OverallDetailLayout]:
1078
+ performance = learned.performance
1079
+ if performance is None:
1080
+ return None
1081
+ raw = learned.raw
1082
+ frame_length = learned.records.family.frame_length
1083
+ record_stride = learned.records.family.record_stride
1084
+ name_offset = learned.records.family.name_offset
1085
+ rank_offset = learned.records.family.rank_offset
1086
+ elo_offset = learned.elo_offset or 0
1087
+ gear_offset = raw.gear_offset if raw is not None else 0
1088
+ addons_offset = raw.addons_offset if raw is not None else 0
1089
+ if learned.records.family.role == "rich":
1090
+ return _RichDetailLayout(
1091
+ layout_id=layout_id,
1092
+ frame_length=frame_length,
1093
+ record_stride=record_stride,
1094
+ name_offset=name_offset,
1095
+ rank_offset=rank_offset,
1096
+ class_offset=performance.class_offset,
1097
+ matches_offset=performance.matches_offset,
1098
+ elo_offset=elo_offset,
1099
+ spec_offset=performance.spec_offset,
1100
+ history_offset=performance.history_offset,
1101
+ gear_offset=gear_offset,
1102
+ draws_offset=performance.draws_offset,
1103
+ addons_offset=addons_offset,
1104
+ wins_offset=performance.wins_offset,
1105
+ losses_offset=performance.losses_offset,
1106
+ )
1107
+ aggregate = learned.aggregate
1108
+ return _OverallDetailLayout(
1109
+ layout_id=layout_id,
1110
+ frame_length=frame_length,
1111
+ record_stride=record_stride,
1112
+ name_offset=name_offset,
1113
+ rank_offset=rank_offset,
1114
+ elo_offset=elo_offset,
1115
+ aggregate_wins_offset=(
1116
+ aggregate.wins_offset if aggregate is not None else 0
1117
+ ),
1118
+ aggregate_draws_offset=(
1119
+ aggregate.draws_offset if aggregate is not None else 0
1120
+ ),
1121
+ aggregate_losses_offset=(
1122
+ aggregate.losses_offset if aggregate is not None else 0
1123
+ ),
1124
+ class_offset=performance.class_offset,
1125
+ spec_offset=performance.spec_offset,
1126
+ matches_offset=performance.matches_offset,
1127
+ wins_offset=performance.wins_offset,
1128
+ draws_offset=performance.draws_offset,
1129
+ losses_offset=performance.losses_offset,
1130
+ history_offset=performance.history_offset,
1131
+ gear_offset=gear_offset,
1132
+ addons_offset=addons_offset,
1133
+ )
1134
+
1135
+
1136
+ def _decode_learned_rich(learned: _LearnedTable) -> Optional[SolareDetailDecode]:
1137
+ if learned.elo_offset is None and learned.performance is None:
1138
+ return None
1139
+ layout_id = _layout_id(
1140
+ "rich",
1141
+ learned.records,
1142
+ learned.elo_offset,
1143
+ learned.performance,
1144
+ learned.raw,
1145
+ )
1146
+ layout = _performance_layout(learned, layout_id)
1147
+ players: list[SolarePlayer] = []
1148
+ primary_codes: list[int] = []
1149
+ previous_elo: Optional[int] = None
1150
+ raw_enabled = learned.raw is not None
1151
+ for ordinal, row in enumerate(learned.records.rows):
1152
+ class_code = (
1153
+ row[learned.performance.class_offset]
1154
+ if learned.performance is not None
1155
+ else learned.records.family.class_codes[ordinal]
1156
+ )
1157
+ display_name = class_name(class_code)
1158
+ if display_name is None:
1159
+ return None
1160
+ elo = (
1161
+ _u32le(row, learned.elo_offset)
1162
+ if learned.elo_offset is not None
1163
+ else None
1164
+ )
1165
+ if elo is not None:
1166
+ if (
1167
+ not 0 < elo <= _MAX_LEARNED_ELO
1168
+ or (previous_elo is not None and previous_elo < elo)
1169
+ ):
1170
+ return None
1171
+ previous_elo = elo
1172
+ performances: tuple[SolareClassPerformance, ...] = ()
1173
+ if layout is not None:
1174
+ decoded = _decode_performance_slots(
1175
+ row,
1176
+ 0,
1177
+ layout,
1178
+ retain_raw_extensions=raw_enabled,
1179
+ )
1180
+ if decoded is None:
1181
+ return None
1182
+ performances = decoded
1183
+ class_code = performances[0].player_class.code
1184
+ display_name = performances[0].player_class.name
1185
+ primary = SolareClass(class_code, display_name)
1186
+ players.append(
1187
+ SolarePlayer(
1188
+ name=learned.records.names[ordinal],
1189
+ global_rank=learned.records.ranks[ordinal],
1190
+ primary_class=primary,
1191
+ elo=elo,
1192
+ classes_played=performances,
1193
+ )
1194
+ )
1195
+ primary_codes.append(class_code)
1196
+ if (
1197
+ tuple(primary_codes) != learned.records.family.class_codes
1198
+ or tuple(sorted(Counter(primary_codes).items()))
1199
+ != learned.records.family.class_counts
1200
+ ):
1201
+ return None
1202
+ capabilities = {"rankings"}
1203
+ if learned.elo_offset is not None:
1204
+ capabilities.add("elo")
1205
+ if learned.performance is not None:
1206
+ capabilities.add("performance")
1207
+ if learned.raw is not None:
1208
+ capabilities.add("raw_extensions")
1209
+ return SolareDetailDecode(layout_id, tuple(players), frozenset(capabilities))
1210
+
1211
+
1212
+ def _decode_learned_overall(
1213
+ learned: _LearnedTable,
1214
+ ) -> Optional[SolareOverallDetailDecode]:
1215
+ if (
1216
+ learned.elo_offset is None
1217
+ and learned.performance is None
1218
+ and learned.aggregate is None
1219
+ ):
1220
+ return None
1221
+ layout_id = _layout_id(
1222
+ "overall",
1223
+ learned.records,
1224
+ learned.elo_offset,
1225
+ learned.performance,
1226
+ learned.raw,
1227
+ learned.aggregate,
1228
+ )
1229
+ layout = _performance_layout(learned, layout_id)
1230
+ raw_enabled = learned.raw is not None
1231
+ entries: list[SolareOverallEntry] = []
1232
+ previous_elo: Optional[int] = None
1233
+ for ordinal, row in enumerate(learned.records.rows):
1234
+ elo = (
1235
+ _u32le(row, learned.elo_offset)
1236
+ if learned.elo_offset is not None
1237
+ else None
1238
+ )
1239
+ if elo is not None:
1240
+ if (
1241
+ not 0 < elo <= _MAX_LEARNED_ELO
1242
+ or (previous_elo is not None and previous_elo < elo)
1243
+ ):
1244
+ return None
1245
+ previous_elo = elo
1246
+ performances: tuple[SolareClassPerformance, ...] = ()
1247
+ aggregate: Optional[tuple[int, int, int]] = None
1248
+ if layout is not None:
1249
+ if not isinstance(layout, _OverallDetailLayout):
1250
+ return None
1251
+ decoded = _decode_performance_slots(
1252
+ row,
1253
+ 0,
1254
+ layout,
1255
+ retain_raw_extensions=raw_enabled,
1256
+ )
1257
+ if decoded is None:
1258
+ return None
1259
+ performances = decoded
1260
+ if learned.aggregate is not None:
1261
+ aggregate = _decode_overall_aggregate_performance(row, 0, layout)
1262
+ if aggregate is None:
1263
+ return None
1264
+ total_wins, total_draws, total_losses = (
1265
+ aggregate if aggregate is not None else (None, None, None)
1266
+ )
1267
+ entries.append(
1268
+ SolareOverallEntry(
1269
+ name=learned.records.names[ordinal],
1270
+ global_rank=learned.records.ranks[ordinal],
1271
+ elo=elo,
1272
+ classes_played=performances,
1273
+ total_wins=total_wins,
1274
+ total_draws=total_draws,
1275
+ total_losses=total_losses,
1276
+ )
1277
+ )
1278
+ capabilities = {"rankings"}
1279
+ if learned.elo_offset is not None:
1280
+ capabilities.add("elo")
1281
+ if learned.performance is not None:
1282
+ capabilities.add("performance")
1283
+ if learned.aggregate is not None:
1284
+ capabilities.add("aggregate_performance")
1285
+ if learned.raw is not None:
1286
+ capabilities.add("raw_extensions")
1287
+ return SolareOverallDetailDecode(
1288
+ layout_id,
1289
+ tuple(entries),
1290
+ frozenset(capabilities),
1291
+ )
1292
+
1293
+
1294
+ def learn_unknown_solare_details(
1295
+ rich_frames: Sequence[BDOFrame],
1296
+ rich: DiscoveredSolareFamily,
1297
+ overall_frames: Sequence[BDOFrame],
1298
+ overall: DiscoveredSolareFamily,
1299
+ *,
1300
+ learn_rich: bool,
1301
+ learn_overall: bool,
1302
+ retain_raw_extensions: bool,
1303
+ known_rich_elos: Optional[Mapping[str, int]] = None,
1304
+ known_overall_elos: Optional[Mapping[str, int]] = None,
1305
+ ) -> tuple[Optional[SolareDetailDecode], Optional[SolareOverallDetailDecode]]:
1306
+ """Learn and decode unregistered detail layouts for one complete snapshot.
1307
+
1308
+ The caller decides which table geometries are unregistered. A returned
1309
+ decode is ephemeral and applies only to this snapshot; no offsets are
1310
+ cached or written to disk.
1311
+ """
1312
+
1313
+ rich_records = _extract_records(
1314
+ rich_frames,
1315
+ rich,
1316
+ expected_frames=_EXPECTED_RICH_FRAMES,
1317
+ expected_records=_EXPECTED_RICH_RECORDS,
1318
+ )
1319
+ overall_records = _extract_records(
1320
+ overall_frames,
1321
+ overall,
1322
+ expected_frames=_EXPECTED_OVERALL_FRAMES,
1323
+ expected_records=_EXPECTED_OVERALL_RECORDS,
1324
+ )
1325
+ if rich_records is None or overall_records is None:
1326
+ return None, None
1327
+
1328
+ rich_occupancies: Optional[tuple[int, ...]] = None
1329
+ if rich.class_offset is not None:
1330
+ rich_occupancies = _class_occupancies(
1331
+ rich_records.rows,
1332
+ rich.class_offset,
1333
+ )
1334
+ if (
1335
+ rich_occupancies is not None
1336
+ and tuple(
1337
+ row[rich.class_offset] for row in rich_records.rows
1338
+ )
1339
+ != rich.class_codes
1340
+ ):
1341
+ rich_occupancies = None
1342
+ overall_class = _learn_overall_class_offset(overall_records)
1343
+
1344
+ if learn_rich and learn_overall:
1345
+ rich_elo, overall_elo = _choose_elo_offsets(
1346
+ rich_records,
1347
+ overall_records,
1348
+ )
1349
+ elif learn_rich:
1350
+ rich_elo = _choose_anchored_elo_offset(
1351
+ rich_records,
1352
+ known_overall_elos,
1353
+ )
1354
+ overall_elo = None
1355
+ elif learn_overall:
1356
+ rich_elo = None
1357
+ overall_elo = _choose_anchored_elo_offset(
1358
+ overall_records,
1359
+ known_rich_elos,
1360
+ )
1361
+ else:
1362
+ rich_elo = None
1363
+ overall_elo = None
1364
+ # Learn the companion table's slot geometry as shadow evidence even when
1365
+ # that geometry has a registered decoder. Public rows still come only
1366
+ # from their owning decoder; this shadow result is used solely to prove
1367
+ # opted-in raw-section boundaries across the two independent responses.
1368
+ rich_performance = (
1369
+ _learn_performance_offsets(
1370
+ rich_records,
1371
+ rich.class_offset,
1372
+ rich_occupancies,
1373
+ )
1374
+ if (learn_rich or retain_raw_extensions)
1375
+ and rich.class_offset is not None
1376
+ and rich_occupancies is not None
1377
+ else None
1378
+ )
1379
+ overall_performance = (
1380
+ _learn_performance_offsets(
1381
+ overall_records,
1382
+ overall_class[0],
1383
+ overall_class[1],
1384
+ )
1385
+ if (learn_overall or retain_raw_extensions)
1386
+ and overall_class is not None
1387
+ else None
1388
+ )
1389
+ rich_learned = _LearnedTable(
1390
+ rich_records,
1391
+ rich_elo if learn_rich else None,
1392
+ rich_performance,
1393
+ )
1394
+ overall_learned = _LearnedTable(
1395
+ overall_records,
1396
+ overall_elo if learn_overall else None,
1397
+ overall_performance,
1398
+ aggregate=(
1399
+ _learn_aggregate(overall_records, overall_performance)
1400
+ if learn_overall and overall_performance is not None
1401
+ else None
1402
+ ),
1403
+ )
1404
+
1405
+ if retain_raw_extensions:
1406
+ rich_raw = (
1407
+ _learn_raw_offsets(rich_records, rich_performance)
1408
+ if rich_performance is not None
1409
+ else None
1410
+ )
1411
+ overall_raw = (
1412
+ _learn_raw_offsets(overall_records, overall_performance)
1413
+ if overall_performance is not None
1414
+ else None
1415
+ )
1416
+ proposed_rich = _LearnedTable(
1417
+ rich_learned.records,
1418
+ rich_learned.elo_offset,
1419
+ rich_learned.performance,
1420
+ raw=rich_raw,
1421
+ aggregate=rich_learned.aggregate,
1422
+ )
1423
+ proposed_overall = _LearnedTable(
1424
+ overall_learned.records,
1425
+ overall_learned.elo_offset,
1426
+ overall_learned.performance,
1427
+ raw=overall_raw,
1428
+ aggregate=overall_learned.aggregate,
1429
+ )
1430
+ if _raw_sections_agree(proposed_rich, proposed_overall):
1431
+ rich_learned = proposed_rich
1432
+ overall_learned = proposed_overall
1433
+
1434
+ return (
1435
+ _decode_learned_rich(rich_learned) if learn_rich else None,
1436
+ _decode_learned_overall(overall_learned) if learn_overall else None,
1437
+ )