bethkit 1.0.0__py3-none-win_amd64.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.
@@ -0,0 +1,472 @@
1
+ """
2
+ Copyright (c) Modding Forge
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import ctypes
7
+ from typing import Any, Optional
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+ from .. import _ffi
12
+ from .._error import BethkitClosedError
13
+ from .._ffi import BethkitFieldValue
14
+ from ..enums import FieldValueKind
15
+
16
+
17
+ class TypedFormId(BaseModel, frozen=True):
18
+ """
19
+ A FormID together with the set of record-type signatures it may
20
+ reference.
21
+ """
22
+
23
+ raw: int
24
+ """Raw 32-bit FormID."""
25
+
26
+ allowed_sigs: tuple[bytes, ...]
27
+ """Permitted target record-type signatures."""
28
+
29
+
30
+ class EnumVal(BaseModel, frozen=True):
31
+ """
32
+ A decoded enumeration field value.
33
+ """
34
+
35
+ value: int
36
+ """Underlying integer value."""
37
+
38
+ name: Optional[str]
39
+ """Human-readable name, or ``None`` if unknown."""
40
+
41
+
42
+ class FlagsVal(BaseModel, frozen=True):
43
+ """
44
+ A decoded bit-flags field value.
45
+ """
46
+
47
+ raw_value: int
48
+ """Full raw flags integer."""
49
+
50
+ active_names: tuple[str, ...]
51
+ """Names of all currently set bits."""
52
+
53
+
54
+ FieldValue = (
55
+ int
56
+ | float
57
+ | str
58
+ | bytes
59
+ | TypedFormId
60
+ | EnumVal
61
+ | FlagsVal
62
+ | list[Any] # struct fields: list[NamedField]; array fields: list[FieldValue]
63
+ | None
64
+ )
65
+ """
66
+ Union of all possible decoded field value types.
67
+
68
+ Simple types (``int``, ``float``, ``str``, ``bytes``, ``None``) are
69
+ returned as-is. Complex types use :class:`TypedFormId`,
70
+ :class:`EnumVal`, or :class:`FlagsVal`. Struct fields produce
71
+ ``list[NamedField]``; array fields produce ``list[FieldValue]``.
72
+ """
73
+
74
+
75
+ class NamedField(BaseModel):
76
+ """
77
+ A single named field inside a struct or record view.
78
+ """
79
+
80
+ model_config = ConfigDict(arbitrary_types_allowed=True)
81
+
82
+ name: str
83
+ """Schema-defined field name."""
84
+
85
+ value: FieldValue
86
+ """Decoded value for this field."""
87
+
88
+
89
+ NamedField.model_rebuild()
90
+
91
+
92
+ def _decode_field_value(raw: BethkitFieldValue, lib: ctypes.CDLL) -> FieldValue:
93
+ """
94
+ Decode a ctypes ``BethkitFieldValue`` into a Python :data:`FieldValue`.
95
+
96
+ Args:
97
+ raw (BethkitFieldValue): The ctypes ``BethkitFieldValue`` struct instance.
98
+ lib (ctypes.CDLL): Loaded bethkit native library handle.
99
+
100
+ Returns:
101
+ FieldValue: The decoded Python value.
102
+ """
103
+
104
+ kind = FieldValueKind(raw.kind)
105
+ p = raw.payload
106
+
107
+ if kind == FieldValueKind.INT:
108
+ return p.int_val
109
+
110
+ if kind == FieldValueKind.FLOAT:
111
+ return p.float_val
112
+
113
+ if kind == FieldValueKind.STR:
114
+ s: Optional[bytes] = p.str_val
115
+ return s.decode("utf-8") if s else ""
116
+
117
+ if kind == FieldValueKind.FORM_ID:
118
+ return p.form_id
119
+
120
+ if kind == FieldValueKind.FORM_ID_TYPED:
121
+ fid = p.form_id_typed
122
+ sigs: tuple[bytes, ...] = ()
123
+ if fid.allowed_sigs and fid.allowed_count:
124
+ raw_ptr = ctypes.cast(
125
+ fid.allowed_sigs, ctypes.POINTER(ctypes.c_uint8 * 4)
126
+ )
127
+ sigs = tuple(
128
+ bytes(raw_ptr[i]) for i in range(fid.allowed_count)
129
+ )
130
+ return TypedFormId(raw=fid.raw, allowed_sigs=sigs)
131
+
132
+ if kind == FieldValueKind.BYTES:
133
+ sl = p.bytes
134
+ if not sl.ptr:
135
+ return b""
136
+ return bytes(ctypes.string_at(sl.ptr, sl.len))
137
+
138
+ if kind == FieldValueKind.ENUM:
139
+ ev = p.enum_val
140
+ name_raw: Optional[bytes] = ev.name
141
+ return EnumVal(
142
+ value=ev.value,
143
+ name=name_raw.decode("utf-8") if name_raw else None,
144
+ )
145
+
146
+ if kind == FieldValueKind.FLAGS:
147
+ fv = p.flags_val
148
+ names: tuple[str, ...] = ()
149
+ if fv.active_names and fv.active_count:
150
+ names = tuple(
151
+ fv.active_names[i].decode("utf-8")
152
+ for i in range(fv.active_count)
153
+ if fv.active_names[i]
154
+ )
155
+ return FlagsVal(raw_value=fv.raw_value, active_names=names)
156
+
157
+ if kind == FieldValueKind.STRUCT:
158
+ entries_ptr = p.struct_entries
159
+ if not entries_ptr:
160
+ return []
161
+ return _decode_entries(entries_ptr, lib)
162
+
163
+ if kind == FieldValueKind.ARRAY:
164
+ values_ptr = p.array_values
165
+ if not values_ptr:
166
+ return []
167
+ return _decode_values(values_ptr, lib)
168
+
169
+ if kind == FieldValueKind.LOCALIZED_ID:
170
+ return p.localized_id
171
+
172
+ return None
173
+
174
+
175
+ def _decode_entries(
176
+ entries_ptr: int, lib: ctypes.CDLL
177
+ ) -> list[NamedField]:
178
+ """
179
+ Convert a native ``BethkitFieldEntries*`` to a list of
180
+ :class:`NamedField`.
181
+
182
+ Args:
183
+ entries_ptr (int): Native pointer to the field-entries object.
184
+ lib (ctypes.CDLL): Loaded bethkit native library handle.
185
+
186
+ Returns:
187
+ list[NamedField]: Decoded list of named fields.
188
+ """
189
+
190
+ n = lib.bethkit_field_entries_len(entries_ptr)
191
+ result: list[NamedField] = []
192
+ for i in range(n):
193
+ nf_ptr = lib.bethkit_field_entries_get(entries_ptr, i)
194
+ if not nf_ptr:
195
+ continue
196
+ nf = nf_ptr.contents
197
+ name_raw: Optional[bytes] = nf.name
198
+ name = name_raw.decode("utf-8") if name_raw else ""
199
+ value = _decode_field_value(nf.value, lib)
200
+ result.append(NamedField(name=name, value=value))
201
+ return result
202
+
203
+
204
+ def _decode_values(
205
+ values_ptr: int, lib: ctypes.CDLL
206
+ ) -> list[FieldValue]:
207
+ """
208
+ Convert a native ``BethkitFieldValues*`` to a list of
209
+ :data:`FieldValue`.
210
+
211
+ Args:
212
+ values_ptr (int): Native pointer to the field-values object.
213
+ lib (ctypes.CDLL): Loaded bethkit native library handle.
214
+
215
+ Returns:
216
+ list[FieldValue]: Decoded list of field values.
217
+ """
218
+
219
+ n = lib.bethkit_field_values_len(values_ptr)
220
+ result: list[FieldValue] = []
221
+ for i in range(n):
222
+ fv_ptr = lib.bethkit_field_values_get(values_ptr, i)
223
+ if not fv_ptr:
224
+ continue
225
+ result.append(_decode_field_value(fv_ptr.contents, lib))
226
+ return result
227
+
228
+
229
+ class SchemaRegistry:
230
+ """
231
+ A registry that maps record signatures to their field schemas.
232
+
233
+ Obtain a pre-built registry for a specific game with the class
234
+ methods (e.g. :meth:`sse`), then use :meth:`has` to check whether a
235
+ given record type is known.
236
+
237
+ This is a **borrowed** handle: the native memory is owned by the
238
+ library and must not be freed.
239
+ """
240
+
241
+ _ptr: int
242
+
243
+ def __init__(self, ptr: int) -> None:
244
+ """
245
+ Args:
246
+ ptr (int): Native handle returned by the FFI factory call.
247
+ """
248
+
249
+ self._ptr = ptr
250
+
251
+ @classmethod
252
+ def sse(cls) -> SchemaRegistry:
253
+ """
254
+ Load the built-in Skyrim Special Edition schema registry.
255
+
256
+ Returns:
257
+ SchemaRegistry: The SSE registry (borrowed, never freed).
258
+
259
+ Raises:
260
+ BethkitNativeError: If the registry cannot be loaded.
261
+ """
262
+
263
+ lib = _ffi.load_lib()
264
+ ptr = lib.bethkit_schema_registry_sse()
265
+ if not ptr:
266
+ _ffi.raise_last_error(lib)
267
+ return cls(ptr)
268
+
269
+ def has(self, sig: bytes | str) -> bool:
270
+ """
271
+ Check whether the registry contains a schema for *sig*.
272
+
273
+ Args:
274
+ sig (bytes | str): Four-byte record-type signature to look up.
275
+
276
+ Returns:
277
+ bool: ``True`` if the signature is known.
278
+
279
+ Raises:
280
+ ValueError: If *sig* is not exactly 4 bytes.
281
+ """
282
+
283
+ if isinstance(sig, str):
284
+ sig = sig.encode("ascii")
285
+ if len(sig) != 4:
286
+ raise ValueError("sig must be exactly 4 bytes")
287
+ buf = (ctypes.c_uint8 * 4)(*sig)
288
+ return bool(
289
+ _ffi.load_lib().bethkit_schema_registry_has(self._ptr, buf)
290
+ )
291
+
292
+ def __repr__(self) -> str:
293
+ """
294
+ Returns:
295
+ str: Developer-friendly representation of the registry.
296
+ """
297
+
298
+ return "<SchemaRegistry>"
299
+
300
+
301
+ class RecordView:
302
+ """
303
+ A decoded, schema-aware view of a plugin record.
304
+
305
+ Create a view with :meth:`new`, specifying the record and its type
306
+ signature. Use :meth:`fields` to retrieve all decoded fields.
307
+
308
+ Use as a context manager to guarantee the native handle is freed::
309
+
310
+ with RecordView.new(record, b"NPC_") as view:
311
+ for field in view.fields():
312
+ print(field.name, field.value)
313
+ """
314
+
315
+ __ptr: int
316
+ _fields_cache: Optional[list[NamedField]]
317
+
318
+ def __init__(self, ptr: int) -> None:
319
+ """
320
+ Args:
321
+ ptr (int): Native handle returned by the FFI new call.
322
+ """
323
+
324
+ self.__ptr = ptr
325
+ self._fields_cache = None
326
+
327
+ def __check_open(self) -> int:
328
+ """
329
+ Return the native pointer, raising if the handle is already closed.
330
+
331
+ Returns:
332
+ int: Non-zero native pointer.
333
+
334
+ Raises:
335
+ BethkitClosedError: If :meth:`close` has already been called.
336
+ """
337
+
338
+ if not self.__ptr:
339
+ raise BethkitClosedError("RecordView has already been closed.")
340
+ return self.__ptr
341
+
342
+ @classmethod
343
+ def new(
344
+ cls,
345
+ record: object,
346
+ sig: bytes | str,
347
+ *,
348
+ localized: bool = False,
349
+ ) -> RecordView:
350
+ """
351
+ Create a schema-decoded view of *record*.
352
+
353
+ Args:
354
+ record: An open :class:`~bethkit.Record` instance.
355
+ sig (bytes | str): Four-byte record-type signature used to
356
+ select the correct schema.
357
+ localized (bool): Whether to interpret string fields as
358
+ localisation IDs rather than inline strings. Defaults
359
+ to ``False``.
360
+
361
+ Returns:
362
+ RecordView: The decoded view.
363
+
364
+ Raises:
365
+ BethkitNativeError: If decoding fails.
366
+ BethkitClosedError: If *record* has already been closed.
367
+ ValueError: If *sig* is not exactly 4 bytes.
368
+ """
369
+
370
+ if isinstance(sig, str):
371
+ sig = sig.encode("ascii")
372
+ if len(sig) != 4:
373
+ raise ValueError("sig must be exactly 4 bytes")
374
+ lib = _ffi.load_lib()
375
+ buf = (ctypes.c_uint8 * 4)(*sig)
376
+ rec_ptr: int = getattr(record, "_ptr", 0)
377
+ if not rec_ptr:
378
+ raise BethkitClosedError(
379
+ "Record is closed or has no native pointer."
380
+ )
381
+ ptr = lib.bethkit_record_view_new(rec_ptr, buf, localized)
382
+ if not ptr:
383
+ _ffi.raise_last_error(lib)
384
+ return cls(ptr)
385
+
386
+ def close(self) -> None:
387
+ """
388
+ Release the native view handle.
389
+
390
+ Safe to call multiple times; subsequent calls are no-ops.
391
+ """
392
+
393
+ if self.__ptr:
394
+ _ffi.load_lib().bethkit_record_view_free(self.__ptr)
395
+ self.__ptr = 0
396
+ self._fields_cache = None
397
+
398
+ def __enter__(self) -> RecordView:
399
+ """
400
+ Return *self* for use as a context manager.
401
+
402
+ Returns:
403
+ RecordView: This instance.
404
+ """
405
+
406
+ return self
407
+
408
+ def __exit__(self, *_: object) -> None:
409
+ """Free the view when exiting the context."""
410
+
411
+ self.close()
412
+
413
+ def __del__(self) -> None:
414
+ """Free the native handle on garbage collection."""
415
+
416
+ try:
417
+ self.close()
418
+ except Exception:
419
+ pass
420
+
421
+ def field_count(self) -> int:
422
+ """
423
+ Return the number of decoded top-level fields in this view.
424
+
425
+ Returns:
426
+ int: Field count.
427
+
428
+ Raises:
429
+ BethkitClosedError: If this view has already been closed.
430
+ """
431
+
432
+ return _ffi.load_lib().bethkit_record_view_field_count(
433
+ self.__check_open()
434
+ )
435
+
436
+ def fields(self) -> list[NamedField]:
437
+ """
438
+ Return all top-level decoded fields, cached after the first call.
439
+
440
+ Returns:
441
+ list[NamedField]: Ordered list of named fields.
442
+
443
+ Raises:
444
+ BethkitClosedError: If this view has already been closed.
445
+ """
446
+
447
+ ptr = self.__check_open()
448
+ if self._fields_cache is None:
449
+ lib = _ffi.load_lib()
450
+ n = lib.bethkit_record_view_field_count(ptr)
451
+ result: list[NamedField] = []
452
+ for i in range(n):
453
+ nf_ptr = lib.bethkit_record_view_field_get(ptr, i)
454
+ if not nf_ptr:
455
+ continue
456
+ nf = nf_ptr.contents
457
+ name_raw: Optional[bytes] = nf.name
458
+ name = name_raw.decode("utf-8") if name_raw else ""
459
+ value = _decode_field_value(nf.value, lib)
460
+ result.append(NamedField(name=name, value=value))
461
+ self._fields_cache = result
462
+ return self._fields_cache
463
+
464
+ def __repr__(self) -> str:
465
+ """
466
+ Returns:
467
+ str: Developer-friendly representation with field count.
468
+ """
469
+
470
+ if not self.__ptr:
471
+ return "<RecordView closed>"
472
+ return f"<RecordView fields={self.field_count()}>"
@@ -0,0 +1,13 @@
1
+ """
2
+ Copyright (c) Modding Forge
3
+
4
+ Strings subpackage — reading and writing Bethesda localisation string tables.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from .strings import LocalizationSet, StringTable
9
+
10
+ __all__ = [
11
+ "LocalizationSet",
12
+ "StringTable",
13
+ ]