PyREUser3 0.1.0__tar.gz → 0.2.0__tar.gz
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-0.1.0 → pyreuser3-0.2.0}/PKG-INFO +1 -1
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/PKG-INFO +1 -1
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyproject.toml +1 -1
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/__init__.py +9 -16
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/__main__.py +2 -1
- pyreuser3-0.2.0/pyreuser3/api.py +411 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/cli.py +71 -12
- pyreuser3-0.2.0/pyreuser3/core.py +410 -0
- pyreuser3-0.2.0/pyreuser3/export/__init__.py +9 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/base.py +82 -74
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/enums.py +44 -31
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/fields.py +65 -44
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/metadata.py +107 -74
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/postprocess.py +127 -83
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/tree.py +102 -74
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/export/user3.py +89 -60
- pyreuser3-0.2.0/pyreuser3/pack/__init__.py +9 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/base.py +115 -82
- pyreuser3-0.2.0/pyreuser3/pack/models.py +154 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/plan.py +220 -158
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/pack/writer.py +104 -67
- pyreuser3-0.2.0/pyreuser3/rich_ui.py +127 -0
- pyreuser3-0.2.0/pyreuser3/schema.py +199 -0
- pyreuser3-0.2.0/pyreuser3/web/__init__.py +10 -0
- pyreuser3-0.2.0/pyreuser3/web/__main__.py +9 -0
- pyreuser3-0.2.0/pyreuser3/web/handler.py +208 -0
- pyreuser3-0.2.0/pyreuser3/web/jobs.py +271 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/pyreuser3/web/page.py +155 -31
- pyreuser3-0.2.0/pyreuser3/web/picker.py +94 -0
- pyreuser3-0.2.0/pyreuser3/web/runners.py +267 -0
- pyreuser3-0.2.0/pyreuser3/web/server.py +102 -0
- pyreuser3-0.2.0/pyreuser3/web/settings.py +45 -0
- pyreuser3-0.1.0/pyreuser3/api.py +0 -410
- pyreuser3-0.1.0/pyreuser3/core.py +0 -358
- pyreuser3-0.1.0/pyreuser3/export/__init__.py +0 -11
- pyreuser3-0.1.0/pyreuser3/pack/__init__.py +0 -10
- pyreuser3-0.1.0/pyreuser3/pack/models.py +0 -140
- pyreuser3-0.1.0/pyreuser3/rich_ui.py +0 -126
- pyreuser3-0.1.0/pyreuser3/schema.py +0 -193
- pyreuser3-0.1.0/pyreuser3/web/__init__.py +0 -6
- pyreuser3-0.1.0/pyreuser3/web/__main__.py +0 -6
- pyreuser3-0.1.0/pyreuser3/web/handler.py +0 -178
- pyreuser3-0.1.0/pyreuser3/web/jobs.py +0 -243
- pyreuser3-0.1.0/pyreuser3/web/picker.py +0 -92
- pyreuser3-0.1.0/pyreuser3/web/runners.py +0 -238
- pyreuser3-0.1.0/pyreuser3/web/server.py +0 -104
- pyreuser3-0.1.0/pyreuser3/web/settings.py +0 -42
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/LICENSE +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/MANIFEST.in +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/SOURCES.txt +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/README.md +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/docs/README.zh-CN.md +0 -0
- {pyreuser3-0.1.0 → pyreuser3-0.2.0}/setup.cfg +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Expose the stable public API while keeping expensive converter modules lazy.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
不需要提前加载完整导出器或封包器。
|
|
3
|
+
The package-level __getattr__ hook avoids importing Rich and binary conversion code for
|
|
4
|
+
lightweight operations such as version checks and Web UI help output.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -40,25 +39,19 @@ _EXPORT_MODULES = {
|
|
|
40
39
|
|
|
41
40
|
|
|
42
41
|
def __getattr__(name: str) -> Any:
|
|
43
|
-
"""
|
|
42
|
+
"""Resolve a lazily exported package attribute on first access.
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
Args:
|
|
45
|
+
name (str): Symbolic schema, class, field, or enum name being stored or looked up.
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
返回:
|
|
52
|
-
Any: 解析并缓存后的目标对象(类、函数或常量)。
|
|
53
|
-
|
|
54
|
-
异常:
|
|
55
|
-
AttributeError: 当 ``name`` 不在惰性导出表 ``_EXPORT_MODULES`` 中时抛出。
|
|
47
|
+
Returns:
|
|
48
|
+
Any: Normalized value ready for the next parse, export, post-processing, or pack step.
|
|
56
49
|
"""
|
|
57
50
|
module_name = _EXPORT_MODULES.get(name)
|
|
58
51
|
if module_name is None:
|
|
59
52
|
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
60
53
|
|
|
61
|
-
#
|
|
54
|
+
# Cache the resolved object in globals so later attribute access bypasses __getattr__ and avoids another import.
|
|
62
55
|
module = import_module(module_name, __name__)
|
|
63
56
|
value = getattr(module, name)
|
|
64
57
|
globals()[name] = value
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
"""Provide the REUser3Converter facade used by downstream Python code.
|
|
2
|
+
|
|
3
|
+
The facade hides exporter and packer construction details, keeps schema and il2cpp dump
|
|
4
|
+
configuration in one place, and offers convenient single-file, batch, and patch-and-
|
|
5
|
+
repack workflows.
|
|
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
|
+
# Preserve the exported JSON structure so external scripts and hand-edited files remain
|
|
21
|
+
# compatible across workflows.
|
|
22
|
+
JsonTree = Any
|
|
23
|
+
# Patch callbacks may accept only the parsed data or both the data and source
|
|
24
|
+
# path; returning None means the callback mutated in place.
|
|
25
|
+
PatchCallback = Callable[..., Optional[JsonTree]]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class REUser3Converter:
|
|
29
|
+
"""Store shared conversion configuration and expose high-level export, parse, patch, and
|
|
30
|
+
pack workflows.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
schema_path: str | Path | None = None,
|
|
36
|
+
il2cpp_dump_path: str | Path | None = None,
|
|
37
|
+
tree_depth: int | str = "auto",
|
|
38
|
+
schema_dir: str | Path | None = None,
|
|
39
|
+
user_magic: int = USR_MAGIC,
|
|
40
|
+
rsz_magic: int = RSZ_MAGIC,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize REUser3Converter with validated configuration and state.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
schema_path (str | Path | None): Explicit RE_RSZ schema JSON file path.
|
|
46
|
+
il2cpp_dump_path (str | Path | None): Path to il2cpp_dump.json for enum metadata.
|
|
47
|
+
tree_depth (int | str): Requested reference-tree expansion depth or auto mode.
|
|
48
|
+
schema_dir (str | Path | None): Compatibility schema argument that must resolve to a
|
|
49
|
+
schema JSON file.
|
|
50
|
+
user_magic (int): Expected magic value for the outer .user.3 container header.
|
|
51
|
+
rsz_magic (int): Expected magic value for embedded RSZ blocks.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
None. The method performs its documented side effect in place and raises on invalid input.
|
|
55
|
+
|
|
56
|
+
Raises:
|
|
57
|
+
TypeError: The caller supplied a value of an unsupported type.
|
|
58
|
+
"""
|
|
59
|
+
# Accept the legacy schema_dir alias from older callers, but normalize all internal state to schema_path.
|
|
60
|
+
if schema_path is None:
|
|
61
|
+
schema_path = schema_dir
|
|
62
|
+
if schema_path is None:
|
|
63
|
+
raise TypeError("schema_path is required")
|
|
64
|
+
self.schema_path = Path(schema_path)
|
|
65
|
+
self.il2cpp_dump_path = Path(il2cpp_dump_path) if il2cpp_dump_path else None
|
|
66
|
+
self.tree_depth = tree_depth
|
|
67
|
+
self.user_magic = int(user_magic)
|
|
68
|
+
self.rsz_magic = int(rsz_magic)
|
|
69
|
+
|
|
70
|
+
def export_directory(
|
|
71
|
+
self,
|
|
72
|
+
user3_root: str | Path,
|
|
73
|
+
output_root: str | Path,
|
|
74
|
+
exclude_regexes: list[str] | None = None,
|
|
75
|
+
) -> dict[str, int]:
|
|
76
|
+
"""Export every selected .user.3 file under a directory or single-file root.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
user3_root (str | Path): Source .user.3 file or directory root.
|
|
80
|
+
output_root (str | Path): Directory where generated output is written.
|
|
81
|
+
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
82
|
+
relative paths.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict[str, int]: Counters describing total, successful, and failed items.
|
|
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
|
+
"""Export one .user.3 file to the requested JSON path.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
99
|
+
json_path (str | Path): Path to the JSON document read from or written by this workflow.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
|
|
103
|
+
"""
|
|
104
|
+
# Reuse parse_file so single-file and batch exports keep the same parsed JSON shape and metadata handling.
|
|
105
|
+
# Preserve the exported JSON structure so external scripts and hand-edited files
|
|
106
|
+
# remain compatible across workflows.
|
|
107
|
+
tree = self.parse_file(user3_path, round_floats=True)
|
|
108
|
+
target = Path(json_path)
|
|
109
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
with target.open("w", encoding="utf-8") as f:
|
|
112
|
+
json.dump(tree, f, ensure_ascii=False, indent=2)
|
|
113
|
+
return target
|
|
114
|
+
|
|
115
|
+
def parse_file(self, user3_path: str | Path, round_floats: bool = True) -> JsonTree:
|
|
116
|
+
"""Parse one .user.3 file into the compact exported JSON tree.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
120
|
+
round_floats (bool): Whether exported floats should be rounded to four decimal places for readability.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
JsonTree: JSON-compatible tree used by export, editing, or packing workflows.
|
|
124
|
+
"""
|
|
125
|
+
exporter = self._new_exporter(user3_path, Path.cwd(), [])
|
|
126
|
+
# Build enum lookup and context metadata in memory; single-file parsing should not create Enums_Internal.json.
|
|
127
|
+
# Register enum values through the shared lookup tables so readable labels and
|
|
128
|
+
# numeric packing stay reversible.
|
|
129
|
+
self._prepare_exporter_metadata(exporter)
|
|
130
|
+
tree = exporter._parse_user3(Path(user3_path))
|
|
131
|
+
tree = exporter._postprocess_enum_nodes(tree)
|
|
132
|
+
tree = exporter._finalize_export_tree(tree)
|
|
133
|
+
if round_floats:
|
|
134
|
+
return exporter._round_export_floats(tree)
|
|
135
|
+
return tree
|
|
136
|
+
|
|
137
|
+
def parse_pack_file(self, user3_path: str | Path) -> JsonTree:
|
|
138
|
+
"""Parse one .user.3 file into the full instance-table JSON used for stable repacking.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
JsonTree: JSON-compatible tree used by export, editing, or packing workflows.
|
|
145
|
+
"""
|
|
146
|
+
exporter = self._new_exporter(user3_path, Path.cwd(), [])
|
|
147
|
+
self._prepare_exporter_metadata(exporter)
|
|
148
|
+
return exporter._parse_user3_pack(Path(user3_path))
|
|
149
|
+
|
|
150
|
+
def pack_directory(
|
|
151
|
+
self,
|
|
152
|
+
json_root: str | Path,
|
|
153
|
+
output_root: str | Path,
|
|
154
|
+
exclude_regexes: list[str] | None = None,
|
|
155
|
+
) -> dict[str, int]:
|
|
156
|
+
"""Pack every selected JSON file under a directory or single-file root.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
json_root (str | Path): JSON file or directory root to process.
|
|
160
|
+
output_root (str | Path): Directory where generated output is written.
|
|
161
|
+
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
162
|
+
relative paths.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
dict[str, int]: Counters describing total, successful, and failed items.
|
|
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
|
+
"""Pack one JSON document to the requested .user.3 path.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
json_path (str | Path): Path to the JSON document read from or written by this workflow.
|
|
175
|
+
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
|
|
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
|
+
"""Encode an in-memory JSON tree as .user.3 bytes.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
data (Any): JSON tree or binary payload consumed by this conversion step.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
bytes: Encoded binary data ready to write to disk.
|
|
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
|
+
"""Patch one .user.3 file through a callback and write the packed result.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
204
|
+
output_path (str | Path): Destination path where the generated file is written.
|
|
205
|
+
callback (PatchCallback): User callback that may inspect or modify parsed JSON.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
|
|
209
|
+
"""
|
|
210
|
+
source = Path(user3_path)
|
|
211
|
+
# Preserve instance numbering and reference identity; RSZ object links depend on
|
|
212
|
+
# these indexes remaining stable.
|
|
213
|
+
data = self.parse_pack_file(source)
|
|
214
|
+
modified = self._run_callback(callback, data, source)
|
|
215
|
+
if modified is None:
|
|
216
|
+
modified = data
|
|
217
|
+
target = Path(output_path)
|
|
218
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
target.write_bytes(self.pack(modified))
|
|
220
|
+
return target
|
|
221
|
+
|
|
222
|
+
def patch_directory(
|
|
223
|
+
self,
|
|
224
|
+
user3_root: str | Path,
|
|
225
|
+
output_root: str | Path,
|
|
226
|
+
callback: PatchCallback,
|
|
227
|
+
include_regexes: list[str] | None = None,
|
|
228
|
+
exclude_regexes: list[str] | None = None,
|
|
229
|
+
) -> dict[str, int]:
|
|
230
|
+
"""Patch every selected .user.3 file under a root directory.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
user3_root (str | Path): Source .user.3 file or directory root.
|
|
234
|
+
output_root (str | Path): Directory where generated output is written.
|
|
235
|
+
callback (PatchCallback): User callback that may inspect or modify parsed JSON.
|
|
236
|
+
include_regexes (list[str] | None): Regular expressions used to include matching
|
|
237
|
+
relative paths.
|
|
238
|
+
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
239
|
+
relative paths.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
dict[str, int]: Counters describing total, successful, and failed items.
|
|
243
|
+
"""
|
|
244
|
+
source_root = Path(user3_root)
|
|
245
|
+
target_root = Path(output_root)
|
|
246
|
+
files = self._discover_user3_files(source_root)
|
|
247
|
+
include_patterns = [re.compile(p) for p in (include_regexes or [])]
|
|
248
|
+
exclude_patterns = [re.compile(p) for p in (exclude_regexes or [])]
|
|
249
|
+
|
|
250
|
+
total = success = failed = skipped = 0
|
|
251
|
+
for file_path in files:
|
|
252
|
+
# Resolve and validate paths at the boundary so later code never guesses
|
|
253
|
+
# relative to a surprising working directory.
|
|
254
|
+
rel = (
|
|
255
|
+
file_path.name
|
|
256
|
+
if source_root.is_file()
|
|
257
|
+
else file_path.relative_to(source_root).as_posix()
|
|
258
|
+
)
|
|
259
|
+
if include_patterns and not any(
|
|
260
|
+
pattern.search(rel) for pattern in include_patterns
|
|
261
|
+
):
|
|
262
|
+
skipped += 1
|
|
263
|
+
continue
|
|
264
|
+
if any(pattern.search(rel) for pattern in exclude_patterns):
|
|
265
|
+
skipped += 1
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
total += 1
|
|
269
|
+
output_path = target_root / (
|
|
270
|
+
file_path.name
|
|
271
|
+
if source_root.is_file()
|
|
272
|
+
else file_path.relative_to(source_root)
|
|
273
|
+
)
|
|
274
|
+
try:
|
|
275
|
+
# Treat each file independently so one malformed resource is reported
|
|
276
|
+
# but does not stop the rest of the batch.
|
|
277
|
+
self.patch_file(file_path, output_path, callback)
|
|
278
|
+
success += 1
|
|
279
|
+
except Exception:
|
|
280
|
+
failed += 1
|
|
281
|
+
return {
|
|
282
|
+
"total": total,
|
|
283
|
+
"success": success,
|
|
284
|
+
"failed": failed,
|
|
285
|
+
"skipped": skipped,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
def _new_exporter(
|
|
289
|
+
self,
|
|
290
|
+
user3_root: str | Path,
|
|
291
|
+
output_root: str | Path,
|
|
292
|
+
exclude_regexes: list[str] | None,
|
|
293
|
+
) -> User3Exporter:
|
|
294
|
+
"""Create an exporter with this facade's schema, enum metadata, and magic values.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
user3_root (str | Path): Source .user.3 file or directory root.
|
|
298
|
+
output_root (str | Path): Directory where generated output is written.
|
|
299
|
+
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
300
|
+
relative paths.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
User3Exporter: Configured object or normalized value returned for the caller to use directly.
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
FileNotFoundError: A required file or directory was missing.
|
|
307
|
+
"""
|
|
308
|
+
if self.il2cpp_dump_path is None:
|
|
309
|
+
raise FileNotFoundError("il2cpp_dump_path is required for exporting JSON")
|
|
310
|
+
return User3Exporter(
|
|
311
|
+
user3_root=user3_root,
|
|
312
|
+
schema_dir=self.schema_path,
|
|
313
|
+
output_root=output_root,
|
|
314
|
+
tree_depth=self.tree_depth,
|
|
315
|
+
exclude_regexes=exclude_regexes or [],
|
|
316
|
+
il2cpp_dump_path=self.il2cpp_dump_path,
|
|
317
|
+
user_magic=self.user_magic,
|
|
318
|
+
rsz_magic=self.rsz_magic,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
def _new_packer(self, output_root: str | Path | None) -> User3Packer:
|
|
322
|
+
"""Create a packer with this facade's schema, enum metadata, and magic values.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
output_root (str | Path | None): Directory where generated output is written.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
User3Packer: Configured object or normalized value returned for the caller to use directly.
|
|
329
|
+
"""
|
|
330
|
+
return User3Packer(
|
|
331
|
+
schema_dir=self.schema_path,
|
|
332
|
+
il2cpp_dump_path=self.il2cpp_dump_path,
|
|
333
|
+
output_root=output_root,
|
|
334
|
+
user_magic=self.user_magic,
|
|
335
|
+
rsz_magic=self.rsz_magic,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _prepare_exporter_metadata(self, exporter: User3Exporter) -> None:
|
|
339
|
+
"""Prepare exporter metadata.
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
exporter (User3Exporter): Exporter instance whose metadata caches are being prepared.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
None. The method performs its documented side effect in place and raises on invalid input.
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
FileNotFoundError: A required file or directory was missing.
|
|
349
|
+
"""
|
|
350
|
+
if self.il2cpp_dump_path is None or not self.il2cpp_dump_path.is_file():
|
|
351
|
+
raise FileNotFoundError("il2cpp_dump_path is required for parsing JSON")
|
|
352
|
+
with self.il2cpp_dump_path.open("r", encoding="utf-8") as f:
|
|
353
|
+
il2cpp_dump = json.load(f)
|
|
354
|
+
# Batch export writes Enums_Internal.json for downstream tools; direct
|
|
355
|
+
# parsing keeps the same lookup only in memory.
|
|
356
|
+
# Keep the generated metadata attached to the exporter so field parsing can
|
|
357
|
+
# format enum names consistently.
|
|
358
|
+
enums_internal = exporter.export_enums_internal(il2cpp_dump)
|
|
359
|
+
exporter.enum_lookup = exporter._build_enum_lookup_from_enums_internal(
|
|
360
|
+
enums_internal
|
|
361
|
+
)
|
|
362
|
+
enum_context = exporter.export_enum_context_internal(il2cpp_dump)
|
|
363
|
+
exporter._apply_enum_context(enum_context)
|
|
364
|
+
exporter._ensure_enum_lookup()
|
|
365
|
+
|
|
366
|
+
@staticmethod
|
|
367
|
+
def _discover_user3_files(user3_root: Path) -> list[Path]:
|
|
368
|
+
"""Discover user3 files.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
user3_root (Path): Source .user.3 file or directory root.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
list[Path]: Filesystem paths selected for batch processing after suffix and exclusion checks.
|
|
375
|
+
|
|
376
|
+
Raises:
|
|
377
|
+
FileNotFoundError: A required file or directory was missing.
|
|
378
|
+
"""
|
|
379
|
+
if user3_root.is_file():
|
|
380
|
+
return [user3_root]
|
|
381
|
+
if not user3_root.is_dir():
|
|
382
|
+
raise FileNotFoundError(f"user3 root not found: {user3_root}")
|
|
383
|
+
files = sorted(user3_root.rglob("*.user.3"))
|
|
384
|
+
if not files:
|
|
385
|
+
raise FileNotFoundError(f"no *.user.3 found under: {user3_root}")
|
|
386
|
+
return files
|
|
387
|
+
|
|
388
|
+
@staticmethod
|
|
389
|
+
def _run_callback(
|
|
390
|
+
callback: PatchCallback, data: JsonTree, source_path: Path
|
|
391
|
+
) -> JsonTree | None:
|
|
392
|
+
"""Run callback.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
callback (PatchCallback): User callback that may inspect or modify parsed JSON.
|
|
396
|
+
data (JsonTree): JSON tree or binary payload consumed by this conversion step.
|
|
397
|
+
source_path (Path): Original source path associated with a patch callback or output
|
|
398
|
+
path.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
JsonTree | None: Configured object or normalized value returned for the caller to use directly.
|
|
402
|
+
"""
|
|
403
|
+
try:
|
|
404
|
+
param_count = len(inspect.signature(callback).parameters)
|
|
405
|
+
except (TypeError, ValueError):
|
|
406
|
+
# Some callables do not expose inspectable signatures, so fall back to
|
|
407
|
+
# the full callback signature when introspection fails.
|
|
408
|
+
param_count = 2
|
|
409
|
+
if param_count <= 1:
|
|
410
|
+
return callback(data)
|
|
411
|
+
return callback(data, source_path)
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Define the pyreuser3 command line interface for exporting .user.3 files to JSON and packing JSON back to .user.3.
|
|
2
|
+
|
|
3
|
+
Heavy converter imports stay inside subcommand handlers so help and version output
|
|
4
|
+
remain lightweight.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
@@ -8,18 +12,27 @@ from importlib.metadata import PackageNotFoundError, version
|
|
|
8
12
|
from typing import Sequence
|
|
9
13
|
|
|
10
14
|
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
|
|
|
15
16
|
|
|
16
17
|
def parse_int_arg(value: str) -> int:
|
|
17
|
-
"""Parse decimal or
|
|
18
|
+
"""Parse a decimal or hexadecimal integer command line value.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
value (str): Value to parse, normalize, compare, or serialize.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
int: Integer decoded from input data, metadata, or the command-line option being parsed.
|
|
25
|
+
"""
|
|
18
26
|
return int(value, 0)
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
def package_version() -> str:
|
|
22
|
-
"""Return installed package version
|
|
30
|
+
"""Return the installed package version with a source-tree fallback.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
str: Normalized or formatted text.
|
|
35
|
+
"""
|
|
23
36
|
try:
|
|
24
37
|
return version("PyREUser3")
|
|
25
38
|
except PackageNotFoundError:
|
|
@@ -27,7 +40,14 @@ def package_version() -> str:
|
|
|
27
40
|
|
|
28
41
|
|
|
29
42
|
def normalize_tree_depth(value: str) -> int | str:
|
|
30
|
-
"""Normalize tree
|
|
43
|
+
"""Normalize a tree-depth CLI value to auto or a non-negative integer.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
value (str): Value to parse, normalize, compare, or serialize.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
int | str: Parsed integer value or the literal auto mode.
|
|
50
|
+
"""
|
|
31
51
|
text = value.strip().lower()
|
|
32
52
|
if text == "auto":
|
|
33
53
|
return "auto"
|
|
@@ -38,7 +58,14 @@ def normalize_tree_depth(value: str) -> int | str:
|
|
|
38
58
|
|
|
39
59
|
|
|
40
60
|
def add_magic_args(parser: argparse.ArgumentParser) -> None:
|
|
41
|
-
"""Add
|
|
61
|
+
"""Add shared USR and RSZ magic number options to a parser.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
parser (argparse.ArgumentParser): Argument parser being populated with CLI subcommands and shared options.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
None. The method performs its documented side effect in place and raises on invalid input.
|
|
68
|
+
"""
|
|
42
69
|
parser.add_argument(
|
|
43
70
|
"--user-magic",
|
|
44
71
|
type=parse_int_arg,
|
|
@@ -54,7 +81,12 @@ def add_magic_args(parser: argparse.ArgumentParser) -> None:
|
|
|
54
81
|
|
|
55
82
|
|
|
56
83
|
def build_parser() -> argparse.ArgumentParser:
|
|
57
|
-
"""Build the
|
|
84
|
+
"""Build the CLI parser and subcommands.
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
argparse.ArgumentParser: Configured argument parser for the command-line interface.
|
|
89
|
+
"""
|
|
58
90
|
parser = argparse.ArgumentParser(
|
|
59
91
|
prog="pyreuser3",
|
|
60
92
|
description="Convert RE Engine .user.3 files to JSON and pack them back.",
|
|
@@ -158,7 +190,17 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
158
190
|
|
|
159
191
|
|
|
160
192
|
def run_export(args: argparse.Namespace) -> int:
|
|
161
|
-
"""Run the export subcommand.
|
|
193
|
+
"""Run the CLI export subcommand.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
args (argparse.Namespace): Parsed command-line namespace for the selected CLI command.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
int: Integer decoded from input data, metadata, or the command-line option being parsed.
|
|
200
|
+
"""
|
|
201
|
+
from .export import User3Exporter
|
|
202
|
+
from .rich_ui import get_console
|
|
203
|
+
|
|
162
204
|
console = get_console()
|
|
163
205
|
console.log("Exporting .user.3 files to JSON...")
|
|
164
206
|
exporter = User3Exporter(
|
|
@@ -177,7 +219,17 @@ def run_export(args: argparse.Namespace) -> int:
|
|
|
177
219
|
|
|
178
220
|
|
|
179
221
|
def run_pack(args: argparse.Namespace) -> int:
|
|
180
|
-
"""Run the pack subcommand.
|
|
222
|
+
"""Run the CLI pack subcommand.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
args (argparse.Namespace): Parsed command-line namespace for the selected CLI command.
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
int: Integer decoded from input data, metadata, or the command-line option being parsed.
|
|
229
|
+
"""
|
|
230
|
+
from .pack import User3Packer
|
|
231
|
+
from .rich_ui import get_console
|
|
232
|
+
|
|
181
233
|
console = get_console()
|
|
182
234
|
console.log("Packing JSON files to .user.3...")
|
|
183
235
|
packer = User3Packer(
|
|
@@ -197,7 +249,14 @@ def run_pack(args: argparse.Namespace) -> int:
|
|
|
197
249
|
|
|
198
250
|
|
|
199
251
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
200
|
-
"""
|
|
252
|
+
"""Run the command entry point.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
argv (Sequence[str] | None): Optional argument list; None means use the process command line.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
int: Integer decoded from input data, metadata, or the command-line option being parsed.
|
|
259
|
+
"""
|
|
201
260
|
parser = build_parser()
|
|
202
261
|
args = parser.parse_args(argv)
|
|
203
262
|
return int(args.func(args))
|