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
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from .. import _ffi
|
|
11
|
+
from .._error import BethkitClosedError
|
|
12
|
+
from ..enums import StringFileKind
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _buf_from_bytes(data: bytes) -> ctypes.Array[ctypes.c_uint8]:
|
|
16
|
+
"""
|
|
17
|
+
Wrap *data* in a ctypes ``c_uint8`` array for FFI calls.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
data (bytes): Byte sequence to wrap.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
ctypes.Array: A ``c_uint8`` array backed by a copy of *data*.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
return (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class StringTable:
|
|
30
|
+
"""
|
|
31
|
+
A Bethesda string localisation table (``.STRINGS``, ``.DLSTRINGS``,
|
|
32
|
+
or ``.ILSTRINGS`` files).
|
|
33
|
+
|
|
34
|
+
String tables map numeric IDs to UTF-8 string payloads. They can be
|
|
35
|
+
loaded from disk with :meth:`open`, or created fresh with :meth:`new`
|
|
36
|
+
and written back with :meth:`write_to_file`.
|
|
37
|
+
|
|
38
|
+
Use as a context manager to guarantee that the native handle is
|
|
39
|
+
freed::
|
|
40
|
+
|
|
41
|
+
with StringTable.open(path) as tbl:
|
|
42
|
+
text = tbl.get_str(0x0001)
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
__ptr: int
|
|
46
|
+
|
|
47
|
+
def __init__(self, ptr: int) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Args:
|
|
50
|
+
ptr (int): Native handle returned by the FFI open/new call.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
self.__ptr = ptr
|
|
54
|
+
|
|
55
|
+
def __check_open(self) -> int:
|
|
56
|
+
"""
|
|
57
|
+
Return the native pointer, raising if the handle has been closed.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
int: Valid native pointer.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
BethkitClosedError: If the table has been closed.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
if not self.__ptr:
|
|
67
|
+
raise BethkitClosedError("StringTable is closed")
|
|
68
|
+
return self.__ptr
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def new(cls, kind: StringFileKind) -> StringTable:
|
|
72
|
+
"""
|
|
73
|
+
Create an empty string table of the given kind.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
kind (StringFileKind): Type of string file to create.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
StringTable: A new, empty ``StringTable``.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
BethkitNativeError: If the native table cannot be created.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
lib = _ffi.load_lib()
|
|
86
|
+
ptr = lib.bethkit_string_table_new(int(kind))
|
|
87
|
+
if not ptr:
|
|
88
|
+
_ffi.raise_last_error(lib)
|
|
89
|
+
return cls(ptr)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def open(cls, path: Path) -> StringTable:
|
|
93
|
+
"""
|
|
94
|
+
Open a string table file from disk.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
path (Path): Filesystem path to the string file.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
StringTable: A new ``StringTable`` loaded from *path*.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
BethkitNativeError: If the file cannot be opened or parsed.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
lib = _ffi.load_lib()
|
|
107
|
+
ptr = lib.bethkit_string_table_open(_ffi.enc(path))
|
|
108
|
+
if not ptr:
|
|
109
|
+
_ffi.raise_last_error(lib)
|
|
110
|
+
return cls(ptr)
|
|
111
|
+
|
|
112
|
+
def close(self) -> None:
|
|
113
|
+
"""
|
|
114
|
+
Release the native string-table handle.
|
|
115
|
+
|
|
116
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if self.__ptr:
|
|
120
|
+
_ffi.load_lib().bethkit_string_table_free(self.__ptr)
|
|
121
|
+
self.__ptr = 0
|
|
122
|
+
|
|
123
|
+
def __enter__(self) -> StringTable:
|
|
124
|
+
"""Return *self* for use as a context manager."""
|
|
125
|
+
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def __exit__(self, *_: object) -> None:
|
|
129
|
+
"""Free the table when exiting the context."""
|
|
130
|
+
|
|
131
|
+
self.close()
|
|
132
|
+
|
|
133
|
+
def __del__(self) -> None:
|
|
134
|
+
"""Free the native handle on garbage collection."""
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
self.close()
|
|
138
|
+
except Exception:
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def kind(self) -> StringFileKind:
|
|
143
|
+
"""
|
|
144
|
+
The string-file format of this table.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
StringFileKind: ``STRINGS``, ``DL_STRINGS``, or
|
|
148
|
+
``IL_STRINGS``.
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
BethkitClosedError: If the table has been closed.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
return StringFileKind(
|
|
155
|
+
_ffi.load_lib().bethkit_string_table_kind(self.__check_open())
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def __len__(self) -> int:
|
|
159
|
+
"""
|
|
160
|
+
Returns:
|
|
161
|
+
int: Number of entries in the table.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
return _ffi.load_lib().bethkit_string_table_len(
|
|
165
|
+
self.__check_open()
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def get(self, id: int) -> Optional[bytes]:
|
|
169
|
+
"""
|
|
170
|
+
Retrieve a string entry as raw bytes by its ID.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
id (int): Numeric string ID.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Optional[bytes]: Raw string bytes, or ``None`` if not found.
|
|
177
|
+
|
|
178
|
+
Raises:
|
|
179
|
+
BethkitClosedError: If the table has been closed.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
lib = _ffi.load_lib()
|
|
183
|
+
out_len = ctypes.c_size_t(0)
|
|
184
|
+
ptr = lib.bethkit_string_table_get(
|
|
185
|
+
self.__check_open(), id, ctypes.byref(out_len)
|
|
186
|
+
)
|
|
187
|
+
if not ptr:
|
|
188
|
+
return None
|
|
189
|
+
return bytes(ctypes.string_at(ptr, out_len.value))
|
|
190
|
+
|
|
191
|
+
def get_str(self, id: int) -> Optional[str]:
|
|
192
|
+
"""
|
|
193
|
+
Retrieve a string entry decoded as UTF-8 by its ID.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
id (int): Numeric string ID.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Optional[str]: Decoded string without trailing null, or
|
|
200
|
+
``None`` if not found.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
raw = self.get(id)
|
|
204
|
+
if raw is None:
|
|
205
|
+
return None
|
|
206
|
+
return raw.rstrip(b"\x00").decode("utf-8")
|
|
207
|
+
|
|
208
|
+
def insert(self, id: int, data: bytes) -> None:
|
|
209
|
+
"""
|
|
210
|
+
Insert or overwrite an entry with the given ID.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
id (int): Numeric string ID.
|
|
214
|
+
data (bytes): String payload (may include trailing null).
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
BethkitClosedError: If the table has been closed.
|
|
218
|
+
BethkitNativeError: If the native call fails.
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
lib = _ffi.load_lib()
|
|
222
|
+
buf = _buf_from_bytes(data)
|
|
223
|
+
if lib.bethkit_string_table_insert(
|
|
224
|
+
self.__check_open(), id, buf, len(data)
|
|
225
|
+
) != 0:
|
|
226
|
+
_ffi.raise_last_error(lib)
|
|
227
|
+
|
|
228
|
+
def insert_new(self, data: bytes) -> int:
|
|
229
|
+
"""
|
|
230
|
+
Insert a new entry and return the auto-assigned ID.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
data (bytes): String payload.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
int: The ID assigned to the new entry.
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
BethkitClosedError: If the table has been closed.
|
|
240
|
+
BethkitNativeError: If the native call fails.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
lib = _ffi.load_lib()
|
|
244
|
+
buf = _buf_from_bytes(data)
|
|
245
|
+
out_id = ctypes.c_uint32(0)
|
|
246
|
+
if (
|
|
247
|
+
lib.bethkit_string_table_insert_new(
|
|
248
|
+
self.__check_open(), buf, len(data), ctypes.byref(out_id)
|
|
249
|
+
)
|
|
250
|
+
!= 0
|
|
251
|
+
):
|
|
252
|
+
_ffi.raise_last_error(lib)
|
|
253
|
+
return out_id.value
|
|
254
|
+
|
|
255
|
+
def remove(self, id: int) -> bool:
|
|
256
|
+
"""
|
|
257
|
+
Remove the entry with the given ID.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
id (int): Numeric string ID to remove.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
bool: ``True`` if the entry existed and was removed.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
return bool(
|
|
267
|
+
_ffi.load_lib().bethkit_string_table_remove(
|
|
268
|
+
self.__check_open(), id
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def write_to_file(self, path: Path) -> None:
|
|
273
|
+
"""
|
|
274
|
+
Serialise the table and write it to *path* on disk.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
path (Path): Destination file path.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
BethkitClosedError: If the table has been closed.
|
|
281
|
+
BethkitNativeError: If serialisation or the write fails.
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
lib = _ffi.load_lib()
|
|
285
|
+
if lib.bethkit_string_table_write_to_file(
|
|
286
|
+
self.__check_open(), _ffi.enc(path)
|
|
287
|
+
) != 0:
|
|
288
|
+
_ffi.raise_last_error(lib)
|
|
289
|
+
|
|
290
|
+
def __repr__(self) -> str:
|
|
291
|
+
"""
|
|
292
|
+
Returns:
|
|
293
|
+
str: Developer-friendly representation with kind and entry
|
|
294
|
+
count.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
return f"<StringTable kind={self.kind.name} len={len(self)}>"
|
|
299
|
+
except BethkitClosedError:
|
|
300
|
+
return "<StringTable closed>"
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
class LocalizationSet:
|
|
304
|
+
"""
|
|
305
|
+
A combined set of all three localisation tables for a single plugin.
|
|
306
|
+
|
|
307
|
+
A :class:`LocalizationSet` bundles the ``.STRINGS``, ``.DLSTRINGS``,
|
|
308
|
+
and ``.ILSTRINGS`` files for a given language. Load them together
|
|
309
|
+
with :meth:`open`, or create an empty set with :meth:`new` and
|
|
310
|
+
populate it manually.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
__ptr: int
|
|
314
|
+
|
|
315
|
+
def __init__(self, ptr: int) -> None:
|
|
316
|
+
"""
|
|
317
|
+
Args:
|
|
318
|
+
ptr (int): Native handle returned by the FFI open/new call.
|
|
319
|
+
"""
|
|
320
|
+
|
|
321
|
+
self.__ptr = ptr
|
|
322
|
+
|
|
323
|
+
def __check_open(self) -> int:
|
|
324
|
+
"""
|
|
325
|
+
Return the native pointer, raising if the handle has been closed.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
int: Valid native pointer.
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
BethkitClosedError: If the set has been closed.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
if not self.__ptr:
|
|
335
|
+
raise BethkitClosedError("LocalizationSet is closed")
|
|
336
|
+
return self.__ptr
|
|
337
|
+
|
|
338
|
+
@classmethod
|
|
339
|
+
def new(cls) -> LocalizationSet:
|
|
340
|
+
"""
|
|
341
|
+
Create an empty localisation set.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
LocalizationSet: A new, empty set with no strings loaded.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
BethkitNativeError: If the native set cannot be created.
|
|
348
|
+
"""
|
|
349
|
+
|
|
350
|
+
lib = _ffi.load_lib()
|
|
351
|
+
ptr = lib.bethkit_localization_set_new()
|
|
352
|
+
if not ptr:
|
|
353
|
+
_ffi.raise_last_error(lib)
|
|
354
|
+
return cls(ptr)
|
|
355
|
+
|
|
356
|
+
@classmethod
|
|
357
|
+
def open(
|
|
358
|
+
cls, plugin_path: Path, language: str
|
|
359
|
+
) -> LocalizationSet:
|
|
360
|
+
"""
|
|
361
|
+
Load all localisation files for the given plugin and language.
|
|
362
|
+
|
|
363
|
+
The method looks for ``<plugin_stem>_<language>.STRINGS`` and
|
|
364
|
+
sibling ``.DLSTRINGS`` / ``.ILSTRINGS`` files next to the plugin.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
plugin_path (Path): Filesystem path to the plugin file.
|
|
368
|
+
language (str): BCP 47-style language code
|
|
369
|
+
(e.g. ``"english"``).
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
LocalizationSet: The loaded set.
|
|
373
|
+
|
|
374
|
+
Raises:
|
|
375
|
+
BethkitNativeError: If any required string file cannot be
|
|
376
|
+
opened.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
lib = _ffi.load_lib()
|
|
380
|
+
ptr = lib.bethkit_localization_set_open(
|
|
381
|
+
_ffi.enc(plugin_path), _ffi.senc(language)
|
|
382
|
+
)
|
|
383
|
+
if not ptr:
|
|
384
|
+
_ffi.raise_last_error(lib)
|
|
385
|
+
return cls(ptr)
|
|
386
|
+
|
|
387
|
+
def close(self) -> None:
|
|
388
|
+
"""
|
|
389
|
+
Release the native localisation-set handle.
|
|
390
|
+
|
|
391
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
if self.__ptr:
|
|
395
|
+
_ffi.load_lib().bethkit_localization_set_free(self.__ptr)
|
|
396
|
+
self.__ptr = 0
|
|
397
|
+
|
|
398
|
+
def __enter__(self) -> LocalizationSet:
|
|
399
|
+
"""Return *self* for use as a context manager."""
|
|
400
|
+
|
|
401
|
+
return self
|
|
402
|
+
|
|
403
|
+
def __exit__(self, *_: object) -> None:
|
|
404
|
+
"""Free the set when exiting the context."""
|
|
405
|
+
|
|
406
|
+
self.close()
|
|
407
|
+
|
|
408
|
+
def __del__(self) -> None:
|
|
409
|
+
"""Free the native handle on garbage collection."""
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
self.close()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
def get(
|
|
417
|
+
self, kind: StringFileKind, id: int
|
|
418
|
+
) -> Optional[bytes]:
|
|
419
|
+
"""
|
|
420
|
+
Retrieve a string from the specified sub-table by its ID.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
kind (StringFileKind): Which sub-table to query.
|
|
424
|
+
id (int): Numeric string ID.
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Optional[bytes]: Raw string bytes, or ``None`` if not found.
|
|
428
|
+
|
|
429
|
+
Raises:
|
|
430
|
+
BethkitClosedError: If the set has been closed.
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
lib = _ffi.load_lib()
|
|
434
|
+
out_len = ctypes.c_size_t(0)
|
|
435
|
+
ptr = lib.bethkit_localization_set_get(
|
|
436
|
+
self.__check_open(), int(kind), id, ctypes.byref(out_len)
|
|
437
|
+
)
|
|
438
|
+
if not ptr:
|
|
439
|
+
return None
|
|
440
|
+
return bytes(ctypes.string_at(ptr, out_len.value))
|
|
441
|
+
|
|
442
|
+
def get_str(
|
|
443
|
+
self, kind: StringFileKind, id: int
|
|
444
|
+
) -> Optional[str]:
|
|
445
|
+
"""
|
|
446
|
+
Retrieve a string from the specified sub-table decoded as UTF-8.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
kind (StringFileKind): Which sub-table to query.
|
|
450
|
+
id (int): Numeric string ID.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Optional[str]: Decoded string without trailing null, or
|
|
454
|
+
``None`` if not found.
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
raw = self.get(kind, id)
|
|
458
|
+
if raw is None:
|
|
459
|
+
return None
|
|
460
|
+
return raw.rstrip(b"\x00").decode("utf-8")
|
|
461
|
+
|
|
462
|
+
def set(
|
|
463
|
+
self, kind: StringFileKind, id: int, data: bytes
|
|
464
|
+
) -> None:
|
|
465
|
+
"""
|
|
466
|
+
Insert or overwrite an entry in the specified sub-table.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
kind (StringFileKind): Which sub-table to modify.
|
|
470
|
+
id (int): Numeric string ID.
|
|
471
|
+
data (bytes): String payload.
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
BethkitClosedError: If the set has been closed.
|
|
475
|
+
BethkitNativeError: If the native call fails.
|
|
476
|
+
"""
|
|
477
|
+
|
|
478
|
+
lib = _ffi.load_lib()
|
|
479
|
+
buf = _buf_from_bytes(data)
|
|
480
|
+
if (
|
|
481
|
+
lib.bethkit_localization_set_set(
|
|
482
|
+
self.__check_open(), int(kind), id, buf, len(data)
|
|
483
|
+
)
|
|
484
|
+
!= 0
|
|
485
|
+
):
|
|
486
|
+
_ffi.raise_last_error(lib)
|
|
487
|
+
|
|
488
|
+
def write(
|
|
489
|
+
self, plugin_path: Path, language: str
|
|
490
|
+
) -> None:
|
|
491
|
+
"""
|
|
492
|
+
Write all sub-tables to disk next to *plugin_path*.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
plugin_path (Path): Path to the plugin file whose name is
|
|
496
|
+
used to derive the output file names.
|
|
497
|
+
language (str): BCP 47-style language code
|
|
498
|
+
(e.g. ``"english"``).
|
|
499
|
+
|
|
500
|
+
Raises:
|
|
501
|
+
BethkitClosedError: If the set has been closed.
|
|
502
|
+
BethkitNativeError: If any write fails.
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
lib = _ffi.load_lib()
|
|
506
|
+
if (
|
|
507
|
+
lib.bethkit_localization_set_write(
|
|
508
|
+
self.__check_open(),
|
|
509
|
+
_ffi.enc(plugin_path),
|
|
510
|
+
_ffi.senc(language),
|
|
511
|
+
)
|
|
512
|
+
!= 0
|
|
513
|
+
):
|
|
514
|
+
_ffi.raise_last_error(lib)
|
|
515
|
+
|
|
516
|
+
def __repr__(self) -> str:
|
|
517
|
+
"""
|
|
518
|
+
Returns:
|
|
519
|
+
str: Developer-friendly representation of the set.
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
return "<LocalizationSet>"
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bethkit
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python bindings for bethkit — a Bethesda plugin and archive toolkit
|
|
5
|
+
Project-URL: Homepage, https://moddingforge.com/
|
|
6
|
+
Project-URL: Repository, https://github.com/Modding-Forge/bethkit.py
|
|
7
|
+
Author-email: Modding Forge <info@moddingforge.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: ba2,bethesda,bsa,esm,esp,fallout,modding,skyrim
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Games/Entertainment
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pyright; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-mock; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# bethkit.py
|
|
32
|
+
|
|
33
|
+
[](LICENSE) [](https://www.python.org) [](https://pypi.org/project/bethkit/)
|
|
34
|
+
|
|
35
|
+
Python bindings for [bethkit](https://github.com/Modding-Forge/bethkit) - a fast Rust library for reading and writing Bethesda game plugin and archive files. `bethkit.py` wraps the `bethkit_ffi` C ABI via `ctypes`; no compiler or build tools required.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Plugin reading** - open `.esp`/`.esm`/`.esl` files by path or from in-memory bytes; iterate groups and records; look up records by FormID or EditorID; inspect sub-records
|
|
40
|
+
- **Plugin writing** - build new plugins from scratch with `PluginWriter`, `WritableGroup`, and `WritableRecord`
|
|
41
|
+
- **BSA / BA2 archives** - open and extract entries from BSA (TES4/SSE) and BA2 (GNRL/DX10) archives; write new archives with `BsaWriter`, `Ba2GnrlWriter`, and `Ba2Dx10Writer`
|
|
42
|
+
- **String tables** - read, edit, and write `.STRINGS`/`.DLSTRINGS`/`.ILSTRINGS` localisation files; apply translation sets with `LocalizationSet`
|
|
43
|
+
- **Schema** - decode sub-records into typed `FieldValue` variants (integers, floats, FormIDs, enums with resolved names, flags, structs, arrays) via `RecordView` and `SchemaRegistry`
|
|
44
|
+
- **Load-order utilities** - `LoadOrder`, `GlobalFormId`, and `PluginCache` for winning-override lookups and EditorID search across multiple plugins
|
|
45
|
+
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
| Requirement | Version |
|
|
49
|
+
| ------------ | -------- |
|
|
50
|
+
| Python | ≥ 3.10 |
|
|
51
|
+
| pydantic | ≥ 2.0 |
|
|
52
|
+
| bethkit\_ffi | matching |
|
|
53
|
+
|
|
54
|
+
Place `bethkit_ffi.dll` (Windows), `libbethkit_ffi.so` (Linux), or `libbethkit_ffi.dylib` (macOS) next to the package, or set the `BETHKIT_LIB` environment variable to the full path of the library.
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
uv add bethkit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
or
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pip install bethkit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
### Reading a plugin
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from pathlib import Path
|
|
74
|
+
from bethkit import Plugin, Game
|
|
75
|
+
|
|
76
|
+
with Plugin.open(Path("Ordinator - Perks of Skyrim.esp"), Game.SKYRIM_SE) as plugin:
|
|
77
|
+
print("Masters:", plugin.masters)
|
|
78
|
+
print("Kind:", plugin.kind)
|
|
79
|
+
|
|
80
|
+
for group in plugin:
|
|
81
|
+
for child in group:
|
|
82
|
+
if hasattr(child, "form_id"):
|
|
83
|
+
print(f" 0x{child.form_id:08X} {child.editor_id}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Building a plugin from scratch
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
from bethkit import Game, PluginWriter, WritableGroup, WritableRecord
|
|
91
|
+
|
|
92
|
+
with PluginWriter(Game.SKYRIM_SE) as writer:
|
|
93
|
+
with WritableGroup.new(b"NPC_") as group:
|
|
94
|
+
rec = WritableRecord.new(b"NPC_", form_id=0x000D62)
|
|
95
|
+
rec.add_subrecord(b"EDID", b"MyNPC\x00")
|
|
96
|
+
group.add_record(rec)
|
|
97
|
+
writer.add_group(group)
|
|
98
|
+
writer.write_to_file(Path("MyMod.esp"))
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Extracting from an archive
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
from bethkit import Archive
|
|
106
|
+
|
|
107
|
+
with Archive.open(Path("Skyrim - Meshes.bsa")) as arc:
|
|
108
|
+
data = arc.extract("meshes/actors/character/character assets/skeleton.nif")
|
|
109
|
+
if data:
|
|
110
|
+
Path("skeleton.nif").write_bytes(data)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Load-order and FormID resolution
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from pathlib import Path
|
|
117
|
+
from bethkit import Game, Plugin, PluginCache, PluginKind, LoadOrder
|
|
118
|
+
|
|
119
|
+
lo = LoadOrder()
|
|
120
|
+
lo.push("Skyrim.esm", PluginKind.FULL)
|
|
121
|
+
lo.push("MyMod.esp", PluginKind.FULL)
|
|
122
|
+
|
|
123
|
+
cache = PluginCache()
|
|
124
|
+
cache.add("Skyrim.esm", Plugin.open(Path("Skyrim.esm"), Game.SKYRIM_SE))
|
|
125
|
+
|
|
126
|
+
hit = cache.find_by_editor_id("ArmorIronCuirass")
|
|
127
|
+
if hit:
|
|
128
|
+
print(hit.global_form_id) # Skyrim.esm:0x012E49
|
|
129
|
+
print(hit.record.editor_id)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```sh
|
|
135
|
+
git clone https://github.com/Modding-Forge/bethkit.py
|
|
136
|
+
cd bethkit.py
|
|
137
|
+
uv sync --extra dev
|
|
138
|
+
uv run pytest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Linting and type-checking:
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
uv tool run ruff check src/ tests/
|
|
145
|
+
uv tool run pyright src/ tests/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Related projects
|
|
149
|
+
|
|
150
|
+
- [bethkit](https://github.com/Modding-Forge/bethkit) - the underlying Rust library; `bethkit.py` wraps its C ABI
|
|
151
|
+
- [SSE-Auto-Translator](https://github.com/Modding-Forge/SSE-Auto-Translator) - uses `bethkit.py` to patch localised strings
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
Apache-2.0 - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
bethkit/__init__.py,sha256=T6x3LQzo8hV1KcHIoXckWqWxNAgqhFuewcYLqS6zmik,2440
|
|
2
|
+
bethkit/_error.py,sha256=-7fHK_bNxlQTXW-9W6a8b2_6df_mF9vUAwy4PTKsCTU,2498
|
|
3
|
+
bethkit/enums.py,sha256=6yhMv13mCuEenz3hoXNGNFny8JUvvYdLr0CuNg3LSZs,3059
|
|
4
|
+
bethkit/load_order.py,sha256=6S3W-GKGvxPm_7UpnV2aOsynRLMJvmVyljifizvHp9g,5550
|
|
5
|
+
bethkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
bethkit/_ffi/__init__.py,sha256=Wzsh11GwBnOX8ryruuJg2bTMVL-UdgMhidsEuQdlips,905
|
|
7
|
+
bethkit/_ffi/_loader.py,sha256=ehI8DwiUJwDNDPPEj4ewrKu-kbiGbZH_enbuIHiieNc,18508
|
|
8
|
+
bethkit/_ffi/_types.py,sha256=hHPTH8ADrh2-RMwhyGzT0MpZCRZdHCA4L-Ispm1R9ZI,3052
|
|
9
|
+
bethkit/archive/__init__.py,sha256=i41QKSXbSoExxnjO33hu89fj9UKJpyWj6e4iu3fi1Ac,352
|
|
10
|
+
bethkit/archive/archive.py,sha256=4qJkOPKrNOoj6rY_NnbVmg9aMchVhr91hnCZB604I7k,19135
|
|
11
|
+
bethkit/plugin/__init__.py,sha256=XrNzsbXdGYEJ8EUCa143gHX-Vl8KIADgAoqZx4dAZYo,499
|
|
12
|
+
bethkit/plugin/cache.py,sha256=b-KWKeDf26iVVBtlwzu4_zC-6pb6iv4jrETYMgTcU4k,7423
|
|
13
|
+
bethkit/plugin/plugin.py,sha256=C0rPNF5nDABt9KEIafcvYU_XdovX6s2_u1_d0h8jARY,22439
|
|
14
|
+
bethkit/plugin/writer.py,sha256=nkZAitR-AvVRnDIwnVS6nNrWWtUmgrj-7HSnI55lkQA,14571
|
|
15
|
+
bethkit/schema/__init__.py,sha256=XZKez2ApXIaioy6pd3AAZoCujjlf_8QIjrFXOxYEOQc,456
|
|
16
|
+
bethkit/schema/schema.py,sha256=q8MksAXDdfH7hvw6GNOI8u5aqxBDb3YCoOU5wA0z50E,13238
|
|
17
|
+
bethkit/strings/__init__.py,sha256=NK6bWOX2txWMWfd3vE9aT7JYhT-AfNaNVttS-u2OvPs,273
|
|
18
|
+
bethkit/strings/strings.py,sha256=8PMaC_W0m4y20xhMdeFd5upDmnfIHuJ4jYzjdhKXsd0,14409
|
|
19
|
+
bethkit-1.0.0.dist-info/METADATA,sha256=vhR7PuHBFzAyG5LlYSrwFMIqVa1gAniFwYHmAsqPogQ,5383
|
|
20
|
+
bethkit-1.0.0.dist-info/WHEEL,sha256=y3FWwPRFPfRewdaamNJ2TA-_oj6MYMwqJXYgc-1N3nY,93
|
|
21
|
+
bethkit-1.0.0.dist-info/licenses/LICENSE,sha256=zpP_oxrFUIxGN0S1gDhVrmhk75Fytp74iwyh4dXNwGs,11617
|
|
22
|
+
bethkit-1.0.0.dist-info/RECORD,,
|