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,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
|
+
)
|