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
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
"""完整 `.user.3` 文件结构解析逻辑。
|
|
2
|
+
|
|
3
|
+
本模块按 RE Engine 的物理布局读取 ``.user.3``:先解析 USR 头与可选的外部
|
|
4
|
+
用户数据路径表,再读取内嵌 RSZ 块的头部、对象表、实例表和用户数据表,最后
|
|
5
|
+
逐个解析实例数据段。解析结果既可用于构造可读紧凑树,也可转成可稳定回封的
|
|
6
|
+
完整实例表 JSON。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from ..core import BinaryReader, PACK_JSON_FORMAT, ParseError, align
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ExporterUser3ParserMixin:
|
|
18
|
+
"""负责读取 USR/RSZ 头、实例表和根对象列表。"""
|
|
19
|
+
|
|
20
|
+
def _parse_user3_document(self, user3_path: Path) -> dict[str, Any]:
|
|
21
|
+
"""解析完整 `.user.3` 文件并保留实例表级中间结果。
|
|
22
|
+
|
|
23
|
+
参数:
|
|
24
|
+
user3_path (Path): 源 ``.user.3`` 文件路径。
|
|
25
|
+
|
|
26
|
+
返回:
|
|
27
|
+
dict[str, Any]: 解析文档,含 USR/RSZ 头、根对象编号、实例元数据、
|
|
28
|
+
已解析实例、实例编号映射以及用户数据引用等键,供后续构造紧凑树或
|
|
29
|
+
封包 JSON 使用。
|
|
30
|
+
|
|
31
|
+
异常:
|
|
32
|
+
ParseError: 当 USR 或 RSZ magic 不匹配时抛出。
|
|
33
|
+
"""
|
|
34
|
+
reader = BinaryReader(user3_path.read_bytes())
|
|
35
|
+
|
|
36
|
+
# `.user.3` 最外层是 USR 头,magic 可由用户覆盖以兼容不同游戏。
|
|
37
|
+
magic = reader.read_u32()
|
|
38
|
+
if magic != self.user_magic:
|
|
39
|
+
raise ParseError(f"not a user file: magic={magic}")
|
|
40
|
+
|
|
41
|
+
usr_header = {
|
|
42
|
+
"signature": magic,
|
|
43
|
+
"resource_count": reader.read_s32(),
|
|
44
|
+
"userdata_count": reader.read_s32(),
|
|
45
|
+
"info_count": reader.read_s32(),
|
|
46
|
+
"resource_info_tbl": reader.read_u64(),
|
|
47
|
+
"userdata_info_tbl": reader.read_u64(),
|
|
48
|
+
"data_offset": reader.read_u64(),
|
|
49
|
+
}
|
|
50
|
+
header_userdata_infos: list[dict[str, Any]] = []
|
|
51
|
+
if usr_header["userdata_count"] > 0 and usr_header["userdata_info_tbl"] > 0:
|
|
52
|
+
try:
|
|
53
|
+
# 部分文件在 USR 头中带有外部 userdata 路径表。
|
|
54
|
+
reader.seek(usr_header["userdata_info_tbl"])
|
|
55
|
+
for idx in range(usr_header["userdata_count"]):
|
|
56
|
+
class_hash = reader.read_u32()
|
|
57
|
+
_crc = reader.read_u32()
|
|
58
|
+
path_offset = reader.read_u64()
|
|
59
|
+
class_name = (
|
|
60
|
+
self.typedb.get_class(class_hash).name
|
|
61
|
+
if self.typedb.get_class(class_hash)
|
|
62
|
+
else "Unknown Class"
|
|
63
|
+
)
|
|
64
|
+
header_userdata_infos.append(
|
|
65
|
+
{
|
|
66
|
+
"index": idx,
|
|
67
|
+
"class_hash": class_hash,
|
|
68
|
+
"class_name": class_name,
|
|
69
|
+
"path": reader.read_wstring_null(path_offset),
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
except Exception:
|
|
73
|
+
# 路径表解析失败不影响主 RSZ 数据块,降级为空列表。
|
|
74
|
+
header_userdata_infos = []
|
|
75
|
+
|
|
76
|
+
rsz_start = usr_header["data_offset"]
|
|
77
|
+
|
|
78
|
+
# 数据偏移指向内嵌 RSZ 块;后续偏移大多是相对 RSZ 起点。
|
|
79
|
+
reader.seek(rsz_start)
|
|
80
|
+
rsz_header = {
|
|
81
|
+
"magic": reader.read_u32(),
|
|
82
|
+
"version": reader.read_u32(),
|
|
83
|
+
"object_count": reader.read_s32(),
|
|
84
|
+
"instance_count": reader.read_s32(),
|
|
85
|
+
"userdata_count": reader.read_s32(),
|
|
86
|
+
"reserved": reader.read_s32(),
|
|
87
|
+
"instance_offset": reader.read_s64(),
|
|
88
|
+
"data_offset": reader.read_s64(),
|
|
89
|
+
"userdata_offset": reader.read_s64(),
|
|
90
|
+
}
|
|
91
|
+
if rsz_header["magic"] != self.rsz_magic:
|
|
92
|
+
raise ParseError(
|
|
93
|
+
f"RSZ magic mismatch at data_offset: {rsz_header['magic']}"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# 对象表保存根对象实例编号,是构造最终 JSON 根节点的首选来源。
|
|
97
|
+
reader.seek(rsz_start + 48)
|
|
98
|
+
object_table = [
|
|
99
|
+
reader.read_s32() for _i in range(max(rsz_header["object_count"], 0))
|
|
100
|
+
]
|
|
101
|
+
object_table_set = set(object_table)
|
|
102
|
+
|
|
103
|
+
instance_infos: list[dict[str, Any]] = []
|
|
104
|
+
reader.seek(rsz_start + rsz_header["instance_offset"])
|
|
105
|
+
for idx in range(max(rsz_header["instance_count"], 0)):
|
|
106
|
+
# 实例表只保存类型哈希和 CRC,真正字段数据在数据偏移后连续存放。
|
|
107
|
+
class_hash = reader.read_u32()
|
|
108
|
+
crc = reader.read_u32()
|
|
109
|
+
class_def = self.typedb.get_class(class_hash)
|
|
110
|
+
instance_infos.append(
|
|
111
|
+
{
|
|
112
|
+
"index": idx,
|
|
113
|
+
"hash": class_hash,
|
|
114
|
+
"class_name": class_def.name if class_def else "Unknown Class",
|
|
115
|
+
"crc": crc,
|
|
116
|
+
"is_object": idx in object_table_set,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
instance_info_map = {item["index"]: item for item in instance_infos}
|
|
120
|
+
|
|
121
|
+
rsz_userdata_instance_ids: list[int] = []
|
|
122
|
+
rsz_userdata_path_by_instance: dict[int, str] = {}
|
|
123
|
+
if rsz_header["userdata_count"] > 0 and rsz_header["userdata_offset"] > 0:
|
|
124
|
+
try:
|
|
125
|
+
# RSZ 用户数据表表示对其他用户数据文件的引用。
|
|
126
|
+
reader.seek(rsz_start + rsz_header["userdata_offset"])
|
|
127
|
+
for _i in range(rsz_header["userdata_count"]):
|
|
128
|
+
instance_id = reader.read_s32()
|
|
129
|
+
_type_hash = reader.read_u32()
|
|
130
|
+
path_offset = reader.read_u64()
|
|
131
|
+
if instance_id >= 0:
|
|
132
|
+
rsz_userdata_instance_ids.append(instance_id)
|
|
133
|
+
path = ""
|
|
134
|
+
if path_offset > 0 and rsz_start + path_offset < reader.size:
|
|
135
|
+
path = reader.read_wstring_null(rsz_start + path_offset)
|
|
136
|
+
rsz_userdata_path_by_instance[instance_id] = path
|
|
137
|
+
except Exception:
|
|
138
|
+
# 用户数据表异常时仍尝试解析内联实例,避免整文件失败。
|
|
139
|
+
rsz_userdata_instance_ids = []
|
|
140
|
+
rsz_userdata_path_by_instance = {}
|
|
141
|
+
rsz_userdata_instance_set = set(rsz_userdata_instance_ids)
|
|
142
|
+
|
|
143
|
+
parsed_instances: list[dict[str, Any]] = []
|
|
144
|
+
reader.seek(rsz_start + rsz_header["data_offset"])
|
|
145
|
+
for idx, info in enumerate(instance_infos):
|
|
146
|
+
class_hash = int(info["hash"])
|
|
147
|
+
if idx == 0:
|
|
148
|
+
# RSZ 实例 0 是固定空槽,引用 0 表示空引用。
|
|
149
|
+
parsed_instances.append(
|
|
150
|
+
{
|
|
151
|
+
"index": idx,
|
|
152
|
+
"class_name": info["class_name"],
|
|
153
|
+
"note": "null instance slot",
|
|
154
|
+
}
|
|
155
|
+
)
|
|
156
|
+
continue
|
|
157
|
+
if idx in rsz_userdata_instance_set:
|
|
158
|
+
# 外部用户数据引用不在当前数据段内展开,只记录路径和实例编号。
|
|
159
|
+
parsed_instances.append(
|
|
160
|
+
{
|
|
161
|
+
"index": idx,
|
|
162
|
+
"class_name": info["class_name"],
|
|
163
|
+
"is_userdata_reference": True,
|
|
164
|
+
"path": rsz_userdata_path_by_instance.get(idx, ""),
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
continue
|
|
168
|
+
cls = self.typedb.get_class(class_hash)
|
|
169
|
+
if cls is None:
|
|
170
|
+
# 模板不认识的类型无法解析字段,但保留元数据方便定位。
|
|
171
|
+
parsed_instances.append(
|
|
172
|
+
{
|
|
173
|
+
"index": idx,
|
|
174
|
+
"class_name": info["class_name"],
|
|
175
|
+
"unparsed": True,
|
|
176
|
+
"reason": "class_not_found_in_schema",
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
continue
|
|
180
|
+
if cls.fields:
|
|
181
|
+
first = cls.fields[0]
|
|
182
|
+
# 实例数据没有显式尺寸,读取前按首字段对齐来同步游标。
|
|
183
|
+
reader.seek(
|
|
184
|
+
align(reader.tell(), 4 if first.is_array else max(first.align, 1))
|
|
185
|
+
)
|
|
186
|
+
start_pos = reader.tell()
|
|
187
|
+
try:
|
|
188
|
+
parsed_instances.append(
|
|
189
|
+
{"index": idx, "data": self._parse_instance(reader, class_hash)}
|
|
190
|
+
)
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
# 某个实例解析失败时,尽量按估算最小尺寸跳过,继续解析后续实例。
|
|
193
|
+
parsed_instances.append(
|
|
194
|
+
{
|
|
195
|
+
"index": idx,
|
|
196
|
+
"class_name": info["class_name"],
|
|
197
|
+
"unparsed": True,
|
|
198
|
+
"reason": str(exc),
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
min_skip = self._estimate_min_instance_size(cls)
|
|
202
|
+
next_pos = min(reader.size, start_pos + min_skip)
|
|
203
|
+
if next_pos <= start_pos:
|
|
204
|
+
break
|
|
205
|
+
reader.seek(next_pos)
|
|
206
|
+
|
|
207
|
+
idx_map = {
|
|
208
|
+
inst["index"]: inst
|
|
209
|
+
for inst in parsed_instances
|
|
210
|
+
if isinstance(inst.get("index"), int)
|
|
211
|
+
}
|
|
212
|
+
object_roots = sorted(
|
|
213
|
+
set(i for i in object_table if isinstance(i, int) and i >= 0)
|
|
214
|
+
)
|
|
215
|
+
if not object_roots:
|
|
216
|
+
# 某些文件对象表为空,需要从引用关系推断根节点。
|
|
217
|
+
object_roots = self._infer_roots_when_object_table_empty(
|
|
218
|
+
idx_map, parsed_instances
|
|
219
|
+
)
|
|
220
|
+
if not object_roots and rsz_userdata_instance_ids:
|
|
221
|
+
# 如果只存在用户数据引用,也可把这些引用作为根节点导出。
|
|
222
|
+
object_roots = sorted(
|
|
223
|
+
set(
|
|
224
|
+
i
|
|
225
|
+
for i in rsz_userdata_instance_ids
|
|
226
|
+
if i in instance_info_map and i > 0
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
if not object_roots:
|
|
230
|
+
# 最后兜底:导出所有非空实例,保证信息尽可能不丢失。
|
|
231
|
+
object_roots = sorted(i for i in instance_info_map.keys() if i > 0)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"user3_path": user3_path,
|
|
235
|
+
"usr_header": usr_header,
|
|
236
|
+
"rsz_header": rsz_header,
|
|
237
|
+
"object_roots": object_roots,
|
|
238
|
+
"instance_infos": instance_infos,
|
|
239
|
+
"instance_info_map": instance_info_map,
|
|
240
|
+
"parsed_instances": parsed_instances,
|
|
241
|
+
"idx_map": idx_map,
|
|
242
|
+
"header_userdata_infos": header_userdata_infos,
|
|
243
|
+
"rsz_userdata_instance_ids": rsz_userdata_instance_ids,
|
|
244
|
+
"rsz_userdata_path_by_instance": rsz_userdata_path_by_instance,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
def _parse_user3(self, user3_path: Path) -> list[dict[str, Any]]:
|
|
248
|
+
"""解析完整 `.user.3` 文件并构造成紧凑对象树。
|
|
249
|
+
|
|
250
|
+
参数:
|
|
251
|
+
user3_path (Path): 源 ``.user.3`` 文件路径。
|
|
252
|
+
|
|
253
|
+
返回:
|
|
254
|
+
list[dict[str, Any]]: 以类名包裹的紧凑对象树列表;只含头部用户数据
|
|
255
|
+
信息的文件则返回可读的引用列表。
|
|
256
|
+
"""
|
|
257
|
+
document = self._parse_user3_document(user3_path)
|
|
258
|
+
parsed_instances = document["parsed_instances"]
|
|
259
|
+
object_roots = document["object_roots"]
|
|
260
|
+
instance_info_map = document["instance_info_map"]
|
|
261
|
+
idx_map = document["idx_map"]
|
|
262
|
+
header_userdata_infos = document["header_userdata_infos"]
|
|
263
|
+
# depth 为 "auto" 时根据复杂度自动决定,否则使用用户指定的固定深度。
|
|
264
|
+
depth = (
|
|
265
|
+
self._auto_pick_tree_depth(parsed_instances, object_roots)
|
|
266
|
+
if self.tree_depth == "auto"
|
|
267
|
+
else self.tree_depth
|
|
268
|
+
)
|
|
269
|
+
object_trees = [
|
|
270
|
+
# 从根实例开始展开引用,生成更适合人工修改的嵌套 JSON。
|
|
271
|
+
self._build_compact_tree(
|
|
272
|
+
root_idx,
|
|
273
|
+
idx_map,
|
|
274
|
+
depth=depth,
|
|
275
|
+
instance_info_map=instance_info_map,
|
|
276
|
+
)
|
|
277
|
+
for root_idx in object_roots
|
|
278
|
+
if root_idx in instance_info_map
|
|
279
|
+
]
|
|
280
|
+
if not object_trees and header_userdata_infos:
|
|
281
|
+
# 对只包含头部用户数据信息的文件,仍返回可读的引用列表。
|
|
282
|
+
return [
|
|
283
|
+
{
|
|
284
|
+
item["class_name"]: {
|
|
285
|
+
"ref_instance_id": item["index"],
|
|
286
|
+
"path": item["path"],
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for item in header_userdata_infos
|
|
290
|
+
]
|
|
291
|
+
return object_trees
|
|
292
|
+
|
|
293
|
+
def _parse_user3_pack(self, user3_path: Path) -> dict[str, Any]:
|
|
294
|
+
"""解析 `.user.3` 并返回适合稳定封包的完整实例表 JSON。
|
|
295
|
+
|
|
296
|
+
参数:
|
|
297
|
+
user3_path (Path): 源 ``.user.3`` 文件路径。
|
|
298
|
+
|
|
299
|
+
返回:
|
|
300
|
+
dict[str, Any]: 完整实例表封包文档(见 :meth:`_build_pack_json`)。
|
|
301
|
+
"""
|
|
302
|
+
return self._build_pack_json(self._parse_user3_document(user3_path))
|
|
303
|
+
|
|
304
|
+
def _build_pack_json(self, document: dict[str, Any]) -> dict[str, Any]:
|
|
305
|
+
"""把解析中间结果转换为完整实例表文档。
|
|
306
|
+
|
|
307
|
+
参数:
|
|
308
|
+
document (dict[str, Any]): :meth:`_parse_user3_document` 返回的解析文档。
|
|
309
|
+
|
|
310
|
+
返回:
|
|
311
|
+
dict[str, Any]: 封包格式文档,含 ``_format``、``_version``、``_source``、
|
|
312
|
+
``_roots``、``_instances``、``_userdata``、``_unsupported``、``_warnings``
|
|
313
|
+
等键;其中 ``_unsupported`` 记录当前写回器尚不支持的原始数据段。
|
|
314
|
+
"""
|
|
315
|
+
instances: dict[str, Any] = {}
|
|
316
|
+
warnings: list[str] = []
|
|
317
|
+
unsupported: list[str] = []
|
|
318
|
+
idx_map = document["idx_map"]
|
|
319
|
+
path_by_userdata = document["rsz_userdata_path_by_instance"]
|
|
320
|
+
usr_header = document["usr_header"]
|
|
321
|
+
rsz_header = document["rsz_header"]
|
|
322
|
+
|
|
323
|
+
# 记录当前最小写回器无法重建的原始数据段,封包阶段据此拒绝有损回封。
|
|
324
|
+
if int(usr_header.get("resource_count", 0)) > 0:
|
|
325
|
+
unsupported.append("USR resource table")
|
|
326
|
+
if int(usr_header.get("userdata_count", 0)) > 0:
|
|
327
|
+
unsupported.append("USR userdata table")
|
|
328
|
+
if int(usr_header.get("info_count", 0)) > 0:
|
|
329
|
+
unsupported.append("USR info table")
|
|
330
|
+
if int(rsz_header.get("userdata_count", 0)) > 0:
|
|
331
|
+
unsupported.append("RSZ userdata table")
|
|
332
|
+
|
|
333
|
+
for info in document["instance_infos"]:
|
|
334
|
+
idx = int(info["index"])
|
|
335
|
+
class_hash = int(info["hash"])
|
|
336
|
+
crc = int(info["crc"])
|
|
337
|
+
entry: dict[str, Any] = {
|
|
338
|
+
"_class": info.get("class_name"),
|
|
339
|
+
"_hash": self._format_hex_u32(class_hash),
|
|
340
|
+
"_crc": self._format_hex_u32(crc),
|
|
341
|
+
}
|
|
342
|
+
inst = idx_map.get(idx)
|
|
343
|
+
if idx == 0:
|
|
344
|
+
# 实例 0 是固定空槽,标记为 null 即可。
|
|
345
|
+
entry["_class"] = None
|
|
346
|
+
entry["_kind"] = "null"
|
|
347
|
+
elif inst is None:
|
|
348
|
+
entry["_unparsed"] = True
|
|
349
|
+
entry["reason"] = "missing parsed instance"
|
|
350
|
+
warnings.append(f"instance {idx} is missing from parsed data")
|
|
351
|
+
elif inst.get("is_userdata_reference"):
|
|
352
|
+
entry["_kind"] = "userdata_reference"
|
|
353
|
+
entry["path"] = path_by_userdata.get(idx, inst.get("path", ""))
|
|
354
|
+
warnings.append(
|
|
355
|
+
f"instance {idx} is an external userdata reference and "
|
|
356
|
+
"cannot be rebuilt by the current minimal writer"
|
|
357
|
+
)
|
|
358
|
+
elif inst.get("unparsed"):
|
|
359
|
+
entry["_unparsed"] = True
|
|
360
|
+
entry["reason"] = inst.get("reason", "unparsed")
|
|
361
|
+
warnings.append(
|
|
362
|
+
f"instance {idx} ({entry.get('_class')}) is unparsed: "
|
|
363
|
+
f"{entry['reason']}"
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
data = inst.get("data", {})
|
|
367
|
+
class_name = data.get("_class") or entry.get("_class")
|
|
368
|
+
entry["_class"] = class_name
|
|
369
|
+
fields = data.get("fields", {})
|
|
370
|
+
if not isinstance(fields, dict):
|
|
371
|
+
fields = {}
|
|
372
|
+
# 字段同样经过枚举后处理,使封包 JSON 中的枚举也呈现可读标签。
|
|
373
|
+
entry["fields"] = self._postprocess_enum_nodes(
|
|
374
|
+
fields,
|
|
375
|
+
current_class=class_name if isinstance(class_name, str) else None,
|
|
376
|
+
)
|
|
377
|
+
instances[str(idx)] = entry
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
"_format": PACK_JSON_FORMAT,
|
|
381
|
+
"_version": 1,
|
|
382
|
+
"_source": {
|
|
383
|
+
"file": str(document["user3_path"]),
|
|
384
|
+
"user_magic": self._format_hex_u32(self.user_magic),
|
|
385
|
+
"rsz_magic": self._format_hex_u32(self.rsz_magic),
|
|
386
|
+
"schema": str(self.schema_path),
|
|
387
|
+
"resource_count": int(usr_header.get("resource_count", 0)),
|
|
388
|
+
"userdata_count": int(usr_header.get("userdata_count", 0)),
|
|
389
|
+
"info_count": int(usr_header.get("info_count", 0)),
|
|
390
|
+
"rsz_userdata_count": int(rsz_header.get("userdata_count", 0)),
|
|
391
|
+
},
|
|
392
|
+
"_roots": document["object_roots"],
|
|
393
|
+
"_instances": instances,
|
|
394
|
+
"_userdata": [
|
|
395
|
+
{
|
|
396
|
+
"instance_id": int(instance_id),
|
|
397
|
+
"path": path_by_userdata.get(instance_id, ""),
|
|
398
|
+
}
|
|
399
|
+
for instance_id in document["rsz_userdata_instance_ids"]
|
|
400
|
+
],
|
|
401
|
+
"_unsupported": unsupported,
|
|
402
|
+
"_warnings": warnings,
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _format_hex_u32(value: int) -> str:
|
|
407
|
+
"""把 32 位整数格式化为稳定的十六进制字符串。
|
|
408
|
+
|
|
409
|
+
参数:
|
|
410
|
+
value (int): 待格式化的整数(按无符号 32 位截断)。
|
|
411
|
+
|
|
412
|
+
返回:
|
|
413
|
+
str: 形如 ``0x0012abcd`` 的 8 位小写十六进制字符串。
|
|
414
|
+
"""
|
|
415
|
+
return f"0x{value & 0xFFFFFFFF:08x}"
|