PyREUser3 0.1.0__py3-none-any.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.
- pyreuser3/__init__.py +65 -0
- pyreuser3/__main__.py +7 -0
- pyreuser3/api.py +410 -0
- pyreuser3/cli.py +207 -0
- pyreuser3/core.py +358 -0
- pyreuser3/export/__init__.py +11 -0
- pyreuser3/export/base.py +245 -0
- pyreuser3/export/enums.py +171 -0
- pyreuser3/export/fields.py +231 -0
- pyreuser3/export/metadata.py +304 -0
- pyreuser3/export/postprocess.py +346 -0
- pyreuser3/export/tree.py +289 -0
- pyreuser3/export/user3.py +415 -0
- pyreuser3/pack/__init__.py +10 -0
- pyreuser3/pack/base.py +281 -0
- pyreuser3/pack/models.py +140 -0
- pyreuser3/pack/plan.py +566 -0
- pyreuser3/pack/writer.py +313 -0
- pyreuser3/rich_ui.py +126 -0
- pyreuser3/schema.py +193 -0
- pyreuser3/web/__init__.py +6 -0
- pyreuser3/web/__main__.py +6 -0
- pyreuser3/web/handler.py +178 -0
- pyreuser3/web/jobs.py +243 -0
- pyreuser3/web/page.py +221 -0
- pyreuser3/web/picker.py +92 -0
- pyreuser3/web/runners.py +238 -0
- pyreuser3/web/server.py +104 -0
- pyreuser3/web/settings.py +42 -0
- pyreuser3-0.1.0.dist-info/METADATA +165 -0
- pyreuser3-0.1.0.dist-info/RECORD +35 -0
- pyreuser3-0.1.0.dist-info/WHEEL +5 -0
- pyreuser3-0.1.0.dist-info/entry_points.txt +3 -0
- pyreuser3-0.1.0.dist-info/licenses/LICENSE +21 -0
- pyreuser3-0.1.0.dist-info/top_level.txt +1 -0
pyreuser3/pack/writer.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""RSZ/USR 二进制写回逻辑。
|
|
2
|
+
|
|
3
|
+
写入阶段消费规划阶段产出的实例列表,按 RE Engine 物理布局拼出字节:先连续
|
|
4
|
+
写入各实例数据段,再写 RSZ 头、对象表、实例表,最后包上最小 USR 头。字段值
|
|
5
|
+
按类型写入,并兼容枚举标签/成员名、原始字节回写等多种来源。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import uuid
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .models import BinaryWriter, InstanceRef, PackError, StructValue
|
|
15
|
+
from ..core import align
|
|
16
|
+
from ..schema import FieldDef
|
|
17
|
+
|
|
18
|
+
# 匹配导出格式 `[123] MemberName`,用于从枚举标签中取出括号内数值。
|
|
19
|
+
ENUM_LABEL_RE = re.compile(r"^\[(-?\d+)\]\s*(.*)$")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class PackerWriterMixin:
|
|
23
|
+
"""负责把规划后的实例表编码成 `.user.3` 字节。"""
|
|
24
|
+
|
|
25
|
+
def _build_binary(self, root_ids: list[int]) -> bytes:
|
|
26
|
+
"""构造完整 USR + RSZ 二进制内容。
|
|
27
|
+
|
|
28
|
+
参数:
|
|
29
|
+
root_ids (list[int]): 根实例编号列表,写入 RSZ 对象表。
|
|
30
|
+
|
|
31
|
+
返回:
|
|
32
|
+
bytes: 完整的 ``.user.3`` 二进制字节。
|
|
33
|
+
"""
|
|
34
|
+
data_writer = BinaryWriter()
|
|
35
|
+
for spec in self.instances[1:]:
|
|
36
|
+
if spec is None:
|
|
37
|
+
continue
|
|
38
|
+
# 先连续写入所有实例的数据段,稍后再计算 RSZ 表偏移。
|
|
39
|
+
self._write_instance(data_writer, spec)
|
|
40
|
+
|
|
41
|
+
object_count = len(root_ids)
|
|
42
|
+
instance_count = len(self.instances)
|
|
43
|
+
# 对象表紧跟 48 字节 RSZ 头,实例表再随其后,数据段按 16 字节对齐。
|
|
44
|
+
instance_offset = 48 + object_count * 4
|
|
45
|
+
data_offset = align(instance_offset + instance_count * 8, 16)
|
|
46
|
+
|
|
47
|
+
rsz_writer = BinaryWriter()
|
|
48
|
+
# RSZ 头固定为 48 字节,偏移均相对 RSZ 块起点。
|
|
49
|
+
rsz_writer.write_struct(
|
|
50
|
+
"<IIiiiiqqq",
|
|
51
|
+
self.rsz_magic,
|
|
52
|
+
16,
|
|
53
|
+
object_count,
|
|
54
|
+
instance_count,
|
|
55
|
+
0,
|
|
56
|
+
0,
|
|
57
|
+
instance_offset,
|
|
58
|
+
data_offset,
|
|
59
|
+
data_offset,
|
|
60
|
+
)
|
|
61
|
+
for root_id in root_ids:
|
|
62
|
+
rsz_writer.write_struct("<i", root_id)
|
|
63
|
+
rsz_writer.pad_to(instance_offset)
|
|
64
|
+
# 实例 0 是空引用槽,对应哈希和 CRC 均为 0。
|
|
65
|
+
rsz_writer.write_struct("<II", 0, 0)
|
|
66
|
+
for spec in self.instances[1:]:
|
|
67
|
+
if spec is None:
|
|
68
|
+
continue
|
|
69
|
+
rsz_writer.write_struct("<II", spec.class_hash, spec.class_def.crc)
|
|
70
|
+
rsz_writer.pad_to(data_offset)
|
|
71
|
+
rsz_writer.write(bytes(data_writer.data))
|
|
72
|
+
|
|
73
|
+
usr_writer = BinaryWriter()
|
|
74
|
+
# 当前封包器生成最小 USR 头:资源表和用户数据表为空,数据偏移指向 RSZ。
|
|
75
|
+
usr_writer.write_struct("<IiiiQQQ", self.user_magic, 0, 0, 0, 0x30, 0x30, 0x30)
|
|
76
|
+
usr_writer.write(b"\x00" * 8)
|
|
77
|
+
usr_writer.write(bytes(rsz_writer.data))
|
|
78
|
+
return bytes(usr_writer.data)
|
|
79
|
+
|
|
80
|
+
def _write_instance(self, writer: BinaryWriter, spec: InstanceSpec) -> None:
|
|
81
|
+
"""按模板字段顺序写入一个实例。
|
|
82
|
+
|
|
83
|
+
参数:
|
|
84
|
+
writer (BinaryWriter): 目标二进制写入器(数据段)。
|
|
85
|
+
spec (InstanceSpec): 规划好的实例规格。
|
|
86
|
+
|
|
87
|
+
返回:
|
|
88
|
+
None: 把实例数据写入 ``writer``。
|
|
89
|
+
"""
|
|
90
|
+
for field_def in spec.class_def.fields:
|
|
91
|
+
# 数组字段按 4 字节对齐,普通字段按模板声明的对齐值对齐。
|
|
92
|
+
writer.align(4 if field_def.is_array else max(field_def.align, 1))
|
|
93
|
+
key = field_def.name or "unnamed"
|
|
94
|
+
self._write_field(writer, field_def, spec.fields.get(key))
|
|
95
|
+
|
|
96
|
+
def _write_field(
|
|
97
|
+
self, writer: BinaryWriter, field_def: FieldDef, value: Any
|
|
98
|
+
) -> None:
|
|
99
|
+
"""写入一个字段,自动处理数组和标量。
|
|
100
|
+
|
|
101
|
+
参数:
|
|
102
|
+
writer (BinaryWriter): 目标二进制写入器。
|
|
103
|
+
field_def (FieldDef): 字段定义。
|
|
104
|
+
value (Any): 字段的中间表示值。
|
|
105
|
+
|
|
106
|
+
返回:
|
|
107
|
+
None: 把字段数据写入 ``writer``。
|
|
108
|
+
"""
|
|
109
|
+
if field_def.is_array:
|
|
110
|
+
items = value if isinstance(value, list) else []
|
|
111
|
+
writer.write_struct("<I", len(items))
|
|
112
|
+
non_array = FieldDef(
|
|
113
|
+
name=field_def.name,
|
|
114
|
+
field_type=field_def.field_type,
|
|
115
|
+
original_type=field_def.original_type,
|
|
116
|
+
size=field_def.size,
|
|
117
|
+
align=field_def.align,
|
|
118
|
+
is_array=False,
|
|
119
|
+
)
|
|
120
|
+
for item in items:
|
|
121
|
+
writer.align(max(field_def.align, 1))
|
|
122
|
+
# 数组元素不再写数组头,按非数组字段写入。
|
|
123
|
+
self._write_scalar(writer, non_array, item)
|
|
124
|
+
return
|
|
125
|
+
self._write_scalar(writer, field_def, value)
|
|
126
|
+
|
|
127
|
+
def _write_scalar(
|
|
128
|
+
self, writer: BinaryWriter, field_def: FieldDef, value: Any
|
|
129
|
+
) -> None:
|
|
130
|
+
"""按字段类型写入标量值。
|
|
131
|
+
|
|
132
|
+
参数:
|
|
133
|
+
writer (BinaryWriter): 目标二进制写入器。
|
|
134
|
+
field_def (FieldDef): 字段定义,决定写入格式与尺寸。
|
|
135
|
+
value (Any): 待写入的标量值(可能是数字、字符串、引用或保留原始字节的 dict)。
|
|
136
|
+
|
|
137
|
+
返回:
|
|
138
|
+
None: 把标量数据写入 ``writer``;无法识别的类型按声明尺寸补零。
|
|
139
|
+
"""
|
|
140
|
+
t = field_def.field_type
|
|
141
|
+
if t == "Bool":
|
|
142
|
+
writer.write_struct("<B", 1 if bool(value) else 0)
|
|
143
|
+
return
|
|
144
|
+
if t == "S8":
|
|
145
|
+
writer.write_struct("<b", self._coerce_int(value, field_def))
|
|
146
|
+
return
|
|
147
|
+
if t == "U8":
|
|
148
|
+
writer.write_struct("<B", self._coerce_int(value, field_def) & 0xFF)
|
|
149
|
+
return
|
|
150
|
+
if t == "S16":
|
|
151
|
+
writer.write_struct("<h", self._coerce_int(value, field_def))
|
|
152
|
+
return
|
|
153
|
+
if t == "U16":
|
|
154
|
+
writer.write_struct("<H", self._coerce_int(value, field_def) & 0xFFFF)
|
|
155
|
+
return
|
|
156
|
+
if t in {"S32", "Enum", "Sfix"}:
|
|
157
|
+
writer.write_struct("<i", self._to_s32(self._coerce_int(value, field_def)))
|
|
158
|
+
return
|
|
159
|
+
if t == "U32":
|
|
160
|
+
writer.write_struct("<I", self._coerce_int(value, field_def) & 0xFFFFFFFF)
|
|
161
|
+
return
|
|
162
|
+
if t == "S64":
|
|
163
|
+
writer.write_struct("<q", self._coerce_int(value, field_def))
|
|
164
|
+
return
|
|
165
|
+
if t == "U64":
|
|
166
|
+
writer.write_struct(
|
|
167
|
+
"<Q", self._coerce_int(value, field_def) & 0xFFFFFFFFFFFFFFFF
|
|
168
|
+
)
|
|
169
|
+
return
|
|
170
|
+
if t == "F32":
|
|
171
|
+
writer.write_struct("<f", float(value or 0.0))
|
|
172
|
+
return
|
|
173
|
+
if t == "F64":
|
|
174
|
+
writer.write_struct("<d", float(value or 0.0))
|
|
175
|
+
return
|
|
176
|
+
if t in {"Object", "UserData"}:
|
|
177
|
+
# 对象字段最终只写入目标实例编号。
|
|
178
|
+
ref_id = (
|
|
179
|
+
value.index
|
|
180
|
+
if isinstance(value, InstanceRef)
|
|
181
|
+
else self._coerce_int(value, field_def)
|
|
182
|
+
)
|
|
183
|
+
writer.write_struct("<i", ref_id)
|
|
184
|
+
return
|
|
185
|
+
if t in {"String", "Resource"}:
|
|
186
|
+
# RE Engine 字符串保存长度前缀,并以 UTF-16LE 空字符结尾。
|
|
187
|
+
writer.align(4)
|
|
188
|
+
raw = f"{value or ''}\x00".encode("utf-16-le")
|
|
189
|
+
writer.write_struct("<I", len(raw) // 2)
|
|
190
|
+
writer.write(raw)
|
|
191
|
+
return
|
|
192
|
+
if t == "C8":
|
|
193
|
+
# C8 使用 UTF-8 字节长度,同样保留结尾空字符。
|
|
194
|
+
writer.align(4)
|
|
195
|
+
raw = f"{value or ''}\x00".encode("utf-8")
|
|
196
|
+
writer.write_struct("<I", len(raw))
|
|
197
|
+
writer.write(raw)
|
|
198
|
+
return
|
|
199
|
+
if t in {"Guid", "GameObjectRef", "Uri"}:
|
|
200
|
+
writer.write(uuid.UUID(str(value)).bytes_le)
|
|
201
|
+
return
|
|
202
|
+
if t == "Struct":
|
|
203
|
+
self._write_struct(writer, value)
|
|
204
|
+
return
|
|
205
|
+
if t in {
|
|
206
|
+
"Float2",
|
|
207
|
+
"Float3",
|
|
208
|
+
"Float4",
|
|
209
|
+
"Vec2",
|
|
210
|
+
"Vec3",
|
|
211
|
+
"Vec4",
|
|
212
|
+
"Quaternion",
|
|
213
|
+
"Color",
|
|
214
|
+
"AABB",
|
|
215
|
+
"Capsule",
|
|
216
|
+
"OBB",
|
|
217
|
+
"Mat3",
|
|
218
|
+
"Mat4",
|
|
219
|
+
"Position",
|
|
220
|
+
}:
|
|
221
|
+
# 向量/矩阵类型按 4 字节浮点元素写入,不足部分补 0。
|
|
222
|
+
values = value if isinstance(value, list) else []
|
|
223
|
+
count = max(field_def.size // 4, 1)
|
|
224
|
+
for i in range(count):
|
|
225
|
+
writer.write_struct("<f", float(values[i]) if i < len(values) else 0.0)
|
|
226
|
+
return
|
|
227
|
+
if isinstance(value, dict) and isinstance(value.get("raw"), str):
|
|
228
|
+
# 未知字段或未知结构体保留原始十六进制时,尽量原样写回。
|
|
229
|
+
writer.write(bytes.fromhex(value["raw"]))
|
|
230
|
+
return
|
|
231
|
+
# 仍无法识别时按声明尺寸补零,保证后续字段偏移不被破坏。
|
|
232
|
+
writer.write(b"\x00" * max(field_def.size, 0))
|
|
233
|
+
|
|
234
|
+
def _write_struct(self, writer: BinaryWriter, value: Any) -> None:
|
|
235
|
+
"""写入结构体字段。
|
|
236
|
+
|
|
237
|
+
参数:
|
|
238
|
+
writer (BinaryWriter): 目标二进制写入器。
|
|
239
|
+
value (Any): 结构体值,应为 :class:`StructValue`;保留原始字节的 dict 也可。
|
|
240
|
+
|
|
241
|
+
返回:
|
|
242
|
+
None: 把结构体数据写入 ``writer``,并按声明尺寸补齐尾部填充。
|
|
243
|
+
"""
|
|
244
|
+
if not isinstance(value, StructValue):
|
|
245
|
+
raw = value.get("raw") if isinstance(value, dict) else None
|
|
246
|
+
if isinstance(raw, str):
|
|
247
|
+
writer.write(bytes.fromhex(raw))
|
|
248
|
+
return
|
|
249
|
+
start = writer.tell()
|
|
250
|
+
for field_def in value.class_def.fields:
|
|
251
|
+
writer.align(4 if field_def.is_array else max(field_def.align, 1))
|
|
252
|
+
key = field_def.name or "unnamed"
|
|
253
|
+
self._write_field(writer, field_def, value.fields.get(key))
|
|
254
|
+
consumed = writer.tell() - start
|
|
255
|
+
if value.declared_size > consumed:
|
|
256
|
+
# 结构体实际字段小于声明尺寸时,需要补齐尾部填充。
|
|
257
|
+
writer.write(b"\x00" * (value.declared_size - consumed))
|
|
258
|
+
|
|
259
|
+
def _coerce_int(self, value: Any, field_def: FieldDef) -> int:
|
|
260
|
+
"""把 JSON 值转换为整数,兼容枚举标签和成员名。
|
|
261
|
+
|
|
262
|
+
参数:
|
|
263
|
+
value (Any): JSON 值:布尔、整数、浮点或字符串(数字 / ``[值] 名称`` / 成员名)。
|
|
264
|
+
field_def (FieldDef): 字段定义,提供枚举成员反查所需的类型上下文。
|
|
265
|
+
|
|
266
|
+
返回:
|
|
267
|
+
int: 转换后的整数值。
|
|
268
|
+
|
|
269
|
+
异常:
|
|
270
|
+
PackError: 当值无法解析为整数且不是已知枚举成员名时抛出。
|
|
271
|
+
"""
|
|
272
|
+
if isinstance(value, bool):
|
|
273
|
+
return int(value)
|
|
274
|
+
if isinstance(value, int):
|
|
275
|
+
return value
|
|
276
|
+
if isinstance(value, float):
|
|
277
|
+
return int(value)
|
|
278
|
+
if isinstance(value, str):
|
|
279
|
+
text = value.strip()
|
|
280
|
+
match = ENUM_LABEL_RE.match(text)
|
|
281
|
+
if match:
|
|
282
|
+
# 导出格式 `[123] MemberName` 优先使用括号内数值。
|
|
283
|
+
return int(match.group(1))
|
|
284
|
+
try:
|
|
285
|
+
return int(text, 0)
|
|
286
|
+
except ValueError:
|
|
287
|
+
# 如果不是数字字符串,再尝试按枚举成员名反查。
|
|
288
|
+
enum_value = self._resolve_enum_member(text, field_def)
|
|
289
|
+
if enum_value is not None:
|
|
290
|
+
return enum_value
|
|
291
|
+
raise PackError(f"cannot convert {value!r} to int for field {field_def.name}")
|
|
292
|
+
|
|
293
|
+
def _resolve_enum_member(self, text: str, field_def: FieldDef) -> int | None:
|
|
294
|
+
"""按字段类型上下文把枚举成员名解析成数值。
|
|
295
|
+
|
|
296
|
+
参数:
|
|
297
|
+
text (str): 枚举成员名。
|
|
298
|
+
field_def (FieldDef): 字段定义,其原始类型用于推断对应的固定枚举类型。
|
|
299
|
+
|
|
300
|
+
返回:
|
|
301
|
+
int | None: 命中时返回成员数值;无法解析时返回 ``None``。
|
|
302
|
+
"""
|
|
303
|
+
candidates = []
|
|
304
|
+
original = field_def.original_type
|
|
305
|
+
if original.endswith("_Serializable"):
|
|
306
|
+
candidates.append(f"{original[:-13]}_Fixed")
|
|
307
|
+
if original.endswith("_Fixed"):
|
|
308
|
+
candidates.append(original)
|
|
309
|
+
for enum_type in candidates:
|
|
310
|
+
member_map = self.member_lookup.get(enum_type)
|
|
311
|
+
if member_map and text in member_map:
|
|
312
|
+
return member_map[text]
|
|
313
|
+
return None
|
pyreuser3/rich_ui.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""基于 Rich 的批处理进度条和日志输出工具。
|
|
2
|
+
|
|
3
|
+
提供一个全局共享的 Rich 控制台,以及把进度条固定在底部、日志滚动输出到上方的
|
|
4
|
+
:class:`BatchProgress` 上下文管理器,供导出/封包批处理复用。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
MofNCompleteColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
SpinnerColumn,
|
|
17
|
+
TaskID,
|
|
18
|
+
TextColumn,
|
|
19
|
+
TimeElapsedColumn,
|
|
20
|
+
TimeRemainingColumn,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# 全局唯一的 Rich 控制台,确保进度条与日志共享同一渲染区域。
|
|
24
|
+
_CONSOLE = Console()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_console() -> Console:
|
|
28
|
+
"""返回命令行批处理共用的 Rich 控制台。
|
|
29
|
+
|
|
30
|
+
返回:
|
|
31
|
+
Console: 模块级单例 :class:`rich.console.Console` 实例。
|
|
32
|
+
"""
|
|
33
|
+
return _CONSOLE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BatchProgress:
|
|
37
|
+
"""让 Rich 进度条固定在底部,并把日志滚动输出到上方。
|
|
38
|
+
|
|
39
|
+
作为上下文管理器使用:进入时启动进度条并创建任务,退出时关闭进度条。
|
|
40
|
+
期间可通过 :meth:`log` 输出滚动日志、:meth:`update` 推进进度。
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self, description: str, total: int, unit: str = "file") -> None:
|
|
44
|
+
"""初始化批处理进度条配置。
|
|
45
|
+
|
|
46
|
+
参数:
|
|
47
|
+
description (str): 进度条左侧显示的任务描述。
|
|
48
|
+
total (int): 任务总量,用于计算完成度和预计剩余时间。
|
|
49
|
+
unit (str): 计量单位文本(如 ``"file"``),仅用于展示。
|
|
50
|
+
|
|
51
|
+
返回:
|
|
52
|
+
None: 构造函数,仅准备 Rich 进度条对象(尚未启动)。
|
|
53
|
+
"""
|
|
54
|
+
self.description = description
|
|
55
|
+
self.total = total
|
|
56
|
+
self.unit = unit
|
|
57
|
+
self.console = get_console()
|
|
58
|
+
# 预先组装进度条的各列:转轮、描述、进度条、计数、已用时和预计剩余时间。
|
|
59
|
+
self._progress = Progress(
|
|
60
|
+
SpinnerColumn(),
|
|
61
|
+
TextColumn("[progress.description]{task.description}"),
|
|
62
|
+
BarColumn(),
|
|
63
|
+
MofNCompleteColumn(),
|
|
64
|
+
TextColumn(unit),
|
|
65
|
+
TextColumn("elapsed"),
|
|
66
|
+
TimeElapsedColumn(),
|
|
67
|
+
TextColumn("eta"),
|
|
68
|
+
TimeRemainingColumn(),
|
|
69
|
+
console=self.console,
|
|
70
|
+
expand=True,
|
|
71
|
+
transient=False,
|
|
72
|
+
)
|
|
73
|
+
self._task_id: TaskID | None = None
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> "BatchProgress":
|
|
76
|
+
"""进入上下文:启动进度条并创建任务。
|
|
77
|
+
|
|
78
|
+
返回:
|
|
79
|
+
BatchProgress: 自身,便于 ``with ... as progress`` 语法使用。
|
|
80
|
+
"""
|
|
81
|
+
self._progress.__enter__()
|
|
82
|
+
self._task_id = self._progress.add_task(self.description, total=self.total)
|
|
83
|
+
return self
|
|
84
|
+
|
|
85
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
86
|
+
"""退出上下文:关闭进度条。
|
|
87
|
+
|
|
88
|
+
参数:
|
|
89
|
+
exc_type (Any): 异常类型(无异常时为 ``None``)。
|
|
90
|
+
exc (Any): 异常实例(无异常时为 ``None``)。
|
|
91
|
+
tb (Any): 异常回溯对象(无异常时为 ``None``)。
|
|
92
|
+
|
|
93
|
+
返回:
|
|
94
|
+
bool: 始终返回 ``False``,表示不吞掉上下文内发生的异常。
|
|
95
|
+
"""
|
|
96
|
+
self._progress.__exit__(exc_type, exc, tb)
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def log(self, message: str, style: str | None = None) -> None:
|
|
100
|
+
"""在实时进度条上方输出一行日志。
|
|
101
|
+
|
|
102
|
+
参数:
|
|
103
|
+
message (str): 日志文本。
|
|
104
|
+
style (str | None): 可选的 Rich 样式名(如 ``"green"`` / ``"red"``)。
|
|
105
|
+
|
|
106
|
+
返回:
|
|
107
|
+
None: 直接输出到共享控制台。
|
|
108
|
+
"""
|
|
109
|
+
self.console.log(message, style=style)
|
|
110
|
+
|
|
111
|
+
def update(self, advance: int = 1, description: str | None = None) -> None:
|
|
112
|
+
"""推进当前任务,并可同时更新底部进度条标签。
|
|
113
|
+
|
|
114
|
+
参数:
|
|
115
|
+
advance (int): 本次推进的进度量;传 0 可只更新描述不推进进度。
|
|
116
|
+
description (str | None): 可选的新任务描述;为 ``None`` 时保持不变。
|
|
117
|
+
|
|
118
|
+
返回:
|
|
119
|
+
None: 更新内部进度状态;尚未创建任务时直接返回。
|
|
120
|
+
"""
|
|
121
|
+
if self._task_id is None:
|
|
122
|
+
return
|
|
123
|
+
kwargs: dict[str, Any] = {"advance": advance}
|
|
124
|
+
if description is not None:
|
|
125
|
+
kwargs["description"] = description
|
|
126
|
+
self._progress.update(self._task_id, **kwargs)
|
pyreuser3/schema.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"""RE_RSZ 模板类型数据库。
|
|
2
|
+
|
|
3
|
+
本模块负责把外部的 RE_RSZ 模板 JSON 加载成内存中的类型索引,供导出器和
|
|
4
|
+
封包器按类型哈希查找字段布局,并提供 RE_RSZ 类型名常用的 MurmurHash3 计算。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def murmur3_32(data: bytes, seed: int = 0xFFFFFFFF) -> int:
|
|
15
|
+
"""计算 RE_RSZ 类型名常用的 MurmurHash3 32 位哈希。
|
|
16
|
+
|
|
17
|
+
参数:
|
|
18
|
+
data (bytes): 输入字节,通常是 UTF-8 编码的类型名。
|
|
19
|
+
seed (int): 哈希种子,RE_RSZ 模板通常使用 ``0xFFFFFFFF``。
|
|
20
|
+
|
|
21
|
+
返回:
|
|
22
|
+
int: 32 位无符号哈希值(0 ~ 0xFFFFFFFF)。
|
|
23
|
+
"""
|
|
24
|
+
c1 = 0xCC9E2D51
|
|
25
|
+
c2 = 0x1B873593
|
|
26
|
+
h1 = seed & 0xFFFFFFFF
|
|
27
|
+
length = len(data)
|
|
28
|
+
# 主体按 4 字节块处理,rounded_end 是最后一个完整块的结束位置。
|
|
29
|
+
rounded_end = length & ~0x3
|
|
30
|
+
|
|
31
|
+
# MurmurHash3 以 4 字节块为主体处理,尾部不足 4 字节再单独混合。
|
|
32
|
+
for i in range(0, rounded_end, 4):
|
|
33
|
+
k1 = int.from_bytes(data[i : i + 4], "little")
|
|
34
|
+
k1 = (k1 * c1) & 0xFFFFFFFF
|
|
35
|
+
k1 = ((k1 << 15) | (k1 >> 17)) & 0xFFFFFFFF
|
|
36
|
+
k1 = (k1 * c2) & 0xFFFFFFFF
|
|
37
|
+
h1 ^= k1
|
|
38
|
+
h1 = ((h1 << 13) | (h1 >> 19)) & 0xFFFFFFFF
|
|
39
|
+
h1 = (h1 * 5 + 0xE6546B64) & 0xFFFFFFFF
|
|
40
|
+
|
|
41
|
+
# 处理尾部 1-3 字节。这里严格保持小端序位移顺序。
|
|
42
|
+
k1 = 0
|
|
43
|
+
tail = data[rounded_end:]
|
|
44
|
+
if len(tail) == 3:
|
|
45
|
+
k1 ^= tail[2] << 16
|
|
46
|
+
if len(tail) >= 2:
|
|
47
|
+
k1 ^= tail[1] << 8
|
|
48
|
+
if len(tail) >= 1:
|
|
49
|
+
k1 ^= tail[0]
|
|
50
|
+
k1 = (k1 * c1) & 0xFFFFFFFF
|
|
51
|
+
k1 = ((k1 << 15) | (k1 >> 17)) & 0xFFFFFFFF
|
|
52
|
+
k1 = (k1 * c2) & 0xFFFFFFFF
|
|
53
|
+
h1 ^= k1
|
|
54
|
+
|
|
55
|
+
# 最终混合阶段把长度和高低位充分混合,得到最终 32 位结果。
|
|
56
|
+
h1 ^= length
|
|
57
|
+
h1 ^= h1 >> 16
|
|
58
|
+
h1 = (h1 * 0x85EBCA6B) & 0xFFFFFFFF
|
|
59
|
+
h1 ^= h1 >> 13
|
|
60
|
+
h1 = (h1 * 0xC2B2AE35) & 0xFFFFFFFF
|
|
61
|
+
h1 ^= h1 >> 16
|
|
62
|
+
return h1 & 0xFFFFFFFF
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class FieldDef:
|
|
67
|
+
"""RE_RSZ 模板中的字段定义。
|
|
68
|
+
|
|
69
|
+
描述一个类型内单个字段的二进制布局信息,是解析和封包时按类型读写
|
|
70
|
+
数据段的依据。
|
|
71
|
+
|
|
72
|
+
属性:
|
|
73
|
+
name (str): 字段名;模板中可能为空字符串。
|
|
74
|
+
field_type (str): 字段的逻辑类型(如 ``S32``、``String``、``Struct`` 等)。
|
|
75
|
+
original_type (str): 字段在游戏中的原始类型名,用于结构体/枚举推断。
|
|
76
|
+
size (int): 字段声明的字节尺寸。
|
|
77
|
+
align (int): 字段要求的对齐字节数。
|
|
78
|
+
is_array (bool): 是否为数组字段(数组带 4 字节长度前缀)。
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
name: str
|
|
82
|
+
field_type: str
|
|
83
|
+
original_type: str
|
|
84
|
+
size: int
|
|
85
|
+
align: int
|
|
86
|
+
is_array: bool
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class ClassDef:
|
|
91
|
+
"""RE_RSZ 模板中的类型定义。
|
|
92
|
+
|
|
93
|
+
属性:
|
|
94
|
+
name (str): 类型完整名称(通常含命名空间)。
|
|
95
|
+
crc (int): 类型的 CRC 校验值,写回 RSZ 实例表时需要。
|
|
96
|
+
fields (list[FieldDef]): 按声明顺序排列的字段列表。
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
name: str
|
|
100
|
+
crc: int
|
|
101
|
+
fields: list[FieldDef]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class TypeDB:
|
|
105
|
+
"""封装 RE_RSZ 模板中的类型索引。
|
|
106
|
+
|
|
107
|
+
内部以“类型哈希 -> 类型定义”为主索引,并额外维护“类型名 -> 哈希”的
|
|
108
|
+
反查表,供封包阶段从 JSON 中的类名还原出类型哈希。
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, classes: dict[int, ClassDef]):
|
|
112
|
+
"""初始化类型数据库。
|
|
113
|
+
|
|
114
|
+
参数:
|
|
115
|
+
classes (dict[int, ClassDef]): 以类型哈希为键、类型定义为值的映射。
|
|
116
|
+
"""
|
|
117
|
+
self.classes = classes
|
|
118
|
+
# name_to_hash 用于封包时从 JSON 类名反查类型哈希。
|
|
119
|
+
self.name_to_hash = {c.name: h for h, c in classes.items()}
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def load(cls, json_path: Path) -> "TypeDB":
|
|
123
|
+
"""从 RE_RSZ 模板 JSON 读取并构建类型数据库。
|
|
124
|
+
|
|
125
|
+
参数:
|
|
126
|
+
json_path (Path): 模板 JSON 文件路径。
|
|
127
|
+
|
|
128
|
+
返回:
|
|
129
|
+
TypeDB: 已加载完毕、可供查询的类型数据库实例。
|
|
130
|
+
"""
|
|
131
|
+
with json_path.open("r", encoding="utf-8") as f:
|
|
132
|
+
raw = json.load(f)
|
|
133
|
+
|
|
134
|
+
classes: dict[int, ClassDef] = {}
|
|
135
|
+
for key, value in raw.items():
|
|
136
|
+
try:
|
|
137
|
+
# RE_RSZ 模板通常以十六进制字符串保存类型哈希。
|
|
138
|
+
class_hash = int(key, 16)
|
|
139
|
+
except ValueError:
|
|
140
|
+
# 非十六进制键(如说明性字段)直接跳过,不影响类型加载。
|
|
141
|
+
continue
|
|
142
|
+
fields: list[FieldDef] = []
|
|
143
|
+
for field in value.get("fields", []):
|
|
144
|
+
# 字段缺省值尽量保守,保证模板中少数字段缺属性时仍能加载。
|
|
145
|
+
fields.append(
|
|
146
|
+
FieldDef(
|
|
147
|
+
name=field.get("name", ""),
|
|
148
|
+
field_type=field.get("type", "Data"),
|
|
149
|
+
original_type=field.get("original_type", ""),
|
|
150
|
+
size=int(field.get("size", 0)),
|
|
151
|
+
align=int(field.get("align", 1)),
|
|
152
|
+
is_array=bool(field.get("array", False)),
|
|
153
|
+
)
|
|
154
|
+
)
|
|
155
|
+
crc_raw = value.get("crc", "0")
|
|
156
|
+
# CRC 可能是十六进制字符串,也可能已经是整数,两种都兼容。
|
|
157
|
+
crc = int(crc_raw, 16) if isinstance(crc_raw, str) else int(crc_raw)
|
|
158
|
+
classes[class_hash] = ClassDef(
|
|
159
|
+
name=value.get("name", ""), crc=crc, fields=fields
|
|
160
|
+
)
|
|
161
|
+
return cls(classes)
|
|
162
|
+
|
|
163
|
+
def get_class(self, class_hash: int) -> ClassDef | None:
|
|
164
|
+
"""按类型哈希查询类型定义。
|
|
165
|
+
|
|
166
|
+
参数:
|
|
167
|
+
class_hash (int): RE_RSZ 类型哈希。
|
|
168
|
+
|
|
169
|
+
返回:
|
|
170
|
+
ClassDef | None: 对应的类型定义;不存在时返回 ``None``。
|
|
171
|
+
"""
|
|
172
|
+
return self.classes.get(class_hash)
|
|
173
|
+
|
|
174
|
+
def resolve_struct_hash(self, original_type: str) -> int | None:
|
|
175
|
+
"""把结构体类型名解析为类型哈希。
|
|
176
|
+
|
|
177
|
+
参数:
|
|
178
|
+
original_type (str): 模板字段中记录的原始结构体类型名。
|
|
179
|
+
|
|
180
|
+
返回:
|
|
181
|
+
int | None: 找到的类型哈希;无法解析时返回 ``None``。
|
|
182
|
+
"""
|
|
183
|
+
if not original_type:
|
|
184
|
+
return None
|
|
185
|
+
known = self.name_to_hash.get(original_type)
|
|
186
|
+
if known is not None:
|
|
187
|
+
return known
|
|
188
|
+
# 有些结构体不会直接出现在 name_to_hash 中,需要按 RE_RSZ 规则
|
|
189
|
+
# 对类型名做 MurmurHash3 后再查模板。
|
|
190
|
+
maybe = murmur3_32(original_type.encode("utf-8"), seed=0xFFFFFFFF)
|
|
191
|
+
if maybe in self.classes:
|
|
192
|
+
return maybe
|
|
193
|
+
return None
|