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/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""RE User3 JSON 工具包的公开导出入口。
|
|
2
|
+
|
|
3
|
+
较重的转换模块会依赖 Rich 等命令行显示库。这里使用惰性导出,
|
|
4
|
+
让 `pyreuser3-web --help` 和 `python -m pyreuser3.web --help` 这类轻量入口
|
|
5
|
+
不需要提前加载完整导出器或封包器。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib import import_module
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"BinaryReader",
|
|
15
|
+
"ClassDef",
|
|
16
|
+
"FieldDef",
|
|
17
|
+
"PackError",
|
|
18
|
+
"ParseError",
|
|
19
|
+
"TypeDB",
|
|
20
|
+
"REUser3Converter",
|
|
21
|
+
"RSZ_MAGIC",
|
|
22
|
+
"User3Exporter",
|
|
23
|
+
"User3Packer",
|
|
24
|
+
"USR_MAGIC",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
_EXPORT_MODULES = {
|
|
28
|
+
"BinaryReader": ".core",
|
|
29
|
+
"ParseError": ".core",
|
|
30
|
+
"RSZ_MAGIC": ".core",
|
|
31
|
+
"USR_MAGIC": ".core",
|
|
32
|
+
"ClassDef": ".schema",
|
|
33
|
+
"FieldDef": ".schema",
|
|
34
|
+
"TypeDB": ".schema",
|
|
35
|
+
"PackError": ".pack",
|
|
36
|
+
"User3Packer": ".pack",
|
|
37
|
+
"User3Exporter": ".export",
|
|
38
|
+
"REUser3Converter": ".api",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def __getattr__(name: str) -> Any:
|
|
43
|
+
"""首次访问公开名称时再惰性导入对应模块。
|
|
44
|
+
|
|
45
|
+
PEP 562 的模块级 ``__getattr__`` 钩子:仅当访问 ``__all__`` 中声明的名称
|
|
46
|
+
且该名称尚未缓存到模块全局时才会触发,从而实现按需导入的惰性导出。
|
|
47
|
+
|
|
48
|
+
参数:
|
|
49
|
+
name (str): 正在访问的属性名(通常是 ``__all__`` 中的某个公开名称)。
|
|
50
|
+
|
|
51
|
+
返回:
|
|
52
|
+
Any: 解析并缓存后的目标对象(类、函数或常量)。
|
|
53
|
+
|
|
54
|
+
异常:
|
|
55
|
+
AttributeError: 当 ``name`` 不在惰性导出表 ``_EXPORT_MODULES`` 中时抛出。
|
|
56
|
+
"""
|
|
57
|
+
module_name = _EXPORT_MODULES.get(name)
|
|
58
|
+
if module_name is None:
|
|
59
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
60
|
+
|
|
61
|
+
# 导入后把结果缓存到 globals,后续访问不再触发 __getattr__。
|
|
62
|
+
module = import_module(module_name, __name__)
|
|
63
|
+
value = getattr(module, name)
|
|
64
|
+
globals()[name] = value
|
|
65
|
+
return value
|
pyreuser3/__main__.py
ADDED
pyreuser3/api.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
"""对外使用的高层 API。
|
|
2
|
+
|
|
3
|
+
`REUser3Converter` 是推荐给其他项目调用的门面类。它把底层的
|
|
4
|
+
`User3Exporter` 和 `User3Packer` 包装成更稳定的工作流:
|
|
5
|
+
解析、导出、封包,以及“解析后交给 callback 修改再自动封包”。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
import json
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Callable, Optional
|
|
15
|
+
|
|
16
|
+
from .core import RSZ_MAGIC, USR_MAGIC
|
|
17
|
+
from .export import User3Exporter
|
|
18
|
+
from .pack import User3Packer
|
|
19
|
+
|
|
20
|
+
# 导出/封包过程中流转的 JSON 树,结构随文件而异,故用 Any 表示。
|
|
21
|
+
JsonTree = Any
|
|
22
|
+
# 用户提供的修补回调:接收 (data) 或 (data, source_path),可返回新树或就地修改后返回 None。
|
|
23
|
+
PatchCallback = Callable[..., Optional[JsonTree]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class REUser3Converter:
|
|
27
|
+
"""RE Engine `.user.3` 与 JSON 互转的可复用门面类。
|
|
28
|
+
|
|
29
|
+
封装解析、导出、封包以及“解析→回调修改→封包”的完整工作流,并统一处理
|
|
30
|
+
模板路径、il2cpp dump、magic 等配置,让外部调用方无需直接接触底层
|
|
31
|
+
导出器/封包器。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
schema_path: str | Path | None = None,
|
|
37
|
+
il2cpp_dump_path: str | Path | None = None,
|
|
38
|
+
tree_depth: int | str = "auto",
|
|
39
|
+
schema_dir: str | Path | None = None,
|
|
40
|
+
user_magic: int = USR_MAGIC,
|
|
41
|
+
rsz_magic: int = RSZ_MAGIC,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""初始化转换器。
|
|
44
|
+
|
|
45
|
+
参数:
|
|
46
|
+
schema_path (str | Path | None): 必填,RE_RSZ 模板 JSON 文件路径。
|
|
47
|
+
il2cpp_dump_path (str | Path | None): ``il2cpp_dump.json`` 路径;解析和导出时必填,
|
|
48
|
+
封包时可选但建议传入,用于枚举名反查。
|
|
49
|
+
tree_depth (int | str): 对象引用树展开深度,支持非负整数或 ``"auto"``。
|
|
50
|
+
schema_dir (str | Path | None): 旧参数名兼容入口,实际含义仍是模板 JSON 文件。
|
|
51
|
+
user_magic (int): ``.user.3`` 文件头 magic,默认沿用当前项目值。
|
|
52
|
+
rsz_magic (int): RSZ 数据块 magic,默认沿用当前项目值。
|
|
53
|
+
|
|
54
|
+
返回:
|
|
55
|
+
None: 构造函数,仅初始化实例属性。
|
|
56
|
+
|
|
57
|
+
异常:
|
|
58
|
+
TypeError: 当 ``schema_path`` 和 ``schema_dir`` 都缺失时抛出。
|
|
59
|
+
"""
|
|
60
|
+
# 兼容旧调用方的 `schema_dir=` 写法,但内部统一使用 schema_path。
|
|
61
|
+
if schema_path is None:
|
|
62
|
+
schema_path = schema_dir
|
|
63
|
+
if schema_path is None:
|
|
64
|
+
raise TypeError("schema_path is required")
|
|
65
|
+
self.schema_path = Path(schema_path)
|
|
66
|
+
self.il2cpp_dump_path = Path(il2cpp_dump_path) if il2cpp_dump_path else None
|
|
67
|
+
self.tree_depth = tree_depth
|
|
68
|
+
self.user_magic = int(user_magic)
|
|
69
|
+
self.rsz_magic = int(rsz_magic)
|
|
70
|
+
|
|
71
|
+
def export_directory(
|
|
72
|
+
self,
|
|
73
|
+
user3_root: str | Path,
|
|
74
|
+
output_root: str | Path,
|
|
75
|
+
exclude_regexes: list[str] | None = None,
|
|
76
|
+
) -> dict[str, int]:
|
|
77
|
+
"""批量导出目录或单文件下的 `.user.3`。
|
|
78
|
+
|
|
79
|
+
参数:
|
|
80
|
+
user3_root (str | Path): ``.user.3`` 根目录或单个 ``.user.3`` 文件。
|
|
81
|
+
output_root (str | Path): JSON 输出根目录。
|
|
82
|
+
exclude_regexes (list[str] | None): 用于排除相对路径的正则表达式列表。
|
|
83
|
+
|
|
84
|
+
返回:
|
|
85
|
+
dict[str, int]: 统计字典,含 ``total``、``success``、``failed`` 三个计数。
|
|
86
|
+
"""
|
|
87
|
+
exporter = self._new_exporter(user3_root, output_root, exclude_regexes)
|
|
88
|
+
return exporter.run()
|
|
89
|
+
|
|
90
|
+
def export_file(
|
|
91
|
+
self,
|
|
92
|
+
user3_path: str | Path,
|
|
93
|
+
json_path: str | Path,
|
|
94
|
+
) -> Path:
|
|
95
|
+
"""导出单个 `.user.3` 文件到指定 JSON 路径。
|
|
96
|
+
|
|
97
|
+
参数:
|
|
98
|
+
user3_path (str | Path): 源 ``.user.3`` 文件。
|
|
99
|
+
json_path (str | Path): 目标 JSON 文件。
|
|
100
|
+
|
|
101
|
+
返回:
|
|
102
|
+
Path: 实际写入的 JSON 文件路径。
|
|
103
|
+
"""
|
|
104
|
+
# 单文件导出复用 parse_file,确保 API 直接解析和批量导出的
|
|
105
|
+
# JSON 形状一致,减少后续封包时的分支差异。
|
|
106
|
+
tree = self.parse_file(user3_path, round_floats=True)
|
|
107
|
+
target = Path(json_path)
|
|
108
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
with target.open("w", encoding="utf-8") as f:
|
|
111
|
+
json.dump(tree, f, ensure_ascii=False, indent=2)
|
|
112
|
+
return target
|
|
113
|
+
|
|
114
|
+
def parse_file(self, user3_path: str | Path, round_floats: bool = True) -> JsonTree:
|
|
115
|
+
"""把单个 `.user.3` 解析成导出器使用的紧凑 JSON 树。
|
|
116
|
+
|
|
117
|
+
参数:
|
|
118
|
+
user3_path (str | Path): 源 ``.user.3`` 文件。
|
|
119
|
+
round_floats (bool): 是否把浮点数四舍五入到 4 位,方便人工阅读。
|
|
120
|
+
|
|
121
|
+
返回:
|
|
122
|
+
JsonTree: 可直接修改或传给 :meth:`pack` 的紧凑 JSON 树。
|
|
123
|
+
"""
|
|
124
|
+
exporter = self._new_exporter(user3_path, Path.cwd(), [])
|
|
125
|
+
# 单文件解析不写 Enums_Internal.json,但仍需要从 il2cpp_dump.json
|
|
126
|
+
# 构建枚举表和枚举上下文,否则固定枚举值无法转换成可读标签。
|
|
127
|
+
self._prepare_exporter_metadata(exporter)
|
|
128
|
+
tree = exporter._parse_user3(Path(user3_path))
|
|
129
|
+
tree = exporter._postprocess_enum_nodes(tree)
|
|
130
|
+
tree = exporter._finalize_export_tree(tree)
|
|
131
|
+
if round_floats:
|
|
132
|
+
return exporter._round_export_floats(tree)
|
|
133
|
+
return tree
|
|
134
|
+
|
|
135
|
+
def parse_pack_file(self, user3_path: str | Path) -> JsonTree:
|
|
136
|
+
"""把单个 `.user.3` 解析成适合修改后稳定封包的实例表 JSON。
|
|
137
|
+
|
|
138
|
+
普通导出和 :meth:`parse_file` 仍然返回 readable JSON;只有需要封回
|
|
139
|
+
``.user.3`` 的流程才应使用这个完整实例表结构。
|
|
140
|
+
|
|
141
|
+
参数:
|
|
142
|
+
user3_path (str | Path): 源 ``.user.3`` 文件。
|
|
143
|
+
|
|
144
|
+
返回:
|
|
145
|
+
JsonTree: 含完整实例表(``_instances`` 等键)的封包格式 JSON。
|
|
146
|
+
"""
|
|
147
|
+
exporter = self._new_exporter(user3_path, Path.cwd(), [])
|
|
148
|
+
self._prepare_exporter_metadata(exporter)
|
|
149
|
+
return exporter._parse_user3_pack(Path(user3_path))
|
|
150
|
+
|
|
151
|
+
def pack_directory(
|
|
152
|
+
self,
|
|
153
|
+
json_root: str | Path,
|
|
154
|
+
output_root: str | Path,
|
|
155
|
+
exclude_regexes: list[str] | None = None,
|
|
156
|
+
) -> dict[str, int]:
|
|
157
|
+
"""批量将 JSON 文件封回 `.user.3`。
|
|
158
|
+
|
|
159
|
+
参数:
|
|
160
|
+
json_root (str | Path): JSON 文件或 JSON 根目录。
|
|
161
|
+
output_root (str | Path): ``.user.3`` 输出根目录。
|
|
162
|
+
exclude_regexes (list[str] | None): 用于排除 JSON 相对路径的正则表达式列表。
|
|
163
|
+
|
|
164
|
+
返回:
|
|
165
|
+
dict[str, int]: 统计字典,含 ``total``、``success``、``failed`` 三个计数。
|
|
166
|
+
"""
|
|
167
|
+
packer = self._new_packer(output_root)
|
|
168
|
+
return packer.pack_directory(json_root, output_root, exclude_regexes)
|
|
169
|
+
|
|
170
|
+
def pack_file(self, json_path: str | Path, user3_path: str | Path) -> Path:
|
|
171
|
+
"""把单个 JSON 文件封包到指定 `.user.3` 路径。
|
|
172
|
+
|
|
173
|
+
参数:
|
|
174
|
+
json_path (str | Path): 源 JSON 文件。
|
|
175
|
+
user3_path (str | Path): 输出 ``.user.3`` 文件路径。
|
|
176
|
+
|
|
177
|
+
返回:
|
|
178
|
+
Path: 实际写入的 ``.user.3`` 文件路径。
|
|
179
|
+
"""
|
|
180
|
+
packer = self._new_packer(Path(user3_path).parent)
|
|
181
|
+
return packer.pack_json_file(json_path, user3_path)
|
|
182
|
+
|
|
183
|
+
def pack(self, data: Any) -> bytes:
|
|
184
|
+
"""把内存中的 JSON 树直接编码为 `.user.3` 二进制。
|
|
185
|
+
|
|
186
|
+
参数:
|
|
187
|
+
data (Any): readable JSON 对象/数组,或 :meth:`parse_pack_file` 返回的实例表 JSON。
|
|
188
|
+
|
|
189
|
+
返回:
|
|
190
|
+
bytes: 可直接写入文件的 ``.user.3`` 字节串。
|
|
191
|
+
"""
|
|
192
|
+
return self._new_packer(None).pack(data)
|
|
193
|
+
|
|
194
|
+
def patch_file(
|
|
195
|
+
self,
|
|
196
|
+
user3_path: str | Path,
|
|
197
|
+
output_path: str | Path,
|
|
198
|
+
callback: PatchCallback,
|
|
199
|
+
) -> Path:
|
|
200
|
+
"""解析、交给 callback 修改、封包并写出单个 `.user.3`。
|
|
201
|
+
|
|
202
|
+
callback 可以接收 ``(data)`` 或 ``(data, source_path)``。这里的 ``data``
|
|
203
|
+
是完整实例表 JSON。callback 既可以返回一个新的 JSON 树,也可以
|
|
204
|
+
原地修改 ``data`` 后返回 ``None``。
|
|
205
|
+
|
|
206
|
+
参数:
|
|
207
|
+
user3_path (str | Path): 源 ``.user.3`` 文件。
|
|
208
|
+
output_path (str | Path): 修改后 ``.user.3`` 的写入路径。
|
|
209
|
+
callback (PatchCallback): 用户提供的 JSON 修改函数。
|
|
210
|
+
|
|
211
|
+
返回:
|
|
212
|
+
Path: 实际写入的 ``.user.3`` 文件路径。
|
|
213
|
+
"""
|
|
214
|
+
source = Path(user3_path)
|
|
215
|
+
# 修改并封回时使用完整实例表格式,避免 readable 树里的旧引用编号
|
|
216
|
+
# 在重建实例表时变成悬空引用。
|
|
217
|
+
data = self.parse_pack_file(source)
|
|
218
|
+
modified = self._run_callback(callback, data, source)
|
|
219
|
+
if modified is None:
|
|
220
|
+
modified = data
|
|
221
|
+
target = Path(output_path)
|
|
222
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
target.write_bytes(self.pack(modified))
|
|
224
|
+
return target
|
|
225
|
+
|
|
226
|
+
def patch_directory(
|
|
227
|
+
self,
|
|
228
|
+
user3_root: str | Path,
|
|
229
|
+
output_root: str | Path,
|
|
230
|
+
callback: PatchCallback,
|
|
231
|
+
include_regexes: list[str] | None = None,
|
|
232
|
+
exclude_regexes: list[str] | None = None,
|
|
233
|
+
) -> dict[str, int]:
|
|
234
|
+
"""批量查找匹配的 `.user.3`,用 callback 修改后自动封包。
|
|
235
|
+
|
|
236
|
+
``include_regexes`` 和 ``exclude_regexes`` 都匹配相对 ``user3_root`` 的
|
|
237
|
+
路径,并统一使用 ``/`` 作为路径分隔符,避免 Windows 与类 Unix
|
|
238
|
+
路径差异影响正则。
|
|
239
|
+
|
|
240
|
+
参数:
|
|
241
|
+
user3_root (str | Path): ``.user.3`` 根目录或单个文件。
|
|
242
|
+
output_root (str | Path): 修改后文件的输出根目录。
|
|
243
|
+
callback (PatchCallback): 用户提供的 JSON 修改函数。
|
|
244
|
+
include_regexes (list[str] | None): 只处理匹配这些正则的相对路径。
|
|
245
|
+
exclude_regexes (list[str] | None): 跳过匹配这些正则的相对路径。
|
|
246
|
+
|
|
247
|
+
返回:
|
|
248
|
+
dict[str, int]: 统计字典,含 ``total``、``success``、``failed``、``skipped`` 四个计数。
|
|
249
|
+
"""
|
|
250
|
+
source_root = Path(user3_root)
|
|
251
|
+
target_root = Path(output_root)
|
|
252
|
+
files = self._discover_user3_files(source_root)
|
|
253
|
+
include_patterns = [re.compile(p) for p in (include_regexes or [])]
|
|
254
|
+
exclude_patterns = [re.compile(p) for p in (exclude_regexes or [])]
|
|
255
|
+
|
|
256
|
+
total = success = failed = skipped = 0
|
|
257
|
+
for file_path in files:
|
|
258
|
+
# 单文件模式使用文件名;目录模式使用相对路径,以便输出时还原目录。
|
|
259
|
+
rel = (
|
|
260
|
+
file_path.name
|
|
261
|
+
if source_root.is_file()
|
|
262
|
+
else file_path.relative_to(source_root).as_posix()
|
|
263
|
+
)
|
|
264
|
+
if include_patterns and not any(
|
|
265
|
+
pattern.search(rel) for pattern in include_patterns
|
|
266
|
+
):
|
|
267
|
+
skipped += 1
|
|
268
|
+
continue
|
|
269
|
+
if any(pattern.search(rel) for pattern in exclude_patterns):
|
|
270
|
+
skipped += 1
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
total += 1
|
|
274
|
+
output_path = target_root / (
|
|
275
|
+
file_path.name
|
|
276
|
+
if source_root.is_file()
|
|
277
|
+
else file_path.relative_to(source_root)
|
|
278
|
+
)
|
|
279
|
+
try:
|
|
280
|
+
# 每个文件独立处理,单个文件失败不会影响整个目录批处理。
|
|
281
|
+
self.patch_file(file_path, output_path, callback)
|
|
282
|
+
success += 1
|
|
283
|
+
except Exception:
|
|
284
|
+
failed += 1
|
|
285
|
+
return {
|
|
286
|
+
"total": total,
|
|
287
|
+
"success": success,
|
|
288
|
+
"failed": failed,
|
|
289
|
+
"skipped": skipped,
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
def _new_exporter(
|
|
293
|
+
self,
|
|
294
|
+
user3_root: str | Path,
|
|
295
|
+
output_root: str | Path,
|
|
296
|
+
exclude_regexes: list[str] | None,
|
|
297
|
+
) -> User3Exporter:
|
|
298
|
+
"""按当前配置创建导出器实例。
|
|
299
|
+
|
|
300
|
+
参数:
|
|
301
|
+
user3_root (str | Path): ``.user.3`` 输入根目录或单个文件。
|
|
302
|
+
output_root (str | Path): JSON 输出根目录。
|
|
303
|
+
exclude_regexes (list[str] | None): 排除相对路径的正则表达式列表。
|
|
304
|
+
|
|
305
|
+
返回:
|
|
306
|
+
User3Exporter: 已用当前模板、dump、magic 等配置初始化的导出器。
|
|
307
|
+
|
|
308
|
+
异常:
|
|
309
|
+
FileNotFoundError: 当导出所需的 ``il2cpp_dump_path`` 未配置时抛出。
|
|
310
|
+
"""
|
|
311
|
+
if self.il2cpp_dump_path is None:
|
|
312
|
+
raise FileNotFoundError("il2cpp_dump_path is required for exporting JSON")
|
|
313
|
+
return User3Exporter(
|
|
314
|
+
user3_root=user3_root,
|
|
315
|
+
schema_dir=self.schema_path,
|
|
316
|
+
output_root=output_root,
|
|
317
|
+
tree_depth=self.tree_depth,
|
|
318
|
+
exclude_regexes=exclude_regexes or [],
|
|
319
|
+
il2cpp_dump_path=self.il2cpp_dump_path,
|
|
320
|
+
user_magic=self.user_magic,
|
|
321
|
+
rsz_magic=self.rsz_magic,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
def _new_packer(self, output_root: str | Path | None) -> User3Packer:
|
|
325
|
+
"""按当前配置创建封包器实例。
|
|
326
|
+
|
|
327
|
+
参数:
|
|
328
|
+
output_root (str | Path | None): 默认输出根目录,可为 ``None``。
|
|
329
|
+
|
|
330
|
+
返回:
|
|
331
|
+
User3Packer: 已用当前模板、dump、magic 等配置初始化的封包器。
|
|
332
|
+
"""
|
|
333
|
+
return User3Packer(
|
|
334
|
+
schema_dir=self.schema_path,
|
|
335
|
+
il2cpp_dump_path=self.il2cpp_dump_path,
|
|
336
|
+
output_root=output_root,
|
|
337
|
+
user_magic=self.user_magic,
|
|
338
|
+
rsz_magic=self.rsz_magic,
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _prepare_exporter_metadata(self, exporter: User3Exporter) -> None:
|
|
342
|
+
"""为单文件解析准备枚举表和枚举上下文。
|
|
343
|
+
|
|
344
|
+
参数:
|
|
345
|
+
exporter (User3Exporter): 待填充枚举索引的导出器实例。
|
|
346
|
+
|
|
347
|
+
返回:
|
|
348
|
+
None: 直接在 ``exporter`` 上设置枚举查找表与上下文。
|
|
349
|
+
|
|
350
|
+
异常:
|
|
351
|
+
FileNotFoundError: 当 ``il2cpp_dump_path`` 缺失或不是文件时抛出。
|
|
352
|
+
"""
|
|
353
|
+
if self.il2cpp_dump_path is None or not self.il2cpp_dump_path.is_file():
|
|
354
|
+
raise FileNotFoundError("il2cpp_dump_path is required for parsing JSON")
|
|
355
|
+
with self.il2cpp_dump_path.open("r", encoding="utf-8") as f:
|
|
356
|
+
il2cpp_dump = json.load(f)
|
|
357
|
+
# 导出器批处理时会写出 Enums_Internal.json;这里是内存解析,
|
|
358
|
+
# 因此只构建运行时索引,不产生额外文件。
|
|
359
|
+
enums_internal = exporter.export_enums_internal(il2cpp_dump)
|
|
360
|
+
exporter.enum_lookup = exporter._build_enum_lookup_from_enums_internal(
|
|
361
|
+
enums_internal
|
|
362
|
+
)
|
|
363
|
+
enum_context = exporter.export_enum_context_internal(il2cpp_dump)
|
|
364
|
+
exporter._apply_enum_context(enum_context)
|
|
365
|
+
exporter._ensure_enum_lookup()
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def _discover_user3_files(user3_root: Path) -> list[Path]:
|
|
369
|
+
"""发现单文件或目录下的 `.user.3` 文件。
|
|
370
|
+
|
|
371
|
+
参数:
|
|
372
|
+
user3_root (Path): ``.user.3`` 根目录或单个文件路径。
|
|
373
|
+
|
|
374
|
+
返回:
|
|
375
|
+
list[Path]: 排序后的 ``.user.3`` 文件路径列表(单文件时只含一项)。
|
|
376
|
+
|
|
377
|
+
异常:
|
|
378
|
+
FileNotFoundError: 当路径不存在或目录下没有 ``.user.3`` 时抛出。
|
|
379
|
+
"""
|
|
380
|
+
if user3_root.is_file():
|
|
381
|
+
return [user3_root]
|
|
382
|
+
if not user3_root.is_dir():
|
|
383
|
+
raise FileNotFoundError(f"user3 root not found: {user3_root}")
|
|
384
|
+
files = sorted(user3_root.rglob("*.user.3"))
|
|
385
|
+
if not files:
|
|
386
|
+
raise FileNotFoundError(f"no *.user.3 found under: {user3_root}")
|
|
387
|
+
return files
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def _run_callback(
|
|
391
|
+
callback: PatchCallback, data: JsonTree, source_path: Path
|
|
392
|
+
) -> JsonTree | None:
|
|
393
|
+
"""根据 callback 参数个数自动决定是否传入源路径。
|
|
394
|
+
|
|
395
|
+
参数:
|
|
396
|
+
callback (PatchCallback): 用户提供的修改函数。
|
|
397
|
+
data (JsonTree): 待修改的完整实例表 JSON。
|
|
398
|
+
source_path (Path): 源 ``.user.3`` 文件路径。
|
|
399
|
+
|
|
400
|
+
返回:
|
|
401
|
+
JsonTree | None: callback 返回的新树,或 ``None``(表示就地修改)。
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
param_count = len(inspect.signature(callback).parameters)
|
|
405
|
+
except (TypeError, ValueError):
|
|
406
|
+
# 某些可调用对象可能无法通过 inspect 取得签名,默认按完整参数调用。
|
|
407
|
+
param_count = 2
|
|
408
|
+
if param_count <= 1:
|
|
409
|
+
return callback(data)
|
|
410
|
+
return callback(data, source_path)
|
pyreuser3/cli.py
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Command line interface for the PyREUser3 package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
8
|
+
from typing import Sequence
|
|
9
|
+
|
|
10
|
+
from .core import RSZ_MAGIC, USR_MAGIC
|
|
11
|
+
from .export import User3Exporter
|
|
12
|
+
from .pack import User3Packer
|
|
13
|
+
from .rich_ui import get_console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def parse_int_arg(value: str) -> int:
|
|
17
|
+
"""Parse decimal or 0x-prefixed integer command line values."""
|
|
18
|
+
return int(value, 0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def package_version() -> str:
|
|
22
|
+
"""Return installed package version, with a source-tree fallback."""
|
|
23
|
+
try:
|
|
24
|
+
return version("PyREUser3")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
return "0.1.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def normalize_tree_depth(value: str) -> int | str:
|
|
30
|
+
"""Normalize tree depth argument to an integer or the string 'auto'."""
|
|
31
|
+
text = value.strip().lower()
|
|
32
|
+
if text == "auto":
|
|
33
|
+
return "auto"
|
|
34
|
+
depth = int(text)
|
|
35
|
+
if depth < 0:
|
|
36
|
+
raise argparse.ArgumentTypeError("tree depth must be non-negative or 'auto'")
|
|
37
|
+
return depth
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def add_magic_args(parser: argparse.ArgumentParser) -> None:
|
|
41
|
+
"""Add common .user.3 and RSZ magic options to a subparser."""
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--user-magic",
|
|
44
|
+
type=parse_int_arg,
|
|
45
|
+
default=USR_MAGIC,
|
|
46
|
+
help=f"USR file magic as decimal or hex (default: 0x{USR_MAGIC:08x}).",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--rsz-magic",
|
|
50
|
+
type=parse_int_arg,
|
|
51
|
+
default=RSZ_MAGIC,
|
|
52
|
+
help=f"RSZ block magic as decimal or hex (default: 0x{RSZ_MAGIC:08x}).",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
57
|
+
"""Build the top-level CLI parser."""
|
|
58
|
+
parser = argparse.ArgumentParser(
|
|
59
|
+
prog="pyreuser3",
|
|
60
|
+
description="Convert RE Engine .user.3 files to JSON and pack them back.",
|
|
61
|
+
)
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--version",
|
|
64
|
+
action="version",
|
|
65
|
+
version=f"%(prog)s {package_version()}",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
69
|
+
|
|
70
|
+
export_parser = subparsers.add_parser(
|
|
71
|
+
"export",
|
|
72
|
+
help="Export .user.3 files to JSON.",
|
|
73
|
+
)
|
|
74
|
+
export_parser.add_argument(
|
|
75
|
+
"--input-dir",
|
|
76
|
+
"-i",
|
|
77
|
+
required=True,
|
|
78
|
+
help="Root directory or single .user.3 file to export.",
|
|
79
|
+
)
|
|
80
|
+
export_parser.add_argument(
|
|
81
|
+
"--schema-path",
|
|
82
|
+
"--schema-dir",
|
|
83
|
+
"-s",
|
|
84
|
+
dest="schema_path",
|
|
85
|
+
required=True,
|
|
86
|
+
help="Explicit RE_RSZ schema JSON file path.",
|
|
87
|
+
)
|
|
88
|
+
export_parser.add_argument(
|
|
89
|
+
"--output-dir",
|
|
90
|
+
"-o",
|
|
91
|
+
required=True,
|
|
92
|
+
help="Root output directory for exported JSON files.",
|
|
93
|
+
)
|
|
94
|
+
export_parser.add_argument(
|
|
95
|
+
"--tree-depth",
|
|
96
|
+
"-d",
|
|
97
|
+
type=normalize_tree_depth,
|
|
98
|
+
default="auto",
|
|
99
|
+
help="Tree depth as a non-negative integer or 'auto' (default: auto).",
|
|
100
|
+
)
|
|
101
|
+
export_parser.add_argument(
|
|
102
|
+
"--exclude-regex",
|
|
103
|
+
"-x",
|
|
104
|
+
action="append",
|
|
105
|
+
default=[],
|
|
106
|
+
help="Regex to exclude matching relative file paths. Can be used multiple times.",
|
|
107
|
+
)
|
|
108
|
+
export_parser.add_argument(
|
|
109
|
+
"--il2cpp-dump-path",
|
|
110
|
+
"-p",
|
|
111
|
+
required=True,
|
|
112
|
+
help="Path to il2cpp_dump.json, used to generate enum labels.",
|
|
113
|
+
)
|
|
114
|
+
add_magic_args(export_parser)
|
|
115
|
+
export_parser.set_defaults(func=run_export)
|
|
116
|
+
|
|
117
|
+
pack_parser = subparsers.add_parser(
|
|
118
|
+
"pack",
|
|
119
|
+
help="Pack .user.3 JSON files back to .user.3.",
|
|
120
|
+
)
|
|
121
|
+
pack_parser.add_argument(
|
|
122
|
+
"--input-json",
|
|
123
|
+
"-j",
|
|
124
|
+
required=True,
|
|
125
|
+
help="JSON file or root directory that contains .user.3.json files.",
|
|
126
|
+
)
|
|
127
|
+
pack_parser.add_argument(
|
|
128
|
+
"--schema-path",
|
|
129
|
+
"--schema-dir",
|
|
130
|
+
"-s",
|
|
131
|
+
dest="schema_path",
|
|
132
|
+
required=True,
|
|
133
|
+
help="Explicit RE_RSZ schema JSON file path.",
|
|
134
|
+
)
|
|
135
|
+
pack_parser.add_argument(
|
|
136
|
+
"--output-dir",
|
|
137
|
+
"-o",
|
|
138
|
+
required=True,
|
|
139
|
+
help="Root output directory for packed .user.3 files.",
|
|
140
|
+
)
|
|
141
|
+
pack_parser.add_argument(
|
|
142
|
+
"--il2cpp-dump-path",
|
|
143
|
+
"-p",
|
|
144
|
+
default="",
|
|
145
|
+
help="Optional il2cpp_dump.json path, used for enum name lookup.",
|
|
146
|
+
)
|
|
147
|
+
pack_parser.add_argument(
|
|
148
|
+
"--exclude-regex",
|
|
149
|
+
"-x",
|
|
150
|
+
action="append",
|
|
151
|
+
default=[],
|
|
152
|
+
help="Regex to exclude matching relative JSON paths. Can be used multiple times.",
|
|
153
|
+
)
|
|
154
|
+
add_magic_args(pack_parser)
|
|
155
|
+
pack_parser.set_defaults(func=run_pack)
|
|
156
|
+
|
|
157
|
+
return parser
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def run_export(args: argparse.Namespace) -> int:
|
|
161
|
+
"""Run the export subcommand."""
|
|
162
|
+
console = get_console()
|
|
163
|
+
console.log("Exporting .user.3 files to JSON...")
|
|
164
|
+
exporter = User3Exporter(
|
|
165
|
+
user3_root=args.input_dir,
|
|
166
|
+
schema_dir=args.schema_path,
|
|
167
|
+
output_root=args.output_dir,
|
|
168
|
+
tree_depth=args.tree_depth,
|
|
169
|
+
exclude_regexes=args.exclude_regex,
|
|
170
|
+
il2cpp_dump_path=args.il2cpp_dump_path,
|
|
171
|
+
user_magic=args.user_magic,
|
|
172
|
+
rsz_magic=args.rsz_magic,
|
|
173
|
+
)
|
|
174
|
+
result = exporter.run()
|
|
175
|
+
console.log("Export complete:", json.dumps(result, ensure_ascii=False))
|
|
176
|
+
return 1 if result.get("failed", 0) else 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_pack(args: argparse.Namespace) -> int:
|
|
180
|
+
"""Run the pack subcommand."""
|
|
181
|
+
console = get_console()
|
|
182
|
+
console.log("Packing JSON files to .user.3...")
|
|
183
|
+
packer = User3Packer(
|
|
184
|
+
schema_dir=args.schema_path,
|
|
185
|
+
il2cpp_dump_path=args.il2cpp_dump_path or None,
|
|
186
|
+
output_root=args.output_dir,
|
|
187
|
+
user_magic=args.user_magic,
|
|
188
|
+
rsz_magic=args.rsz_magic,
|
|
189
|
+
)
|
|
190
|
+
result = packer.pack_directory(
|
|
191
|
+
json_root=args.input_json,
|
|
192
|
+
output_root=args.output_dir,
|
|
193
|
+
exclude_regexes=args.exclude_regex,
|
|
194
|
+
)
|
|
195
|
+
console.log("Pack complete:", json.dumps(result, ensure_ascii=False))
|
|
196
|
+
return 1 if result.get("failed", 0) else 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
200
|
+
"""CLI entry point used by the pyreuser3 console script."""
|
|
201
|
+
parser = build_parser()
|
|
202
|
+
args = parser.parse_args(argv)
|
|
203
|
+
return int(args.func(args))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
if __name__ == "__main__":
|
|
207
|
+
raise SystemExit(main())
|