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,694 @@
|
|
|
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 Optional
|
|
10
|
+
|
|
11
|
+
from .. import _ffi
|
|
12
|
+
from .._error import BethkitClosedError, BethkitNotFoundError
|
|
13
|
+
from ..enums import Ba2Version, BsaVersion
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _buf_from_bytes(data: bytes) -> ctypes.Array[ctypes.c_uint8]:
|
|
17
|
+
"""
|
|
18
|
+
Wrap *data* in a ctypes ``c_uint8`` array for FFI calls.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
data (bytes): Byte sequence to wrap.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
ctypes.Array: A ``c_uint8`` array backed by a copy of *data*.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
return (ctypes.c_uint8 * len(data)).from_buffer_copy(data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ArchiveEntry:
|
|
31
|
+
"""
|
|
32
|
+
A single file entry inside an open archive.
|
|
33
|
+
|
|
34
|
+
Instances borrow their data from the parent :class:`Archive` and
|
|
35
|
+
become invalid once the archive is closed or freed.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_ptr: int
|
|
39
|
+
_parent: Archive
|
|
40
|
+
|
|
41
|
+
def __init__(self, ptr: int, parent: Archive) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Args:
|
|
44
|
+
ptr (int): Native pointer to the underlying entry object.
|
|
45
|
+
parent (Archive): Owning archive that keeps native memory alive.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
self._ptr = ptr
|
|
49
|
+
self._parent = parent
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def path(self) -> str:
|
|
53
|
+
"""
|
|
54
|
+
Virtual path of the entry as stored in the archive.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
str: Path string, or an empty string when unavailable.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
lib = _ffi.load_lib()
|
|
61
|
+
ptr = lib.bethkit_archive_entry_path(self._ptr)
|
|
62
|
+
if not ptr:
|
|
63
|
+
return ""
|
|
64
|
+
return _ffi.copy_and_free_str(
|
|
65
|
+
ptr, lib.bethkit_archive_entry_path_free, lib
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def uncompressed_size(self) -> int:
|
|
70
|
+
"""
|
|
71
|
+
Uncompressed size of the entry data in bytes.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
int: Byte count of the decompressed content.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
return _ffi.load_lib().bethkit_archive_entry_uncompressed_size(
|
|
78
|
+
self._ptr
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
def __repr__(self) -> str:
|
|
82
|
+
"""
|
|
83
|
+
Returns:
|
|
84
|
+
str: Developer-friendly representation showing the entry path.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return f"<ArchiveEntry {self.path!r}>"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class Archive:
|
|
91
|
+
"""
|
|
92
|
+
An open Bethesda archive (BSA or BA2) in read-only mode.
|
|
93
|
+
|
|
94
|
+
Use as a context manager to guarantee that the native handle is
|
|
95
|
+
freed even on error::
|
|
96
|
+
|
|
97
|
+
with Archive.open(path) as arc:
|
|
98
|
+
data = arc.extract("meshes/foo.nif")
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
__ptr: int
|
|
102
|
+
|
|
103
|
+
def __init__(self, ptr: int) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Args:
|
|
106
|
+
ptr (int): Native handle returned by the FFI open call.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
self.__ptr = ptr
|
|
110
|
+
|
|
111
|
+
def __check_open(self) -> int:
|
|
112
|
+
"""
|
|
113
|
+
Return the native pointer, raising if the handle has been closed.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
int: Valid native pointer.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
BethkitClosedError: If the archive has been closed.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
if not self.__ptr:
|
|
123
|
+
raise BethkitClosedError("Archive is closed")
|
|
124
|
+
return self.__ptr
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def open(cls, path: Path) -> Archive:
|
|
128
|
+
"""
|
|
129
|
+
Open an archive file from disk.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
path (Path): Filesystem path to the ``.bsa`` or ``.ba2`` file.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Archive: A new ``Archive`` wrapping the open file.
|
|
136
|
+
|
|
137
|
+
Raises:
|
|
138
|
+
BethkitNativeError: If the file cannot be opened or parsed.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
lib = _ffi.load_lib()
|
|
142
|
+
ptr = lib.bethkit_archive_open(_ffi.enc(path))
|
|
143
|
+
if not ptr:
|
|
144
|
+
_ffi.raise_last_error(lib)
|
|
145
|
+
return cls(ptr)
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
"""
|
|
149
|
+
Release the native archive handle.
|
|
150
|
+
|
|
151
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
if self.__ptr:
|
|
155
|
+
_ffi.load_lib().bethkit_archive_free(self.__ptr)
|
|
156
|
+
self.__ptr = 0
|
|
157
|
+
|
|
158
|
+
def __enter__(self) -> Archive:
|
|
159
|
+
"""Return *self* for use as a context manager."""
|
|
160
|
+
|
|
161
|
+
return self
|
|
162
|
+
|
|
163
|
+
def __exit__(self, *_: object) -> None:
|
|
164
|
+
"""Close the archive when exiting the context."""
|
|
165
|
+
|
|
166
|
+
self.close()
|
|
167
|
+
|
|
168
|
+
def __del__(self) -> None:
|
|
169
|
+
"""Free the native handle on garbage collection."""
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
self.close()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def format_name(self) -> str:
|
|
178
|
+
"""
|
|
179
|
+
Human-readable name of the archive format (e.g. ``"BSA"`` or
|
|
180
|
+
``"BA2"``).
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: Format identifier string.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
BethkitClosedError: If the archive has been closed.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
lib = _ffi.load_lib()
|
|
190
|
+
raw: Optional[bytes] = lib.bethkit_archive_format_name(
|
|
191
|
+
self.__check_open()
|
|
192
|
+
)
|
|
193
|
+
return raw.decode("utf-8") if raw else ""
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def file_count(self) -> int:
|
|
197
|
+
"""
|
|
198
|
+
Total number of file entries in the archive.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
int: Entry count.
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
BethkitClosedError: If the archive has been closed.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
return _ffi.load_lib().bethkit_archive_file_count(
|
|
208
|
+
self.__check_open()
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
def entry_at(self, index: int) -> ArchiveEntry:
|
|
212
|
+
"""
|
|
213
|
+
Return the entry at the given index.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
index (int): Zero-based entry index.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
ArchiveEntry: Borrowed entry for the given index.
|
|
220
|
+
|
|
221
|
+
Raises:
|
|
222
|
+
BethkitClosedError: If the archive has been closed.
|
|
223
|
+
BethkitNativeError: If *index* is out of range.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
lib = _ffi.load_lib()
|
|
227
|
+
ptr = lib.bethkit_archive_entry_get(self.__check_open(), index)
|
|
228
|
+
if not ptr:
|
|
229
|
+
_ffi.raise_last_error(lib)
|
|
230
|
+
return ArchiveEntry(ptr, self)
|
|
231
|
+
|
|
232
|
+
def entries(self) -> Iterator[ArchiveEntry]:
|
|
233
|
+
"""
|
|
234
|
+
Iterate over all entries in the archive.
|
|
235
|
+
|
|
236
|
+
Yields:
|
|
237
|
+
ArchiveEntry: Each entry in insertion order.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
BethkitClosedError: If the archive has been closed.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
for i in range(self.file_count):
|
|
244
|
+
yield self.entry_at(i)
|
|
245
|
+
|
|
246
|
+
def extract(self, path: str) -> Optional[bytes]:
|
|
247
|
+
"""
|
|
248
|
+
Extract a single file from the archive by its virtual path.
|
|
249
|
+
|
|
250
|
+
Returns ``None`` when the path is not found.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
path (str): Virtual path of the entry to extract.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Optional[bytes]: Decompressed file data, or ``None`` if the
|
|
257
|
+
path does not exist in the archive.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
BethkitClosedError: If the archive has been closed.
|
|
261
|
+
BethkitNativeError: If extraction fails for a reason other than
|
|
262
|
+
a missing path.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
lib = _ffi.load_lib()
|
|
266
|
+
out_len = ctypes.c_size_t(0)
|
|
267
|
+
ptr = lib.bethkit_archive_extract(
|
|
268
|
+
self.__check_open(), _ffi.senc(path), ctypes.byref(out_len)
|
|
269
|
+
)
|
|
270
|
+
if not ptr:
|
|
271
|
+
return None
|
|
272
|
+
try:
|
|
273
|
+
return bytes(ctypes.string_at(ptr, out_len.value))
|
|
274
|
+
finally:
|
|
275
|
+
lib.bethkit_bytes_free(ptr, out_len.value)
|
|
276
|
+
|
|
277
|
+
def extract_required(self, path: str) -> bytes:
|
|
278
|
+
"""
|
|
279
|
+
Extract a single file, raising when the path is not found.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
path (str): Virtual path of the entry to extract.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
bytes: Decompressed file data.
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
BethkitClosedError: If the archive has been closed.
|
|
289
|
+
BethkitNotFoundError: If the path is not in the archive.
|
|
290
|
+
BethkitNativeError: If extraction fails.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
data = self.extract(path)
|
|
294
|
+
if data is None:
|
|
295
|
+
raise BethkitNotFoundError(f"Entry not found in archive: {path!r}")
|
|
296
|
+
return data
|
|
297
|
+
|
|
298
|
+
def extract_to_file(self, path: str, dest: Path) -> None:
|
|
299
|
+
"""
|
|
300
|
+
Extract a single entry and write it to *dest* on disk.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
path (str): Virtual path of the entry to extract.
|
|
304
|
+
dest (Path): Destination file path.
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
BethkitClosedError: If the archive has been closed.
|
|
308
|
+
BethkitNativeError: If the path is not found or the write fails.
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
lib = _ffi.load_lib()
|
|
312
|
+
rc = lib.bethkit_archive_extract_to_file(
|
|
313
|
+
self.__check_open(), _ffi.senc(path), _ffi.enc(dest)
|
|
314
|
+
)
|
|
315
|
+
if rc != 0:
|
|
316
|
+
_ffi.raise_last_error(lib)
|
|
317
|
+
|
|
318
|
+
def __repr__(self) -> str:
|
|
319
|
+
"""
|
|
320
|
+
Returns:
|
|
321
|
+
str: Developer-friendly representation showing format and count.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
if not self.__ptr:
|
|
325
|
+
return "<Archive closed>"
|
|
326
|
+
return (
|
|
327
|
+
f"<Archive format={self.format_name!r} files={self.file_count}>"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class BsaWriter:
|
|
332
|
+
"""
|
|
333
|
+
Builder for Bethesda Softworks Archive (BSA) files.
|
|
334
|
+
|
|
335
|
+
Create a writer, add files, then call :meth:`write_to` to produce
|
|
336
|
+
the BSA on disk. Use as a context manager to ensure the native
|
|
337
|
+
handle is released::
|
|
338
|
+
|
|
339
|
+
with BsaWriter(BsaVersion.SSE) as w:
|
|
340
|
+
w.add("meshes/foo.nif", data)
|
|
341
|
+
w.write_to(output_path)
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
__ptr: int
|
|
345
|
+
|
|
346
|
+
def __init__(self, version: BsaVersion) -> None:
|
|
347
|
+
"""
|
|
348
|
+
Args:
|
|
349
|
+
version (BsaVersion): BSA format version to write.
|
|
350
|
+
|
|
351
|
+
Raises:
|
|
352
|
+
BethkitNativeError: If the native writer cannot be created.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
lib = _ffi.load_lib()
|
|
356
|
+
ptr = lib.bethkit_bsa_writer_new(int(version))
|
|
357
|
+
if not ptr:
|
|
358
|
+
_ffi.raise_last_error(lib)
|
|
359
|
+
self.__ptr = ptr
|
|
360
|
+
|
|
361
|
+
def __check_open(self) -> int:
|
|
362
|
+
"""
|
|
363
|
+
Return the native pointer, raising if the handle has been closed.
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
int: Valid native pointer.
|
|
367
|
+
|
|
368
|
+
Raises:
|
|
369
|
+
BethkitClosedError: If the writer has been closed.
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
if not self.__ptr:
|
|
373
|
+
raise BethkitClosedError("BsaWriter is closed")
|
|
374
|
+
return self.__ptr
|
|
375
|
+
|
|
376
|
+
def close(self) -> None:
|
|
377
|
+
"""
|
|
378
|
+
Release the native writer handle.
|
|
379
|
+
|
|
380
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
if self.__ptr:
|
|
384
|
+
_ffi.load_lib().bethkit_bsa_writer_free(self.__ptr)
|
|
385
|
+
self.__ptr = 0
|
|
386
|
+
|
|
387
|
+
def __enter__(self) -> BsaWriter:
|
|
388
|
+
"""Return *self* for use as a context manager."""
|
|
389
|
+
|
|
390
|
+
return self
|
|
391
|
+
|
|
392
|
+
def __exit__(self, *_: object) -> None:
|
|
393
|
+
"""Free the writer when exiting the context."""
|
|
394
|
+
|
|
395
|
+
self.close()
|
|
396
|
+
|
|
397
|
+
def __del__(self) -> None:
|
|
398
|
+
"""Free the native handle on garbage collection."""
|
|
399
|
+
|
|
400
|
+
try:
|
|
401
|
+
self.close()
|
|
402
|
+
except Exception:
|
|
403
|
+
pass
|
|
404
|
+
|
|
405
|
+
def set_compress(self, compress: bool) -> None:
|
|
406
|
+
"""
|
|
407
|
+
Enable or disable default compression for entries.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
compress (bool): ``True`` to enable compression by default.
|
|
411
|
+
|
|
412
|
+
Raises:
|
|
413
|
+
BethkitClosedError: If the writer has been closed.
|
|
414
|
+
BethkitNativeError: If the native call fails.
|
|
415
|
+
"""
|
|
416
|
+
|
|
417
|
+
lib = _ffi.load_lib()
|
|
418
|
+
if lib.bethkit_bsa_writer_set_compress(
|
|
419
|
+
self.__check_open(), compress
|
|
420
|
+
) != 0:
|
|
421
|
+
_ffi.raise_last_error(lib)
|
|
422
|
+
|
|
423
|
+
def set_embed_names(self, embed: bool) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Enable or disable embedded file-name strings in the archive.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
embed (bool): ``True`` to embed file names.
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
BethkitClosedError: If the writer has been closed.
|
|
432
|
+
BethkitNativeError: If the native call fails.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
lib = _ffi.load_lib()
|
|
436
|
+
if lib.bethkit_bsa_writer_set_embed_names(
|
|
437
|
+
self.__check_open(), embed
|
|
438
|
+
) != 0:
|
|
439
|
+
_ffi.raise_last_error(lib)
|
|
440
|
+
|
|
441
|
+
def add(self, path: str, data: bytes) -> None:
|
|
442
|
+
"""
|
|
443
|
+
Add a file to the archive.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
path (str): Virtual path used to store the file inside the
|
|
447
|
+
archive.
|
|
448
|
+
data (bytes): File contents.
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
BethkitClosedError: If the writer has been closed.
|
|
452
|
+
BethkitNativeError: If the entry cannot be added.
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
lib = _ffi.load_lib()
|
|
456
|
+
buf = _buf_from_bytes(data)
|
|
457
|
+
if lib.bethkit_bsa_writer_add(
|
|
458
|
+
self.__check_open(), _ffi.senc(path), buf, len(data)
|
|
459
|
+
) != 0:
|
|
460
|
+
_ffi.raise_last_error(lib)
|
|
461
|
+
|
|
462
|
+
def write_to(self, dest: Path) -> None:
|
|
463
|
+
"""
|
|
464
|
+
Finalise and write the archive to disk.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
dest (Path): Destination file path.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
BethkitClosedError: If the writer has been closed.
|
|
471
|
+
BethkitNativeError: If serialisation or the write fails.
|
|
472
|
+
"""
|
|
473
|
+
|
|
474
|
+
lib = _ffi.load_lib()
|
|
475
|
+
if lib.bethkit_bsa_writer_write_to(
|
|
476
|
+
self.__check_open(), _ffi.enc(dest)
|
|
477
|
+
) != 0:
|
|
478
|
+
_ffi.raise_last_error(lib)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
class Ba2GnrlWriter:
|
|
482
|
+
"""
|
|
483
|
+
Builder for Fallout 4 BA2 general (non-texture) archives.
|
|
484
|
+
|
|
485
|
+
Use this for non-texture assets packed in the general BA2 format.
|
|
486
|
+
The interface mirrors :class:`BsaWriter`.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
__ptr: int
|
|
490
|
+
|
|
491
|
+
def __init__(self, version: Ba2Version) -> None:
|
|
492
|
+
"""
|
|
493
|
+
Args:
|
|
494
|
+
version (Ba2Version): BA2 format version to write.
|
|
495
|
+
|
|
496
|
+
Raises:
|
|
497
|
+
BethkitNativeError: If the native writer cannot be created.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
lib = _ffi.load_lib()
|
|
501
|
+
ptr = lib.bethkit_ba2_gnrl_writer_new(int(version))
|
|
502
|
+
if not ptr:
|
|
503
|
+
_ffi.raise_last_error(lib)
|
|
504
|
+
self.__ptr = ptr
|
|
505
|
+
|
|
506
|
+
def __check_open(self) -> int:
|
|
507
|
+
"""
|
|
508
|
+
Return the native pointer, raising if the handle has been closed.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
int: Valid native pointer.
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
BethkitClosedError: If the writer has been closed.
|
|
515
|
+
"""
|
|
516
|
+
|
|
517
|
+
if not self.__ptr:
|
|
518
|
+
raise BethkitClosedError("Ba2GnrlWriter is closed")
|
|
519
|
+
return self.__ptr
|
|
520
|
+
|
|
521
|
+
def close(self) -> None:
|
|
522
|
+
"""
|
|
523
|
+
Release the native writer handle.
|
|
524
|
+
|
|
525
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
if self.__ptr:
|
|
529
|
+
_ffi.load_lib().bethkit_ba2_gnrl_writer_free(self.__ptr)
|
|
530
|
+
self.__ptr = 0
|
|
531
|
+
|
|
532
|
+
def __enter__(self) -> Ba2GnrlWriter:
|
|
533
|
+
"""Return *self* for use as a context manager."""
|
|
534
|
+
|
|
535
|
+
return self
|
|
536
|
+
|
|
537
|
+
def __exit__(self, *_: object) -> None:
|
|
538
|
+
"""Free the writer when exiting the context."""
|
|
539
|
+
|
|
540
|
+
self.close()
|
|
541
|
+
|
|
542
|
+
def __del__(self) -> None:
|
|
543
|
+
"""Free the native handle on garbage collection."""
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
self.close()
|
|
547
|
+
except Exception:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
def add(self, path: str, data: bytes) -> None:
|
|
551
|
+
"""
|
|
552
|
+
Add a file to the archive.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
path (str): Virtual path inside the archive.
|
|
556
|
+
data (bytes): File contents.
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
BethkitClosedError: If the writer has been closed.
|
|
560
|
+
BethkitNativeError: If the entry cannot be added.
|
|
561
|
+
"""
|
|
562
|
+
|
|
563
|
+
lib = _ffi.load_lib()
|
|
564
|
+
buf = _buf_from_bytes(data)
|
|
565
|
+
if lib.bethkit_ba2_gnrl_writer_add(
|
|
566
|
+
self.__check_open(), _ffi.senc(path), buf, len(data)
|
|
567
|
+
) != 0:
|
|
568
|
+
_ffi.raise_last_error(lib)
|
|
569
|
+
|
|
570
|
+
def write_to(self, dest: Path) -> None:
|
|
571
|
+
"""
|
|
572
|
+
Finalise and write the archive to disk.
|
|
573
|
+
|
|
574
|
+
Args:
|
|
575
|
+
dest (Path): Destination file path.
|
|
576
|
+
|
|
577
|
+
Raises:
|
|
578
|
+
BethkitClosedError: If the writer has been closed.
|
|
579
|
+
BethkitNativeError: If serialisation or the write fails.
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
lib = _ffi.load_lib()
|
|
583
|
+
if lib.bethkit_ba2_gnrl_writer_write_to(
|
|
584
|
+
self.__check_open(), _ffi.enc(dest)
|
|
585
|
+
) != 0:
|
|
586
|
+
_ffi.raise_last_error(lib)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class Ba2Dx10Writer:
|
|
590
|
+
"""
|
|
591
|
+
Builder for Fallout 4 BA2 DX10 (texture) archives.
|
|
592
|
+
|
|
593
|
+
Use this for texture assets packed in the DX10 BA2 format used by
|
|
594
|
+
Fallout 4.
|
|
595
|
+
"""
|
|
596
|
+
|
|
597
|
+
__ptr: int
|
|
598
|
+
|
|
599
|
+
def __init__(self, version: Ba2Version) -> None:
|
|
600
|
+
"""
|
|
601
|
+
Args:
|
|
602
|
+
version (Ba2Version): BA2 format version to write.
|
|
603
|
+
|
|
604
|
+
Raises:
|
|
605
|
+
BethkitNativeError: If the native writer cannot be created.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
lib = _ffi.load_lib()
|
|
609
|
+
ptr = lib.bethkit_ba2_dx10_writer_new(int(version))
|
|
610
|
+
if not ptr:
|
|
611
|
+
_ffi.raise_last_error(lib)
|
|
612
|
+
self.__ptr = ptr
|
|
613
|
+
|
|
614
|
+
def __check_open(self) -> int:
|
|
615
|
+
"""
|
|
616
|
+
Return the native pointer, raising if the handle has been closed.
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
int: Valid native pointer.
|
|
620
|
+
|
|
621
|
+
Raises:
|
|
622
|
+
BethkitClosedError: If the writer has been closed.
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
if not self.__ptr:
|
|
626
|
+
raise BethkitClosedError("Ba2Dx10Writer is closed")
|
|
627
|
+
return self.__ptr
|
|
628
|
+
|
|
629
|
+
def close(self) -> None:
|
|
630
|
+
"""
|
|
631
|
+
Release the native writer handle.
|
|
632
|
+
|
|
633
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
634
|
+
"""
|
|
635
|
+
|
|
636
|
+
if self.__ptr:
|
|
637
|
+
_ffi.load_lib().bethkit_ba2_dx10_writer_free(self.__ptr)
|
|
638
|
+
self.__ptr = 0
|
|
639
|
+
|
|
640
|
+
def __enter__(self) -> Ba2Dx10Writer:
|
|
641
|
+
"""Return *self* for use as a context manager."""
|
|
642
|
+
|
|
643
|
+
return self
|
|
644
|
+
|
|
645
|
+
def __exit__(self, *_: object) -> None:
|
|
646
|
+
"""Free the writer when exiting the context."""
|
|
647
|
+
|
|
648
|
+
self.close()
|
|
649
|
+
|
|
650
|
+
def __del__(self) -> None:
|
|
651
|
+
"""Free the native handle on garbage collection."""
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
self.close()
|
|
655
|
+
except Exception:
|
|
656
|
+
pass
|
|
657
|
+
|
|
658
|
+
def add(self, path: str, data: bytes) -> None:
|
|
659
|
+
"""
|
|
660
|
+
Add a texture file to the archive.
|
|
661
|
+
|
|
662
|
+
Args:
|
|
663
|
+
path (str): Virtual path inside the archive.
|
|
664
|
+
data (bytes): Raw DDS texture data.
|
|
665
|
+
|
|
666
|
+
Raises:
|
|
667
|
+
BethkitClosedError: If the writer has been closed.
|
|
668
|
+
BethkitNativeError: If the entry cannot be added.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
lib = _ffi.load_lib()
|
|
672
|
+
buf = _buf_from_bytes(data)
|
|
673
|
+
if lib.bethkit_ba2_dx10_writer_add(
|
|
674
|
+
self.__check_open(), _ffi.senc(path), buf, len(data)
|
|
675
|
+
) != 0:
|
|
676
|
+
_ffi.raise_last_error(lib)
|
|
677
|
+
|
|
678
|
+
def write_to(self, dest: Path) -> None:
|
|
679
|
+
"""
|
|
680
|
+
Finalise and write the archive to disk.
|
|
681
|
+
|
|
682
|
+
Args:
|
|
683
|
+
dest (Path): Destination file path.
|
|
684
|
+
|
|
685
|
+
Raises:
|
|
686
|
+
BethkitClosedError: If the writer has been closed.
|
|
687
|
+
BethkitNativeError: If serialisation or the write fails.
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
lib = _ffi.load_lib()
|
|
691
|
+
if lib.bethkit_ba2_dx10_writer_write_to(
|
|
692
|
+
self.__check_open(), _ffi.enc(dest)
|
|
693
|
+
) != 0:
|
|
694
|
+
_ffi.raise_last_error(lib)
|