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/enums.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from enum import IntEnum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Game(IntEnum):
|
|
10
|
+
"""
|
|
11
|
+
Supported Bethesda game titles.
|
|
12
|
+
|
|
13
|
+
These values are passed to the native library to select the correct
|
|
14
|
+
plugin and archive format for a given game.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
SKYRIM_SE = 0
|
|
18
|
+
"""Skyrim Special Edition (64-bit, AE/SE)."""
|
|
19
|
+
|
|
20
|
+
FALLOUT4 = 1
|
|
21
|
+
"""Fallout 4."""
|
|
22
|
+
|
|
23
|
+
SKYRIM = 2
|
|
24
|
+
"""The Elder Scrolls V: Skyrim (Classic, 32-bit)."""
|
|
25
|
+
|
|
26
|
+
FALLOUT3 = 3
|
|
27
|
+
"""Fallout 3."""
|
|
28
|
+
|
|
29
|
+
FALLOUT_NV = 4
|
|
30
|
+
"""Fallout: New Vegas."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class PluginKind(IntEnum):
|
|
34
|
+
"""
|
|
35
|
+
Plugin type as declared in the file header.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
FULL = 0
|
|
39
|
+
"""Standard full plugin (ESP/ESM with a complete load-order slot)."""
|
|
40
|
+
|
|
41
|
+
LIGHT = 1
|
|
42
|
+
"""Light plugin (ESL) with a 12-bit FormID range."""
|
|
43
|
+
|
|
44
|
+
OVERLAY = 2
|
|
45
|
+
"""Overlay plugin (ESM override) introduced in Starfield."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StringFileKind(IntEnum):
|
|
49
|
+
"""
|
|
50
|
+
The three localisation string-file types used by Bethesda games.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
STRINGS = 0
|
|
54
|
+
"""Plain null-terminated strings (.STRINGS)."""
|
|
55
|
+
|
|
56
|
+
DL_STRINGS = 1
|
|
57
|
+
"""Length-prefixed strings (.DLSTRINGS)."""
|
|
58
|
+
|
|
59
|
+
IL_STRINGS = 2
|
|
60
|
+
"""Length-prefixed localised strings (.ILSTRINGS)."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class BsaVersion(IntEnum):
|
|
64
|
+
"""
|
|
65
|
+
BSA (Bethesda Softworks Archive) format version selector.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
TES3 = 0
|
|
69
|
+
"""Morrowind BSA format."""
|
|
70
|
+
|
|
71
|
+
TES4 = 1
|
|
72
|
+
"""Oblivion BSA format."""
|
|
73
|
+
|
|
74
|
+
FO3 = 2
|
|
75
|
+
"""Fallout 3 / New Vegas BSA format."""
|
|
76
|
+
|
|
77
|
+
SSE = 3
|
|
78
|
+
"""Skyrim Special Edition BSA format."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Ba2Version(IntEnum):
|
|
82
|
+
"""
|
|
83
|
+
BA2 (Bethesda Archive 2) format version selector.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
V1 = 0
|
|
87
|
+
"""Fallout 4 original BA2 version."""
|
|
88
|
+
|
|
89
|
+
V7 = 1
|
|
90
|
+
"""Fallout 4 Next-Gen patch BA2 version 7."""
|
|
91
|
+
|
|
92
|
+
V8 = 2
|
|
93
|
+
"""Fallout 4 Next-Gen patch BA2 version 8."""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class FieldValueKind(IntEnum):
|
|
97
|
+
"""
|
|
98
|
+
Discriminant tag for the ``BethkitFieldValue`` tagged union.
|
|
99
|
+
|
|
100
|
+
Each variant corresponds to a concrete Python type returned by the
|
|
101
|
+
schema decoding layer.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
INT = 0
|
|
105
|
+
"""Signed or unsigned integer field, decoded as ``int``."""
|
|
106
|
+
|
|
107
|
+
FLOAT = 1
|
|
108
|
+
"""Floating-point field, decoded as ``float``."""
|
|
109
|
+
|
|
110
|
+
STR = 2
|
|
111
|
+
"""Zero-terminated string field, decoded as ``str``."""
|
|
112
|
+
|
|
113
|
+
FORM_ID = 3
|
|
114
|
+
"""Raw 32-bit FormID, decoded as ``int``."""
|
|
115
|
+
|
|
116
|
+
FORM_ID_TYPED = 4
|
|
117
|
+
"""FormID with type constraints, decoded as :class:`TypedFormId`."""
|
|
118
|
+
|
|
119
|
+
BYTES = 5
|
|
120
|
+
"""Raw byte slice, decoded as ``bytes``."""
|
|
121
|
+
|
|
122
|
+
ENUM = 6
|
|
123
|
+
"""Enumeration field, decoded as :class:`EnumVal`."""
|
|
124
|
+
|
|
125
|
+
FLAGS = 7
|
|
126
|
+
"""Bit-flags field, decoded as :class:`FlagsVal`."""
|
|
127
|
+
|
|
128
|
+
STRUCT = 8
|
|
129
|
+
"""Inline struct, decoded as ``list[NamedField]``."""
|
|
130
|
+
|
|
131
|
+
ARRAY = 9
|
|
132
|
+
"""Repeated field, decoded as ``list[FieldValue]``."""
|
|
133
|
+
|
|
134
|
+
LOCALIZED_ID = 10
|
|
135
|
+
"""Localisation string ID, decoded as ``int``."""
|
|
136
|
+
|
|
137
|
+
MISSING = 11
|
|
138
|
+
"""Field absent or unknown; value is ``None``."""
|
|
139
|
+
|
bethkit/load_order.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from . import _ffi
|
|
12
|
+
from ._error import BethkitClosedError
|
|
13
|
+
from ._ffi import BethkitGlobalFormId
|
|
14
|
+
from .enums import PluginKind
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GlobalFormId(BaseModel, frozen=True):
|
|
18
|
+
"""
|
|
19
|
+
A globally unique FormID consisting of a plugin name and a 24-bit
|
|
20
|
+
object ID.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
plugin_name: str
|
|
24
|
+
"""Name of the owning plugin (e.g. ``"Skyrim.esm"``)."""
|
|
25
|
+
|
|
26
|
+
object_id: int
|
|
27
|
+
"""24-bit object identifier within *plugin_name*."""
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
"""
|
|
31
|
+
Return a human-readable representation of this FormID.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: Human-readable ``"PluginName:0xOBJECTID"`` representation.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
return f"{self.plugin_name}:0x{self.object_id:06X}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LoadOrder:
|
|
41
|
+
"""
|
|
42
|
+
An ordered list of plugins that mirrors the active load order.
|
|
43
|
+
|
|
44
|
+
Use :meth:`push` to register plugins in load order, then call
|
|
45
|
+
:meth:`resolve` to translate a local FormID into a
|
|
46
|
+
:class:`GlobalFormId`.
|
|
47
|
+
|
|
48
|
+
Use as a context manager to guarantee that the native handle is
|
|
49
|
+
freed::
|
|
50
|
+
|
|
51
|
+
with LoadOrder() as lo:
|
|
52
|
+
lo.push("Skyrim.esm", PluginKind.FULL)
|
|
53
|
+
gfid = lo.resolve(0x00012E49, "Skyrim.esm")
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
__ptr: int
|
|
57
|
+
|
|
58
|
+
def __init__(self) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Raises:
|
|
61
|
+
BethkitNativeError: If the native load-order object cannot be
|
|
62
|
+
created.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
lib = _ffi.load_lib()
|
|
66
|
+
ptr = lib.bethkit_load_order_new()
|
|
67
|
+
if not ptr:
|
|
68
|
+
_ffi.raise_last_error(lib)
|
|
69
|
+
self.__ptr = ptr
|
|
70
|
+
|
|
71
|
+
def __check_open(self) -> int:
|
|
72
|
+
"""
|
|
73
|
+
Return the native pointer, raising if the handle is already closed.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
int: Non-zero native pointer.
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
BethkitClosedError: If :meth:`close` has already been called.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
if not self.__ptr:
|
|
83
|
+
raise BethkitClosedError(
|
|
84
|
+
"LoadOrder has already been closed."
|
|
85
|
+
)
|
|
86
|
+
return self.__ptr
|
|
87
|
+
|
|
88
|
+
def close(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Release the native load-order handle.
|
|
91
|
+
|
|
92
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
if self.__ptr:
|
|
96
|
+
_ffi.load_lib().bethkit_load_order_free(self.__ptr)
|
|
97
|
+
self.__ptr = 0
|
|
98
|
+
|
|
99
|
+
def __enter__(self) -> LoadOrder:
|
|
100
|
+
"""
|
|
101
|
+
Return *self* for use as a context manager.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
LoadOrder: This instance.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *_: object) -> None:
|
|
110
|
+
"""Close the load order when exiting the context."""
|
|
111
|
+
|
|
112
|
+
self.close()
|
|
113
|
+
|
|
114
|
+
def __del__(self) -> None:
|
|
115
|
+
"""Free the native handle on garbage collection."""
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
self.close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
def push(self, name: str, kind: PluginKind) -> None:
|
|
123
|
+
"""
|
|
124
|
+
Append a plugin to the end of the load order.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
name (str): Plugin file name (e.g. ``"Skyrim.esm"``).
|
|
128
|
+
kind (PluginKind): Whether the plugin is a full, light, or
|
|
129
|
+
overlay plugin.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
BethkitClosedError: If this load order has already been closed.
|
|
133
|
+
BethkitNativeError: If the native call fails.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
lib = _ffi.load_lib()
|
|
137
|
+
ptr = self.__check_open()
|
|
138
|
+
if lib.bethkit_load_order_push(
|
|
139
|
+
ptr, _ffi.senc(name), int(kind)
|
|
140
|
+
) != 0:
|
|
141
|
+
_ffi.raise_last_error(lib)
|
|
142
|
+
|
|
143
|
+
def __len__(self) -> int:
|
|
144
|
+
"""
|
|
145
|
+
Returns:
|
|
146
|
+
int: Number of plugins currently in the load order.
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
BethkitClosedError: If this load order has already been closed.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
return _ffi.load_lib().bethkit_load_order_len(self.__check_open())
|
|
153
|
+
|
|
154
|
+
def resolve(self, form_id: int, source_plugin: str) -> GlobalFormId:
|
|
155
|
+
"""
|
|
156
|
+
Resolve a local FormID to a globally unique :class:`GlobalFormId`.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
form_id (int): The raw 32-bit FormID as stored in a plugin
|
|
160
|
+
record.
|
|
161
|
+
source_plugin (str): Name of the plugin that contains the
|
|
162
|
+
FormID.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
GlobalFormId: The resolved global FormID.
|
|
166
|
+
|
|
167
|
+
Raises:
|
|
168
|
+
BethkitClosedError: If this load order has already been closed.
|
|
169
|
+
BethkitNativeError: If *form_id* or *source_plugin* cannot be
|
|
170
|
+
resolved.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
lib = _ffi.load_lib()
|
|
174
|
+
ptr = self.__check_open()
|
|
175
|
+
out = BethkitGlobalFormId()
|
|
176
|
+
if (
|
|
177
|
+
lib.bethkit_load_order_resolve(
|
|
178
|
+
ptr,
|
|
179
|
+
form_id,
|
|
180
|
+
_ffi.senc(source_plugin),
|
|
181
|
+
ctypes.byref(out),
|
|
182
|
+
)
|
|
183
|
+
!= 0
|
|
184
|
+
):
|
|
185
|
+
_ffi.raise_last_error(lib)
|
|
186
|
+
plugin_name_raw: Optional[bytes] = out.plugin_name
|
|
187
|
+
plugin_name = plugin_name_raw.decode("utf-8") if plugin_name_raw else ""
|
|
188
|
+
return GlobalFormId(plugin_name=plugin_name, object_id=out.object_id)
|
|
189
|
+
|
|
190
|
+
def __repr__(self) -> str:
|
|
191
|
+
"""
|
|
192
|
+
Returns:
|
|
193
|
+
str: Developer-friendly representation with plugin count.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
if not self.__ptr:
|
|
197
|
+
return "<LoadOrder closed>"
|
|
198
|
+
return f"<LoadOrder len={len(self)}>"
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
|
|
4
|
+
Plugin subpackage — reading, writing, and caching Bethesda plugin files.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from .cache import CacheHit, PluginCache
|
|
9
|
+
from .plugin import Group, Plugin, Record, SubRecord
|
|
10
|
+
from .writer import PluginWriter, WritableGroup, WritableRecord
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CacheHit",
|
|
14
|
+
"Group",
|
|
15
|
+
"Plugin",
|
|
16
|
+
"PluginCache",
|
|
17
|
+
"PluginWriter",
|
|
18
|
+
"Record",
|
|
19
|
+
"SubRecord",
|
|
20
|
+
"WritableGroup",
|
|
21
|
+
"WritableRecord",
|
|
22
|
+
]
|
bethkit/plugin/cache.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (c) Modding Forge
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import ctypes
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict
|
|
10
|
+
|
|
11
|
+
from .. import _ffi
|
|
12
|
+
from .._error import BethkitClosedError, BethkitOwnershipError
|
|
13
|
+
from .._ffi import BethkitGlobalFormId
|
|
14
|
+
from ..load_order import GlobalFormId
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CacheHit(BaseModel):
|
|
18
|
+
"""
|
|
19
|
+
Result of a successful :meth:`PluginCache.find_by_editor_id` lookup.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
23
|
+
|
|
24
|
+
record: object
|
|
25
|
+
"""The matched record (borrowed, valid while the cache is open)."""
|
|
26
|
+
|
|
27
|
+
global_form_id: GlobalFormId
|
|
28
|
+
"""The resolved global FormID of the matched record."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PluginCache:
|
|
32
|
+
"""
|
|
33
|
+
An in-memory cache that indexes records across multiple loaded plugins.
|
|
34
|
+
|
|
35
|
+
Add plugins with :meth:`add`, then use :meth:`resolve` or
|
|
36
|
+
:meth:`find_by_editor_id` to look up records across all loaded
|
|
37
|
+
plugins.
|
|
38
|
+
|
|
39
|
+
Use as a context manager to guarantee that the native handle is
|
|
40
|
+
freed::
|
|
41
|
+
|
|
42
|
+
with PluginCache() as cache:
|
|
43
|
+
cache.add("Skyrim.esm", plugin)
|
|
44
|
+
rec = cache.resolve("Skyrim.esm", 0x12E49)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
__ptr: int
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Raises:
|
|
52
|
+
BethkitNativeError: If the native cache object cannot be created.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
lib = _ffi.load_lib()
|
|
56
|
+
ptr = lib.bethkit_plugin_cache_new()
|
|
57
|
+
if not ptr:
|
|
58
|
+
_ffi.raise_last_error(lib)
|
|
59
|
+
self.__ptr = ptr
|
|
60
|
+
|
|
61
|
+
def __check_open(self) -> int:
|
|
62
|
+
"""
|
|
63
|
+
Return the native pointer, raising if the handle is already closed.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
int: Non-zero native pointer.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
BethkitClosedError: If :meth:`close` has already been called.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if not self.__ptr:
|
|
73
|
+
raise BethkitClosedError("PluginCache has already been closed.")
|
|
74
|
+
return self.__ptr
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
"""
|
|
78
|
+
Release the native cache handle.
|
|
79
|
+
|
|
80
|
+
Safe to call multiple times; subsequent calls are no-ops.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
if self.__ptr:
|
|
84
|
+
_ffi.load_lib().bethkit_plugin_cache_free(self.__ptr)
|
|
85
|
+
self.__ptr = 0
|
|
86
|
+
|
|
87
|
+
def __enter__(self) -> PluginCache:
|
|
88
|
+
"""
|
|
89
|
+
Return *self* for use as a context manager.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
PluginCache: This instance.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def __exit__(self, *_: object) -> None:
|
|
98
|
+
"""Free the cache when exiting the context."""
|
|
99
|
+
|
|
100
|
+
self.close()
|
|
101
|
+
|
|
102
|
+
def __del__(self) -> None:
|
|
103
|
+
"""Free the native handle on garbage collection."""
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
self.close()
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def add(self, name: str, plugin: object) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Transfer a :class:`~bethkit.Plugin` into the cache.
|
|
113
|
+
|
|
114
|
+
Ownership of the native plugin handle is transferred to the cache;
|
|
115
|
+
the :class:`~bethkit.Plugin` wrapper becomes invalid after this call.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name (str): Plugin file name used as the lookup key
|
|
119
|
+
(e.g. ``"Skyrim.esm"``).
|
|
120
|
+
plugin: The plugin to add. Must be a valid, open
|
|
121
|
+
:class:`~bethkit.Plugin` instance.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
BethkitClosedError: If this cache or the plugin is already closed.
|
|
125
|
+
BethkitOwnershipError: If the plugin handle has already been
|
|
126
|
+
transferred to another container.
|
|
127
|
+
BethkitNativeError: If the native call fails.
|
|
128
|
+
TypeError: If *plugin* is not a ``Plugin`` instance.
|
|
129
|
+
"""
|
|
130
|
+
|
|
131
|
+
from .plugin import Plugin
|
|
132
|
+
|
|
133
|
+
if not isinstance(plugin, Plugin):
|
|
134
|
+
raise TypeError(
|
|
135
|
+
f"plugin must be a Plugin instance, got {type(plugin).__name__!r}"
|
|
136
|
+
)
|
|
137
|
+
ptr = self.__check_open()
|
|
138
|
+
lib = _ffi.load_lib()
|
|
139
|
+
plugin_ptr: int = plugin._transfer_ptr()
|
|
140
|
+
if not plugin_ptr:
|
|
141
|
+
raise BethkitOwnershipError(
|
|
142
|
+
"Plugin handle has already been transferred or closed."
|
|
143
|
+
)
|
|
144
|
+
if lib.bethkit_plugin_cache_add(
|
|
145
|
+
ptr, _ffi.senc(name), plugin_ptr
|
|
146
|
+
) != 0:
|
|
147
|
+
_ffi.raise_last_error(lib)
|
|
148
|
+
|
|
149
|
+
def __len__(self) -> int:
|
|
150
|
+
"""
|
|
151
|
+
Return the number of plugins currently held in the cache.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
int: Number of plugins.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
BethkitClosedError: If this cache has already been closed.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
return _ffi.load_lib().bethkit_plugin_cache_len(self.__check_open())
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def record_count(self) -> int:
|
|
164
|
+
"""
|
|
165
|
+
Total number of records indexed across all cached plugins.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
int: Aggregate record count.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
BethkitClosedError: If this cache has already been closed.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
return _ffi.load_lib().bethkit_plugin_cache_record_count(
|
|
175
|
+
self.__check_open()
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def resolve(
|
|
179
|
+
self, plugin_name: str, object_id: int
|
|
180
|
+
) -> Optional[object]:
|
|
181
|
+
"""
|
|
182
|
+
Look up a record by its global FormID components.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
plugin_name (str): Name of the owning plugin.
|
|
186
|
+
object_id (int): 24-bit object ID within that plugin.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Optional[Record]: The matching :class:`~bethkit.Record`, or
|
|
190
|
+
``None`` if not found.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
BethkitClosedError: If this cache has already been closed.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
from .plugin import Record
|
|
197
|
+
|
|
198
|
+
lib = _ffi.load_lib()
|
|
199
|
+
ptr_val = lib.bethkit_plugin_cache_resolve(
|
|
200
|
+
self.__check_open(), _ffi.senc(plugin_name), object_id
|
|
201
|
+
)
|
|
202
|
+
if not ptr_val:
|
|
203
|
+
return None
|
|
204
|
+
return Record(ptr_val, self)
|
|
205
|
+
|
|
206
|
+
def find_by_editor_id(self, edid: str) -> Optional[CacheHit]:
|
|
207
|
+
"""
|
|
208
|
+
Search for a record by its EDID (editor ID) string.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
edid (str): The editor ID to search for (e.g. ``"ArmorIron"``).
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Optional[CacheHit]: A :class:`CacheHit` containing the matched
|
|
215
|
+
record and its :class:`~bethkit.GlobalFormId`, or ``None`` if
|
|
216
|
+
not found.
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
BethkitClosedError: If this cache has already been closed.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
from .plugin import Record
|
|
223
|
+
|
|
224
|
+
lib = _ffi.load_lib()
|
|
225
|
+
out = BethkitGlobalFormId()
|
|
226
|
+
ptr_val = lib.bethkit_plugin_cache_find_by_editor_id(
|
|
227
|
+
self.__check_open(), _ffi.senc(edid), ctypes.byref(out)
|
|
228
|
+
)
|
|
229
|
+
if not ptr_val:
|
|
230
|
+
return None
|
|
231
|
+
plugin_name_raw: Optional[bytes] = out.plugin_name
|
|
232
|
+
plugin_name = (
|
|
233
|
+
plugin_name_raw.decode("utf-8") if plugin_name_raw else ""
|
|
234
|
+
)
|
|
235
|
+
gfid = GlobalFormId(plugin_name=plugin_name, object_id=out.object_id)
|
|
236
|
+
return CacheHit(record=Record(ptr_val, self), global_form_id=gfid)
|
|
237
|
+
|
|
238
|
+
def __repr__(self) -> str:
|
|
239
|
+
"""
|
|
240
|
+
Returns:
|
|
241
|
+
str: Developer-friendly representation with plugin and record
|
|
242
|
+
counts.
|
|
243
|
+
"""
|
|
244
|
+
|
|
245
|
+
if not self.__ptr:
|
|
246
|
+
return "<PluginCache closed>"
|
|
247
|
+
return (
|
|
248
|
+
f"<PluginCache plugins={len(self)} records={self.record_count}>"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|