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/plugin/plugin.py
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from collections.abc import Iterator
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Optional
|
|
10
|
+
|
|
11
|
+
from .. import _ffi
|
|
12
|
+
from .._error import BethkitClosedError, BethkitNativeError
|
|
13
|
+
from ..enums import Game, PluginKind
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SubRecord:
|
|
20
|
+
"""
|
|
21
|
+
A single sub-record field inside a :class:`Record`.
|
|
22
|
+
|
|
23
|
+
Sub-records are borrowed from the parent ``Record`` and become
|
|
24
|
+
invalid once the record is closed or freed.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_ptr: int
|
|
28
|
+
_parent: Record
|
|
29
|
+
|
|
30
|
+
def __init__(self, ptr: int, parent: Record) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Args:
|
|
33
|
+
ptr (int): Native pointer to the underlying sub-record object.
|
|
34
|
+
parent (Record): Owning record that keeps native memory alive.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
self._ptr = ptr
|
|
38
|
+
self._parent = parent
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def signature(self) -> bytes:
|
|
42
|
+
"""
|
|
43
|
+
Four-byte ASCII signature identifying the sub-record type.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
bytes: Four-byte signature (e.g. ``b"EDID"``).
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
BethkitNativeError: If the native call fails.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
lib = _ffi.load_lib()
|
|
53
|
+
buf = (ctypes.c_uint8 * 4)()
|
|
54
|
+
if lib.bethkit_subrecord_signature(self._ptr, buf) != 0:
|
|
55
|
+
_ffi.raise_last_error(lib)
|
|
56
|
+
return bytes(buf)
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def raw_bytes(self) -> bytes: # type: ignore[return]
|
|
60
|
+
"""
|
|
61
|
+
Raw byte content of the sub-record as stored in the plugin.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
bytes: Sub-record data, or ``b""`` if empty.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
lib = _ffi.load_lib()
|
|
68
|
+
sl = lib.bethkit_subrecord_bytes(self._ptr)
|
|
69
|
+
if not sl.ptr:
|
|
70
|
+
return b""
|
|
71
|
+
return bytes(ctypes.string_at(sl.ptr, sl.len))
|
|
72
|
+
|
|
73
|
+
def as_u8(self) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Interpret the sub-record data as an unsigned 8-bit integer.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
int: Decoded value.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
BethkitNativeError: If the data length does not match.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
lib = _ffi.load_lib()
|
|
85
|
+
out = ctypes.c_uint8()
|
|
86
|
+
if lib.bethkit_subrecord_as_u8(self._ptr, ctypes.byref(out)) != 0:
|
|
87
|
+
_ffi.raise_last_error(lib)
|
|
88
|
+
return out.value
|
|
89
|
+
|
|
90
|
+
def as_u16(self) -> int:
|
|
91
|
+
"""
|
|
92
|
+
Interpret the sub-record data as an unsigned 16-bit integer.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
int: Decoded value.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
BethkitNativeError: If the data length does not match.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
lib = _ffi.load_lib()
|
|
102
|
+
out = ctypes.c_uint16()
|
|
103
|
+
if lib.bethkit_subrecord_as_u16(self._ptr, ctypes.byref(out)) != 0:
|
|
104
|
+
_ffi.raise_last_error(lib)
|
|
105
|
+
return out.value
|
|
106
|
+
|
|
107
|
+
def as_u32(self) -> int:
|
|
108
|
+
"""
|
|
109
|
+
Interpret the sub-record data as an unsigned 32-bit integer.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
int: Decoded value.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
BethkitNativeError: If the data length does not match.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
lib = _ffi.load_lib()
|
|
119
|
+
out = ctypes.c_uint32()
|
|
120
|
+
if lib.bethkit_subrecord_as_u32(self._ptr, ctypes.byref(out)) != 0:
|
|
121
|
+
_ffi.raise_last_error(lib)
|
|
122
|
+
return out.value
|
|
123
|
+
|
|
124
|
+
def as_f32(self) -> float:
|
|
125
|
+
"""
|
|
126
|
+
Interpret the sub-record data as a 32-bit float.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
float: Decoded value.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
BethkitNativeError: If the data length does not match.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
lib = _ffi.load_lib()
|
|
136
|
+
out = ctypes.c_float()
|
|
137
|
+
if lib.bethkit_subrecord_as_f32(self._ptr, ctypes.byref(out)) != 0:
|
|
138
|
+
_ffi.raise_last_error(lib)
|
|
139
|
+
return out.value
|
|
140
|
+
|
|
141
|
+
def as_str(self) -> str:
|
|
142
|
+
"""
|
|
143
|
+
Interpret the sub-record data as a null-terminated UTF-8 string.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
str: Decoded string.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
BethkitNativeError: If the data is not a valid null-terminated
|
|
150
|
+
string.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
lib = _ffi.load_lib()
|
|
154
|
+
ptr = lib.bethkit_subrecord_as_zstring(self._ptr)
|
|
155
|
+
if not ptr:
|
|
156
|
+
_ffi.raise_last_error(lib)
|
|
157
|
+
try:
|
|
158
|
+
return ctypes.string_at(ptr).decode("utf-8")
|
|
159
|
+
finally:
|
|
160
|
+
lib.bethkit_zstring_free(ptr)
|
|
161
|
+
|
|
162
|
+
def __repr__(self) -> str:
|
|
163
|
+
"""
|
|
164
|
+
Returns:
|
|
165
|
+
str: Developer-friendly representation showing the signature.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
sig = self.signature.decode("ascii", errors="replace")
|
|
170
|
+
except BethkitNativeError:
|
|
171
|
+
sig = "?"
|
|
172
|
+
return f"<SubRecord {sig!r}>"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Record:
|
|
176
|
+
"""
|
|
177
|
+
A single plugin record containing sub-records.
|
|
178
|
+
|
|
179
|
+
Records are owned by their parent :class:`Group` or
|
|
180
|
+
:class:`PluginCache` and must not outlive it.
|
|
181
|
+
"""
|
|
182
|
+
|
|
183
|
+
_ptr: int
|
|
184
|
+
_parent: object
|
|
185
|
+
|
|
186
|
+
def __init__(self, ptr: int, parent: object) -> None:
|
|
187
|
+
"""
|
|
188
|
+
Args:
|
|
189
|
+
ptr (int): Native pointer to the underlying record object.
|
|
190
|
+
parent (object): Owner that keeps native memory alive.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
self._ptr = ptr
|
|
194
|
+
self._parent = parent
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def signature(self) -> bytes:
|
|
198
|
+
"""
|
|
199
|
+
Four-byte ASCII record type signature.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
bytes: Four-byte signature (e.g. ``b"NPC_"``).
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
BethkitNativeError: If the native call fails.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
lib = _ffi.load_lib()
|
|
209
|
+
buf = (ctypes.c_uint8 * 4)()
|
|
210
|
+
if lib.bethkit_record_signature(self._ptr, buf) != 0:
|
|
211
|
+
_ffi.raise_last_error(lib)
|
|
212
|
+
return bytes(buf)
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def form_id(self) -> int:
|
|
216
|
+
"""
|
|
217
|
+
Raw 32-bit FormID of the record as stored in the plugin.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
int: FormID value.
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
return _ffi.load_lib().bethkit_record_form_id(self._ptr)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def flags(self) -> int:
|
|
227
|
+
"""
|
|
228
|
+
Record header flags bitmask.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
int: Flags value.
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
return _ffi.load_lib().bethkit_record_flags(self._ptr)
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def form_version(self) -> int:
|
|
238
|
+
"""
|
|
239
|
+
Form version stored in the record header.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
int: Form version number.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
return _ffi.load_lib().bethkit_record_form_version(self._ptr)
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def editor_id(self) -> Optional[str]:
|
|
249
|
+
"""
|
|
250
|
+
Editor ID string (EDID sub-record), if present.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Optional[str]: The editor ID, or ``None`` if absent.
|
|
254
|
+
"""
|
|
255
|
+
|
|
256
|
+
lib = _ffi.load_lib()
|
|
257
|
+
ptr = lib.bethkit_record_editor_id(self._ptr)
|
|
258
|
+
if not ptr:
|
|
259
|
+
return None
|
|
260
|
+
try:
|
|
261
|
+
return ctypes.string_at(ptr).decode("utf-8")
|
|
262
|
+
finally:
|
|
263
|
+
lib.bethkit_record_editor_id_free(ptr)
|
|
264
|
+
|
|
265
|
+
def subrecord_count(self) -> int:
|
|
266
|
+
"""
|
|
267
|
+
Return the number of sub-records in this record.
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
int: Sub-record count.
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
BethkitNativeError: If the native call fails.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
lib = _ffi.load_lib()
|
|
277
|
+
n = lib.bethkit_record_subrecord_count(self._ptr)
|
|
278
|
+
if n < 0:
|
|
279
|
+
_ffi.raise_last_error(lib)
|
|
280
|
+
return n
|
|
281
|
+
|
|
282
|
+
def subrecord_at(self, index: int) -> SubRecord:
|
|
283
|
+
"""
|
|
284
|
+
Return the sub-record at the given index.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
index (int): Zero-based sub-record index.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
SubRecord: Borrowed sub-record.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
BethkitNativeError: If *index* is out of range.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
lib = _ffi.load_lib()
|
|
297
|
+
ptr = lib.bethkit_record_subrecord_get(self._ptr, index)
|
|
298
|
+
if not ptr:
|
|
299
|
+
_ffi.raise_last_error(lib)
|
|
300
|
+
return SubRecord(ptr, self)
|
|
301
|
+
|
|
302
|
+
def find_subrecord(
|
|
303
|
+
self, sig: bytes | str
|
|
304
|
+
) -> Optional[SubRecord]:
|
|
305
|
+
"""
|
|
306
|
+
Find the first sub-record matching the given 4-byte signature.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
sig (bytes | str): Four-byte signature to search for.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Optional[SubRecord]: The first matching sub-record, or ``None``.
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
ValueError: If *sig* is not exactly 4 bytes.
|
|
316
|
+
"""
|
|
317
|
+
|
|
318
|
+
if isinstance(sig, str):
|
|
319
|
+
sig = sig.encode("ascii")
|
|
320
|
+
if len(sig) != 4:
|
|
321
|
+
raise ValueError("sig must be exactly 4 bytes")
|
|
322
|
+
lib = _ffi.load_lib()
|
|
323
|
+
buf = (ctypes.c_uint8 * 4)(*sig)
|
|
324
|
+
ptr = lib.bethkit_record_subrecord_find(self._ptr, buf)
|
|
325
|
+
if not ptr:
|
|
326
|
+
return None
|
|
327
|
+
return SubRecord(ptr, self)
|
|
328
|
+
|
|
329
|
+
def __iter__(self) -> Iterator[SubRecord]:
|
|
330
|
+
"""
|
|
331
|
+
Iterate over all sub-records in this record.
|
|
332
|
+
|
|
333
|
+
Yields:
|
|
334
|
+
SubRecord: Each sub-record in order.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
for i in range(self.subrecord_count()):
|
|
338
|
+
yield self.subrecord_at(i)
|
|
339
|
+
|
|
340
|
+
def __repr__(self) -> str:
|
|
341
|
+
"""
|
|
342
|
+
Returns:
|
|
343
|
+
str: Developer-friendly representation with signature and FormID.
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
try:
|
|
347
|
+
sig = self.signature.decode("ascii", errors="replace")
|
|
348
|
+
fid = self.form_id
|
|
349
|
+
except BethkitNativeError:
|
|
350
|
+
return "<Record ?>"
|
|
351
|
+
return f"<Record {sig!r} FormID=0x{fid:08X}>"
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class Group:
|
|
355
|
+
"""
|
|
356
|
+
A top-level group inside a plugin, containing records or sub-groups.
|
|
357
|
+
|
|
358
|
+
Groups are the primary organisational unit in Bethesda plugin files.
|
|
359
|
+
They may contain :class:`Record` children or nested :class:`Group`
|
|
360
|
+
children.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
_ptr: int
|
|
364
|
+
_parent: Plugin | Group
|
|
365
|
+
|
|
366
|
+
def __init__(self, ptr: int, parent: Plugin | Group) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Args:
|
|
369
|
+
ptr (int): Native pointer to the underlying group object.
|
|
370
|
+
parent (Plugin | Group): Owner that keeps native memory alive.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
self._ptr = ptr
|
|
374
|
+
self._parent = parent
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def group_type(self) -> int:
|
|
378
|
+
"""
|
|
379
|
+
Numeric group type code as defined in the plugin format.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
int: Group type (e.g. ``0`` for top-level groups).
|
|
383
|
+
|
|
384
|
+
Raises:
|
|
385
|
+
BethkitNativeError: If the native call fails.
|
|
386
|
+
"""
|
|
387
|
+
|
|
388
|
+
lib = _ffi.load_lib()
|
|
389
|
+
t = lib.bethkit_group_type(self._ptr)
|
|
390
|
+
if t < 0:
|
|
391
|
+
_ffi.raise_last_error(lib)
|
|
392
|
+
return t
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def child_count(self) -> int:
|
|
396
|
+
"""
|
|
397
|
+
Total number of direct children (records and sub-groups).
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
int: Child count.
|
|
401
|
+
"""
|
|
402
|
+
|
|
403
|
+
return _ffi.load_lib().bethkit_group_child_count(self._ptr)
|
|
404
|
+
|
|
405
|
+
def child_is_record(self, index: int) -> bool:
|
|
406
|
+
"""
|
|
407
|
+
Return whether the child at *index* is a record (vs. a group).
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
index (int): Zero-based child index.
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
bool: ``True`` if the child is a :class:`Record`.
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
return bool(
|
|
417
|
+
_ffi.load_lib().bethkit_group_child_is_record(self._ptr, index)
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def child_as_record(
|
|
421
|
+
self, index: int
|
|
422
|
+
) -> Optional[Record]:
|
|
423
|
+
"""
|
|
424
|
+
Return the child at *index* as a :class:`Record`.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
index (int): Zero-based child index.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
Optional[Record]: The child record, or ``None`` if the child is
|
|
431
|
+
a group or the index is out of range.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
lib = _ffi.load_lib()
|
|
435
|
+
ptr = lib.bethkit_group_child_as_record(self._ptr, index)
|
|
436
|
+
if not ptr:
|
|
437
|
+
return None
|
|
438
|
+
return Record(ptr, self)
|
|
439
|
+
|
|
440
|
+
def child_as_group(
|
|
441
|
+
self, index: int
|
|
442
|
+
) -> Optional[Group]:
|
|
443
|
+
"""
|
|
444
|
+
Return the child at *index* as a :class:`Group`.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
index (int): Zero-based child index.
|
|
448
|
+
|
|
449
|
+
Returns:
|
|
450
|
+
Optional[Group]: The child group, or ``None`` if the child is
|
|
451
|
+
a record or the index is out of range.
|
|
452
|
+
"""
|
|
453
|
+
|
|
454
|
+
lib = _ffi.load_lib()
|
|
455
|
+
ptr = lib.bethkit_group_child_as_group(self._ptr, index)
|
|
456
|
+
if not ptr:
|
|
457
|
+
return None
|
|
458
|
+
return Group(ptr, self)
|
|
459
|
+
|
|
460
|
+
def __iter__(self) -> Iterator[Record | Group]:
|
|
461
|
+
"""
|
|
462
|
+
Iterate over all direct children of this group.
|
|
463
|
+
|
|
464
|
+
Yields:
|
|
465
|
+
Record | Group: Each child in order.
|
|
466
|
+
"""
|
|
467
|
+
|
|
468
|
+
lib = _ffi.load_lib()
|
|
469
|
+
for i in range(self.child_count):
|
|
470
|
+
if lib.bethkit_group_child_is_record(self._ptr, i):
|
|
471
|
+
ptr = lib.bethkit_group_child_as_record(self._ptr, i)
|
|
472
|
+
if ptr:
|
|
473
|
+
yield Record(ptr, self)
|
|
474
|
+
else:
|
|
475
|
+
ptr = lib.bethkit_group_child_as_group(self._ptr, i)
|
|
476
|
+
if ptr:
|
|
477
|
+
yield Group(ptr, self)
|
|
478
|
+
|
|
479
|
+
def __repr__(self) -> str:
|
|
480
|
+
"""
|
|
481
|
+
Returns:
|
|
482
|
+
str: Developer-friendly representation showing type and count.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
return (
|
|
487
|
+
f"<Group type={self.group_type} children={self.child_count}>"
|
|
488
|
+
)
|
|
489
|
+
except BethkitNativeError:
|
|
490
|
+
return "<Group ?>"
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class Plugin:
|
|
495
|
+
"""
|
|
496
|
+
An open Bethesda plugin file (ESP, ESM, or ESL).
|
|
497
|
+
|
|
498
|
+
Use as a context manager to guarantee that the native handle is freed
|
|
499
|
+
even on error::
|
|
500
|
+
|
|
501
|
+
with Plugin.open(Path("Skyrim.esm"), Game.SKYRIM_SE) as p:
|
|
502
|
+
for group in p:
|
|
503
|
+
for child in group:
|
|
504
|
+
if isinstance(child, Record):
|
|
505
|
+
print(child.editor_id)
|
|
506
|
+
"""
|
|
507
|
+
|
|
508
|
+
__ptr: int
|
|
509
|
+
|
|
510
|
+
def __init__(self, ptr: int) -> None:
|
|
511
|
+
"""
|
|
512
|
+
Args:
|
|
513
|
+
ptr (int): Native handle returned by the FFI open call.
|
|
514
|
+
"""
|
|
515
|
+
|
|
516
|
+
self.__ptr = ptr
|
|
517
|
+
|
|
518
|
+
def __check_open(self) -> int:
|
|
519
|
+
"""
|
|
520
|
+
Return the native pointer, raising if the handle has been closed.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
int: Valid native pointer.
|
|
524
|
+
|
|
525
|
+
Raises:
|
|
526
|
+
BethkitClosedError: If the plugin has been closed or transferred.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
if not self.__ptr:
|
|
530
|
+
raise BethkitClosedError("Plugin is closed")
|
|
531
|
+
return self.__ptr
|
|
532
|
+
|
|
533
|
+
@classmethod
|
|
534
|
+
def open(cls, path: Path, game: Game) -> Plugin:
|
|
535
|
+
"""
|
|
536
|
+
Open a plugin file from disk.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
path (Path): Filesystem path to the ``.esp``, ``.esm``, or
|
|
540
|
+
``.esl`` file.
|
|
541
|
+
game (Game): Target game; selects the correct format variant.
|
|
542
|
+
|
|
543
|
+
Returns:
|
|
544
|
+
Plugin: A new ``Plugin`` wrapping the open file.
|
|
545
|
+
|
|
546
|
+
Raises:
|
|
547
|
+
BethkitNativeError: If the file cannot be opened or parsed.
|
|
548
|
+
"""
|
|
549
|
+
|
|
550
|
+
lib = _ffi.load_lib()
|
|
551
|
+
ptr = lib.bethkit_plugin_open(_ffi.enc(path), int(game))
|
|
552
|
+
if not ptr:
|
|
553
|
+
_ffi.raise_last_error(lib)
|
|
554
|
+
return cls(ptr)
|
|
555
|
+
|
|
556
|
+
@classmethod
|
|
557
|
+
def from_bytes(cls, data: bytes, game: Game) -> Plugin:
|
|
558
|
+
"""
|
|
559
|
+
Parse a plugin from an in-memory byte buffer.
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
data (bytes): Raw plugin file contents.
|
|
563
|
+
game (Game): Target game; selects the correct format variant.
|
|
564
|
+
|
|
565
|
+
Returns:
|
|
566
|
+
Plugin: A new ``Plugin`` parsed from *data*.
|
|
567
|
+
|
|
568
|
+
Raises:
|
|
569
|
+
BethkitNativeError: If parsing fails.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
lib = _ffi.load_lib()
|
|
573
|
+
buf = (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
|
574
|
+
ptr = lib.bethkit_plugin_open_from_bytes(buf, len(data), int(game))
|
|
575
|
+
if not ptr:
|
|
576
|
+
_ffi.raise_last_error(lib)
|
|
577
|
+
return cls(ptr)
|
|
578
|
+
|
|
579
|
+
def close(self) -> None:
|
|
580
|
+
"""
|
|
581
|
+
Release the native plugin handle.
|
|
582
|
+
|
|
583
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
584
|
+
"""
|
|
585
|
+
|
|
586
|
+
if self.__ptr:
|
|
587
|
+
_ffi.load_lib().bethkit_plugin_free(self.__ptr)
|
|
588
|
+
self.__ptr = 0
|
|
589
|
+
|
|
590
|
+
def _transfer_ptr(self) -> int:
|
|
591
|
+
"""
|
|
592
|
+
Transfer ownership of the native handle to the caller.
|
|
593
|
+
|
|
594
|
+
After this call the wrapper is closed (``__ptr`` is set to ``0``).
|
|
595
|
+
Called by :class:`PluginCache` when it takes ownership of the plugin.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
int: The raw native pointer.
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
BethkitClosedError: If the plugin has already been closed or
|
|
602
|
+
transferred.
|
|
603
|
+
"""
|
|
604
|
+
|
|
605
|
+
ptr = self.__check_open()
|
|
606
|
+
self.__ptr = 0
|
|
607
|
+
return ptr
|
|
608
|
+
|
|
609
|
+
def __enter__(self) -> Plugin:
|
|
610
|
+
"""Return *self* for use as a context manager."""
|
|
611
|
+
|
|
612
|
+
return self
|
|
613
|
+
|
|
614
|
+
def __exit__(self, *_: object) -> None:
|
|
615
|
+
"""Close the plugin when exiting the context."""
|
|
616
|
+
|
|
617
|
+
self.close()
|
|
618
|
+
|
|
619
|
+
def __del__(self) -> None:
|
|
620
|
+
"""Free the native handle on garbage collection."""
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
self.close()
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
|
|
627
|
+
@property
|
|
628
|
+
def kind(self) -> PluginKind:
|
|
629
|
+
"""
|
|
630
|
+
Plugin type as declared in the file header.
|
|
631
|
+
|
|
632
|
+
Returns:
|
|
633
|
+
PluginKind: ``FULL``, ``LIGHT``, or ``OVERLAY``.
|
|
634
|
+
|
|
635
|
+
Raises:
|
|
636
|
+
BethkitClosedError: If the plugin has been closed.
|
|
637
|
+
"""
|
|
638
|
+
|
|
639
|
+
return PluginKind(_ffi.load_lib().bethkit_plugin_kind(
|
|
640
|
+
self.__check_open()
|
|
641
|
+
))
|
|
642
|
+
|
|
643
|
+
@property
|
|
644
|
+
def is_localized(self) -> bool:
|
|
645
|
+
"""
|
|
646
|
+
Whether the plugin uses external string localisation files.
|
|
647
|
+
|
|
648
|
+
Returns:
|
|
649
|
+
bool: ``True`` if the plugin sets the localised flag.
|
|
650
|
+
|
|
651
|
+
Raises:
|
|
652
|
+
BethkitClosedError: If the plugin has been closed.
|
|
653
|
+
"""
|
|
654
|
+
|
|
655
|
+
return bool(_ffi.load_lib().bethkit_plugin_is_localized(
|
|
656
|
+
self.__check_open()
|
|
657
|
+
))
|
|
658
|
+
|
|
659
|
+
@property
|
|
660
|
+
def description(self) -> Optional[str]:
|
|
661
|
+
"""
|
|
662
|
+
Plugin description from the SNAM sub-record, if present.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
Optional[str]: Description string, or ``None`` if absent.
|
|
666
|
+
|
|
667
|
+
Raises:
|
|
668
|
+
BethkitClosedError: If the plugin has been closed.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
lib = _ffi.load_lib()
|
|
672
|
+
raw: Optional[bytes] = lib.bethkit_plugin_description(
|
|
673
|
+
self.__check_open()
|
|
674
|
+
)
|
|
675
|
+
return raw.decode("utf-8") if raw else None
|
|
676
|
+
|
|
677
|
+
@property
|
|
678
|
+
def master_count(self) -> int:
|
|
679
|
+
"""
|
|
680
|
+
Number of master plugin dependencies declared in the header.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
int: Master count.
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
BethkitClosedError: If the plugin has been closed.
|
|
687
|
+
"""
|
|
688
|
+
|
|
689
|
+
return _ffi.load_lib().bethkit_plugin_master_count(
|
|
690
|
+
self.__check_open()
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
def master_at(self, index: int) -> str:
|
|
694
|
+
"""
|
|
695
|
+
Return the master plugin name at the given index.
|
|
696
|
+
|
|
697
|
+
Args:
|
|
698
|
+
index (int): Zero-based master index.
|
|
699
|
+
|
|
700
|
+
Returns:
|
|
701
|
+
str: Master file name (e.g. ``"Skyrim.esm"``).
|
|
702
|
+
|
|
703
|
+
Raises:
|
|
704
|
+
BethkitClosedError: If the plugin has been closed.
|
|
705
|
+
BethkitNativeError: If *index* is out of range.
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
lib = _ffi.load_lib()
|
|
709
|
+
raw: Optional[bytes] = lib.bethkit_plugin_master_get(
|
|
710
|
+
self.__check_open(), index
|
|
711
|
+
)
|
|
712
|
+
if raw is None:
|
|
713
|
+
_ffi.raise_last_error(lib)
|
|
714
|
+
return raw.decode("utf-8") # type: ignore[union-attr]
|
|
715
|
+
|
|
716
|
+
@property
|
|
717
|
+
def masters(self) -> list[str]:
|
|
718
|
+
"""
|
|
719
|
+
All master plugin names in load order.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
list[str]: Ordered list of master file names.
|
|
723
|
+
|
|
724
|
+
Raises:
|
|
725
|
+
BethkitClosedError: If the plugin has been closed.
|
|
726
|
+
"""
|
|
727
|
+
|
|
728
|
+
return [self.master_at(i) for i in range(self.master_count)]
|
|
729
|
+
|
|
730
|
+
@property
|
|
731
|
+
def group_count(self) -> int:
|
|
732
|
+
"""
|
|
733
|
+
Number of top-level groups in the plugin.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
int: Group count.
|
|
737
|
+
|
|
738
|
+
Raises:
|
|
739
|
+
BethkitClosedError: If the plugin has been closed.
|
|
740
|
+
"""
|
|
741
|
+
|
|
742
|
+
return _ffi.load_lib().bethkit_plugin_group_count(
|
|
743
|
+
self.__check_open()
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
def group_at(self, index: int) -> Group:
|
|
747
|
+
"""
|
|
748
|
+
Return the top-level group at the given index.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
index (int): Zero-based group index.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Group: Borrowed group object.
|
|
755
|
+
|
|
756
|
+
Raises:
|
|
757
|
+
BethkitClosedError: If the plugin has been closed.
|
|
758
|
+
BethkitNativeError: If *index* is out of range.
|
|
759
|
+
"""
|
|
760
|
+
|
|
761
|
+
lib = _ffi.load_lib()
|
|
762
|
+
ptr = lib.bethkit_plugin_group_get(self.__check_open(), index)
|
|
763
|
+
if not ptr:
|
|
764
|
+
_ffi.raise_last_error(lib)
|
|
765
|
+
return Group(ptr, self)
|
|
766
|
+
|
|
767
|
+
@property
|
|
768
|
+
def groups(self) -> Iterator[Group]:
|
|
769
|
+
"""
|
|
770
|
+
Iterate over all top-level groups.
|
|
771
|
+
|
|
772
|
+
Yields:
|
|
773
|
+
Group: Each top-level group in order.
|
|
774
|
+
|
|
775
|
+
Raises:
|
|
776
|
+
BethkitClosedError: If the plugin has been closed.
|
|
777
|
+
"""
|
|
778
|
+
|
|
779
|
+
for i in range(self.group_count):
|
|
780
|
+
yield self.group_at(i)
|
|
781
|
+
|
|
782
|
+
def __iter__(self) -> Iterator[Group]:
|
|
783
|
+
"""
|
|
784
|
+
Iterate over all top-level groups (alias for :attr:`groups`).
|
|
785
|
+
|
|
786
|
+
Yields:
|
|
787
|
+
Group: Each top-level group in order.
|
|
788
|
+
"""
|
|
789
|
+
|
|
790
|
+
return self.groups
|
|
791
|
+
|
|
792
|
+
def find_record(self, form_id: int) -> Optional[Record]:
|
|
793
|
+
"""
|
|
794
|
+
Search for a record by its raw 32-bit FormID.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
form_id (int): The raw FormID to look up.
|
|
798
|
+
|
|
799
|
+
Returns:
|
|
800
|
+
Optional[Record]: The matching record, or ``None`` if not found.
|
|
801
|
+
|
|
802
|
+
Raises:
|
|
803
|
+
BethkitClosedError: If the plugin has been closed.
|
|
804
|
+
"""
|
|
805
|
+
|
|
806
|
+
lib = _ffi.load_lib()
|
|
807
|
+
ptr = lib.bethkit_plugin_find_record(self.__check_open(), form_id)
|
|
808
|
+
if not ptr:
|
|
809
|
+
return None
|
|
810
|
+
return Record(ptr, self)
|
|
811
|
+
|
|
812
|
+
def __repr__(self) -> str:
|
|
813
|
+
"""
|
|
814
|
+
Returns:
|
|
815
|
+
str: Developer-friendly representation with kind and group count.
|
|
816
|
+
"""
|
|
817
|
+
|
|
818
|
+
if not self.__ptr:
|
|
819
|
+
return "<Plugin closed>"
|
|
820
|
+
try:
|
|
821
|
+
return f"<Plugin kind={self.kind.name} groups={self.group_count}>"
|
|
822
|
+
except BethkitNativeError:
|
|
823
|
+
return "<Plugin ?>"
|