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,370 @@
1
+ """Opcode profile helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from types import MappingProxyType
11
+ from typing import Any, Optional
12
+
13
+
14
+ OPCODE_PROFILE_SCHEMA_VERSION = 1
15
+
16
+
17
+ class ProfileError(ValueError):
18
+ """Raised when an opcode profile has an invalid JSON or schema shape."""
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class OriginCompanionFamily:
23
+ """One explicitly promoted structural family trusted by the classifier."""
24
+
25
+ delta_opcode: int
26
+ companion_opcodes: tuple[int, int]
27
+ companion_lengths: tuple[int, int]
28
+ detection: str
29
+ observations: int
30
+ promoted_at: Optional[str]
31
+
32
+ @property
33
+ def family_key(self) -> tuple[int, int, int, int, int]:
34
+ return (
35
+ self.delta_opcode,
36
+ self.companion_opcodes[0],
37
+ self.companion_opcodes[1],
38
+ self.companion_lengths[0],
39
+ self.companion_lengths[1],
40
+ )
41
+
42
+ def to_dict(self) -> dict[str, Any]:
43
+ return {
44
+ "delta_opcode": f"0x{self.delta_opcode:04X}",
45
+ "companion_opcodes": [
46
+ f"0x{opcode:04X}" for opcode in self.companion_opcodes
47
+ ],
48
+ "companion_lengths": list(self.companion_lengths),
49
+ "detection": self.detection,
50
+ "observations": self.observations,
51
+ "promoted_at": self.promoted_at,
52
+ }
53
+
54
+
55
+ @dataclass(frozen=True)
56
+ class OpcodeProfile:
57
+ """Versioned opcode profile metadata and raw specs."""
58
+
59
+ path: Path
60
+ active: bool
61
+ version: int
62
+ updated_at: Optional[str]
63
+ calibration_item_id: Optional[int]
64
+ specs: Mapping[str, tuple[Mapping[str, Any], ...]]
65
+ origin_companion_families: tuple[OriginCompanionFamily, ...] = ()
66
+
67
+ def __post_init__(self) -> None:
68
+ object.__setattr__(
69
+ self,
70
+ "specs",
71
+ MappingProxyType(
72
+ {
73
+ event: tuple(
74
+ _freeze_profile_mapping(entry) for entry in entries
75
+ )
76
+ for event, entries in self.specs.items()
77
+ }
78
+ ),
79
+ )
80
+
81
+ def to_dict(self) -> dict[str, Any]:
82
+ return {
83
+ "path": str(self.path),
84
+ "active": self.active,
85
+ "version": self.version,
86
+ "updated_at": self.updated_at,
87
+ "calibration_item_id": self.calibration_item_id,
88
+ "specs": {
89
+ event: [_thaw_profile_value(entry) for entry in entries]
90
+ for event, entries in self.specs.items()
91
+ },
92
+ "origin_companion_families": [
93
+ family.to_dict() for family in self.origin_companion_families
94
+ ],
95
+ }
96
+
97
+
98
+ def load_opcode_profile(path: str | Path) -> OpcodeProfile:
99
+ """Load and validate one explicit, app-owned opcode profile."""
100
+
101
+ profile_path = Path(path)
102
+ if not profile_path.is_file():
103
+ raise FileNotFoundError(f"Opcode profile does not exist: {profile_path}")
104
+ try:
105
+ data = json.loads(profile_path.read_text(encoding="utf-8-sig"))
106
+ except (json.JSONDecodeError, RecursionError, UnicodeError) as exc:
107
+ raise ProfileError(f"Could not parse opcodes JSON {profile_path}: {exc}") from exc
108
+ if not isinstance(data, dict):
109
+ raise ProfileError(f"Opcodes JSON {profile_path} must be a top-level object")
110
+
111
+ active_value = data.get("profile_active", False)
112
+ if not isinstance(active_value, bool):
113
+ raise ProfileError(f"profile_active in {profile_path} must be a boolean")
114
+
115
+ version_value = data.get("version")
116
+ if (
117
+ isinstance(version_value, bool)
118
+ or not isinstance(version_value, int)
119
+ or version_value != OPCODE_PROFILE_SCHEMA_VERSION
120
+ ):
121
+ raise ProfileError(
122
+ f"version in {profile_path} must be "
123
+ f"{OPCODE_PROFILE_SCHEMA_VERSION}"
124
+ )
125
+
126
+ updated_at_value = data.get("updated_at")
127
+ if updated_at_value is not None and not isinstance(updated_at_value, str):
128
+ raise ProfileError(f"updated_at in {profile_path} must be a string")
129
+
130
+ calibration_item_value = data.get("calibration_item_id")
131
+ if calibration_item_value is not None and (
132
+ isinstance(calibration_item_value, bool)
133
+ or not isinstance(calibration_item_value, int)
134
+ or not 1 <= calibration_item_value <= 0xFFFFFFFF
135
+ ):
136
+ raise ProfileError(
137
+ f"calibration_item_id in {profile_path} must be a positive uint32"
138
+ )
139
+
140
+ specs = data.get("specs", {})
141
+ if not isinstance(specs, dict):
142
+ raise ProfileError(f"specs in {profile_path} must be an object")
143
+ normalized_specs: dict[str, list[dict[str, Any]]] = {}
144
+ for event_name, entries in specs.items():
145
+ if not isinstance(event_name, str):
146
+ raise ProfileError(f"spec event names in {profile_path} must be strings")
147
+ if not isinstance(entries, list):
148
+ raise ProfileError(
149
+ f"specs[{event_name!r}] in {profile_path} must be a list"
150
+ )
151
+ if any(not isinstance(entry, dict) for entry in entries):
152
+ raise ProfileError(
153
+ f"every specs[{event_name!r}] entry in {profile_path} must be an object"
154
+ )
155
+ normalized_entries: list[dict[str, Any]] = []
156
+ for index, entry in enumerate(entries):
157
+ normalized = dict(entry)
158
+ _validate_profile_entry(profile_path, event_name, index, normalized)
159
+ normalized_entries.append(normalized)
160
+ normalized_specs[event_name] = normalized_entries
161
+
162
+ raw_families = data.get("origin_companion_families", [])
163
+ if not isinstance(raw_families, list):
164
+ raise ProfileError(
165
+ f"origin_companion_families in {profile_path} must be a list"
166
+ )
167
+ families = tuple(
168
+ _origin_companion_family(profile_path, index, entry)
169
+ for index, entry in enumerate(raw_families)
170
+ )
171
+ immutable_specs = MappingProxyType(
172
+ {
173
+ event: tuple(_freeze_profile_mapping(entry) for entry in entries)
174
+ for event, entries in normalized_specs.items()
175
+ }
176
+ )
177
+ return OpcodeProfile(
178
+ path=profile_path,
179
+ active=active_value,
180
+ version=version_value,
181
+ updated_at=updated_at_value,
182
+ calibration_item_id=calibration_item_value,
183
+ specs=immutable_specs,
184
+ origin_companion_families=families,
185
+ )
186
+
187
+
188
+ def _freeze_profile_mapping(value: Mapping[str, Any]) -> Mapping[str, Any]:
189
+ return MappingProxyType(
190
+ {key: _freeze_profile_value(item) for key, item in value.items()}
191
+ )
192
+
193
+
194
+ def _freeze_profile_value(value: Any) -> Any:
195
+ if isinstance(value, Mapping):
196
+ return _freeze_profile_mapping(value)
197
+ if isinstance(value, (list, tuple)):
198
+ return tuple(_freeze_profile_value(item) for item in value)
199
+ return value
200
+
201
+
202
+ def _thaw_profile_value(value: Any) -> Any:
203
+ if isinstance(value, Mapping):
204
+ return {
205
+ key: _thaw_profile_value(item) for key, item in value.items()
206
+ }
207
+ if isinstance(value, tuple):
208
+ return [_thaw_profile_value(item) for item in value]
209
+ return value
210
+
211
+
212
+ def _profile_opcode(value: object, location: str) -> int:
213
+ if isinstance(value, bool):
214
+ raise ProfileError(f"{location} must be a uint16")
215
+ if isinstance(value, int):
216
+ opcode = value
217
+ elif isinstance(value, str):
218
+ try:
219
+ opcode = int(value, 16 if value.lower().startswith("0x") else 10)
220
+ except ValueError as exc:
221
+ raise ProfileError(f"{location} must be a uint16") from exc
222
+ else:
223
+ raise ProfileError(f"{location} must be a uint16")
224
+ if not 0 <= opcode <= 0xFFFF:
225
+ raise ProfileError(f"{location} must be a uint16")
226
+ return opcode
227
+
228
+
229
+ def _origin_companion_family(
230
+ path: Path,
231
+ index: int,
232
+ value: object,
233
+ ) -> OriginCompanionFamily:
234
+ location = f"origin_companion_families[{index}] in {path}"
235
+ if not isinstance(value, dict):
236
+ raise ProfileError(f"{location} must be an object")
237
+ raw_opcodes = value.get("companion_opcodes")
238
+ raw_lengths = value.get("companion_lengths")
239
+ if not isinstance(raw_opcodes, list) or len(raw_opcodes) != 2:
240
+ raise ProfileError(f"companion_opcodes in {location} must contain two values")
241
+ if not isinstance(raw_lengths, list) or len(raw_lengths) != 2:
242
+ raise ProfileError(f"companion_lengths in {location} must contain two values")
243
+ lengths: list[int] = []
244
+ for raw_length in raw_lengths:
245
+ if (
246
+ isinstance(raw_length, bool)
247
+ or not isinstance(raw_length, int)
248
+ or not 5 <= raw_length <= 0xFFFF
249
+ ):
250
+ raise ProfileError(
251
+ f"companion_lengths in {location} must contain values from 5 to 65535"
252
+ )
253
+ lengths.append(raw_length)
254
+ detection = value.get("detection")
255
+ if detection != "shared-token-chain-v1":
256
+ raise ProfileError(
257
+ f"detection in {location} must be 'shared-token-chain-v1'"
258
+ )
259
+ observations = value.get("observations")
260
+ if (
261
+ isinstance(observations, bool)
262
+ or not isinstance(observations, int)
263
+ or observations <= 0
264
+ ):
265
+ raise ProfileError(f"observations in {location} must be a positive integer")
266
+ promoted_at = value.get("promoted_at")
267
+ if promoted_at is not None and not isinstance(promoted_at, str):
268
+ raise ProfileError(f"promoted_at in {location} must be a string or null")
269
+ return OriginCompanionFamily(
270
+ delta_opcode=_profile_opcode(value.get("delta_opcode"), f"delta_opcode in {location}"),
271
+ companion_opcodes=(
272
+ _profile_opcode(raw_opcodes[0], f"companion_opcodes in {location}"),
273
+ _profile_opcode(raw_opcodes[1], f"companion_opcodes in {location}"),
274
+ ),
275
+ companion_lengths=(lengths[0], lengths[1]),
276
+ detection=detection,
277
+ observations=observations,
278
+ promoted_at=promoted_at,
279
+ )
280
+
281
+
282
+ def _validate_profile_entry(
283
+ path: Path,
284
+ event: str,
285
+ index: int,
286
+ entry: dict[str, Any],
287
+ ) -> None:
288
+ location = f"specs[{event!r}][{index}] in {path}"
289
+ embedded_event = entry.get("event")
290
+ if embedded_event is not None and embedded_event != event:
291
+ raise ProfileError(
292
+ f"event in {location} must match its containing {event!r} category"
293
+ )
294
+ opcode = entry.get("opcode")
295
+ if isinstance(opcode, bool):
296
+ raise ProfileError(f"opcode in {location} must be a uint16")
297
+ if isinstance(opcode, str):
298
+ try:
299
+ opcode_number = int(opcode, 16 if opcode.lower().startswith("0x") else 10)
300
+ except ValueError as exc:
301
+ raise ProfileError(f"opcode in {location} must be a uint16") from exc
302
+ elif isinstance(opcode, int):
303
+ opcode_number = opcode
304
+ else:
305
+ raise ProfileError(f"opcode in {location} must be a uint16")
306
+ if not 0 <= opcode_number <= 0xFFFF:
307
+ raise ProfileError(f"opcode in {location} must be a uint16")
308
+
309
+ length = entry.get("length")
310
+ if length is not None and (
311
+ isinstance(length, bool)
312
+ or not isinstance(length, int)
313
+ or not 5 <= length <= 0xFFFF
314
+ ):
315
+ raise ProfileError(f"length in {location} must be 5..65535 or null")
316
+
317
+ offset_fields = (
318
+ "item_id_offset",
319
+ "quantity_offset",
320
+ "item_instance_offset",
321
+ "context_offset",
322
+ "record_count_offset",
323
+ "inventory_slot_offset",
324
+ "source_instance_offset",
325
+ "quantity_removed_offset",
326
+ "quantity_added_offset",
327
+ "destination_instance_offset",
328
+ )
329
+ for field_name in offset_fields:
330
+ value = entry.get(field_name)
331
+ if value is not None and (
332
+ isinstance(value, bool) or not isinstance(value, int) or value < 0
333
+ ):
334
+ raise ProfileError(
335
+ f"{field_name} in {location} must be a non-negative integer or null"
336
+ )
337
+ if event == "STORAGE_ITEM_DELTA":
338
+ item_offset = entry.get("item_id_offset")
339
+ context_offset = entry.get("context_offset")
340
+ count_offset = entry.get("record_count_offset")
341
+ if isinstance(item_offset, int) and not isinstance(item_offset, bool):
342
+ if context_offset is not None and context_offset + 4 > item_offset:
343
+ raise ProfileError(
344
+ f"context_offset in {location} must end before item_id_offset"
345
+ )
346
+ if count_offset is not None and count_offset + 2 > item_offset:
347
+ raise ProfileError(
348
+ f"record_count_offset in {location} must end before item_id_offset"
349
+ )
350
+ repeat_stride = entry.get("repeat_stride")
351
+ if repeat_stride is not None and (
352
+ isinstance(repeat_stride, bool)
353
+ or not isinstance(repeat_stride, int)
354
+ or repeat_stride <= 0
355
+ ):
356
+ raise ProfileError(
357
+ f"repeat_stride in {location} must be a positive integer or null"
358
+ )
359
+ for field_name in ("confidence", "source", "observed_at"):
360
+ value = entry.get(field_name)
361
+ if value is not None and not isinstance(value, str):
362
+ raise ProfileError(f"{field_name} in {location} must be a string or null")
363
+ score = entry.get("score")
364
+ if score is not None and (
365
+ isinstance(score, bool)
366
+ or not isinstance(score, (int, float))
367
+ or not math.isfinite(score)
368
+ or not 0 <= score <= 1
369
+ ):
370
+ raise ProfileError(f"score in {location} must be a finite number from 0 to 1")
bdo_toolkit/py.typed ADDED
@@ -0,0 +1 @@
1
+