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.
- bethkit/__init__.py +102 -0
- bethkit/_error.py +85 -0
- bethkit/_ffi/__init__.py +44 -0
- bethkit/_ffi/_loader.py +568 -0
- bethkit/_ffi/_types.py +97 -0
- bethkit/archive/__init__.py +16 -0
- bethkit/archive/archive.py +694 -0
- bethkit/enums.py +139 -0
- bethkit/load_order.py +198 -0
- bethkit/plugin/__init__.py +22 -0
- bethkit/plugin/cache.py +251 -0
- bethkit/plugin/plugin.py +823 -0
- bethkit/plugin/writer.py +510 -0
- bethkit/py.typed +0 -0
- bethkit/schema/__init__.py +26 -0
- bethkit/schema/schema.py +472 -0
- bethkit/strings/__init__.py +13 -0
- bethkit/strings/strings.py +522 -0
- bethkit-1.0.0.dist-info/METADATA +155 -0
- bethkit-1.0.0.dist-info/RECORD +22 -0
- bethkit-1.0.0.dist-info/WHEEL +4 -0
- bethkit-1.0.0.dist-info/licenses/LICENSE +203 -0
bethkit/schema/schema.py
ADDED
|
@@ -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
|
+
]
|