PyREUser3 0.5.0__tar.gz → 0.7.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.5.0 → pyreuser3-0.7.0}/PKG-INFO +7 -1
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/PKG-INFO +7 -1
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/SOURCES.txt +6 -1
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/README.md +36 -4
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/docs/PYPI.md +6 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/docs/README.zh-CN.md +31 -4
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyproject.toml +1 -1
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/__init__.py +3 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/api.py +18 -5
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/cli.py +10 -6
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/core.py +6 -1
- pyreuser3-0.7.0/pyreuser3/enum_codec.py +231 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/base.py +19 -5
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/enums.py +36 -20
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/metadata.py +36 -11
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/postprocess.py +97 -7
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/user3.py +114 -97
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/pack/base.py +98 -21
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/pack/models.py +46 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/pack/plan.py +379 -9
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/pack/writer.py +263 -46
- pyreuser3-0.7.0/pyreuser3/usr_container.py +339 -0
- pyreuser3-0.7.0/pyreuser3/usr_layouts.py +321 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/tests/test_enum_binary.py +17 -0
- pyreuser3-0.7.0/tests/test_enum_codec.py +208 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/tests/test_export_enums.py +46 -0
- pyreuser3-0.7.0/tests/test_usr_container.py +377 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/LICENSE +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/MANIFEST.in +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/fields.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.5.0 → pyreuser3-0.7.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
|
|
5
5
|
Author: Egg Targaryen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,11 +48,17 @@ pip install pyreuser3
|
|
|
48
48
|
- A reusable Python API through `REUser3Converter`.
|
|
49
49
|
- CLI commands through `pyreuser3`.
|
|
50
50
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
51
|
+
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
52
|
+
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
51
53
|
|
|
52
54
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
53
55
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
54
56
|
version.
|
|
55
57
|
|
|
58
|
+
Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
|
|
59
|
+
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
60
|
+
until real fixtures provide byte-for-byte validation.
|
|
61
|
+
|
|
56
62
|
## Requirements
|
|
57
63
|
|
|
58
64
|
- Python 3.9 or newer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
|
|
5
5
|
Author: Egg Targaryen
|
|
6
6
|
License-Expression: MIT
|
|
@@ -48,11 +48,17 @@ pip install pyreuser3
|
|
|
48
48
|
- A reusable Python API through `REUser3Converter`.
|
|
49
49
|
- CLI commands through `pyreuser3`.
|
|
50
50
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
51
|
+
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
52
|
+
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
51
53
|
|
|
52
54
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
53
55
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
54
56
|
version.
|
|
55
57
|
|
|
58
|
+
Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
|
|
59
|
+
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
60
|
+
until real fixtures provide byte-for-byte validation.
|
|
61
|
+
|
|
56
62
|
## Requirements
|
|
57
63
|
|
|
58
64
|
- Python 3.9 or newer.
|
|
@@ -15,8 +15,11 @@ pyreuser3/__main__.py
|
|
|
15
15
|
pyreuser3/api.py
|
|
16
16
|
pyreuser3/cli.py
|
|
17
17
|
pyreuser3/core.py
|
|
18
|
+
pyreuser3/enum_codec.py
|
|
18
19
|
pyreuser3/rich_ui.py
|
|
19
20
|
pyreuser3/schema.py
|
|
21
|
+
pyreuser3/usr_container.py
|
|
22
|
+
pyreuser3/usr_layouts.py
|
|
20
23
|
pyreuser3/export/__init__.py
|
|
21
24
|
pyreuser3/export/base.py
|
|
22
25
|
pyreuser3/export/enums.py
|
|
@@ -40,4 +43,6 @@ pyreuser3/web/runners.py
|
|
|
40
43
|
pyreuser3/web/server.py
|
|
41
44
|
pyreuser3/web/settings.py
|
|
42
45
|
tests/test_enum_binary.py
|
|
43
|
-
tests/
|
|
46
|
+
tests/test_enum_codec.py
|
|
47
|
+
tests/test_export_enums.py
|
|
48
|
+
tests/test_usr_container.py
|
|
@@ -55,11 +55,20 @@ pyreuser3 export \
|
|
|
55
55
|
-p <il2cpp_dump.json>
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
Export full repack JSON, then pack it back to `.user.3`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pyreuser3 export \
|
|
62
|
+
-i <input-user3-file-or-directory> \
|
|
63
|
+
-s <RE_RSZ-schema.json> \
|
|
64
|
+
-o <repack-json-output-directory> \
|
|
65
|
+
-p <il2cpp_dump.json> \
|
|
66
|
+
--json-format repack
|
|
67
|
+
```
|
|
59
68
|
|
|
60
69
|
```bash
|
|
61
70
|
pyreuser3 pack \
|
|
62
|
-
-j <input-json-file-or-directory> \
|
|
71
|
+
-j <input-repack-json-file-or-directory> \
|
|
63
72
|
-s <RE_RSZ-schema.json> \
|
|
64
73
|
-o <user3-output-directory> \
|
|
65
74
|
-p <il2cpp_dump.json>
|
|
@@ -90,8 +99,14 @@ converter.export_file(
|
|
|
90
99
|
"json/OtomonData.user.3.json",
|
|
91
100
|
)
|
|
92
101
|
|
|
102
|
+
# Packing only accepts the full repack document. Readable exports are read-only.
|
|
103
|
+
converter.export_file(
|
|
104
|
+
"input/OtomonData.user.3",
|
|
105
|
+
"json/OtomonData.user.3.pack.json",
|
|
106
|
+
json_format="repack",
|
|
107
|
+
)
|
|
93
108
|
converter.pack_file(
|
|
94
|
-
"json/OtomonData.user.3.json",
|
|
109
|
+
"json/OtomonData.user.3.pack.json",
|
|
95
110
|
"mod/OtomonData.user.3",
|
|
96
111
|
)
|
|
97
112
|
```
|
|
@@ -110,7 +125,24 @@ repack_data = converter.user3_to_json(
|
|
|
110
125
|
)
|
|
111
126
|
```
|
|
112
127
|
|
|
113
|
-
Use `json_format="readable"` for the same shape produced by `export_file()
|
|
128
|
+
Use `json_format="readable"` for the same shape produced by `export_file()`. This
|
|
129
|
+
shape is read-only. Use `json_format="repack"` for the full document accepted by
|
|
130
|
+
`pack()`; the packer rejects readable JSON.
|
|
131
|
+
|
|
132
|
+
Enum fields are rendered as `[numeric] Name` labels using the enum's actual storage
|
|
133
|
+
width. Scalar flag enums are rendered as arrays of labels. ``ace.Bitset`1<T>`` values are
|
|
134
|
+
rendered as enum-index labels together with `_MaxElement` and `_WordCount`, so unknown
|
|
135
|
+
bits and padded word arrays remain reversible. Repack exports use
|
|
136
|
+
`re_user3_pack_v3`, which records the independently detected USR outer layout, RSZ
|
|
137
|
+
header family, and numeric RSZ version while preserving resource and userdata
|
|
138
|
+
dependency tables. Layout candidates and their read/repack capability status are
|
|
139
|
+
declared in `pyreuser3/usr_layouts.py`; RSZ field definitions still come from the
|
|
140
|
+
supplied REFramework-compatible schema. The verified modern header family accepts
|
|
141
|
+
structurally valid RSZ v4+ files and preserves their original version instead of
|
|
142
|
+
forcing MHWS version 16. Experimental physical H28 and legacy RSZ v3 candidates are
|
|
143
|
+
read-only until real fixtures validate byte-for-byte repacking. V1 and v2 documents
|
|
144
|
+
are recognized for diagnostics but must be re-exported as v3 before packing because
|
|
145
|
+
they do not record the required layout metadata.
|
|
114
146
|
|
|
115
147
|
For stable patch-and-repack workflows, use `patch_file()` or `parse_pack_file()`:
|
|
116
148
|
|
|
@@ -15,11 +15,17 @@ pip install pyreuser3
|
|
|
15
15
|
- A reusable Python API through `REUser3Converter`.
|
|
16
16
|
- CLI commands through `pyreuser3`.
|
|
17
17
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
18
|
+
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
19
|
+
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
18
20
|
|
|
19
21
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
20
22
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
21
23
|
version.
|
|
22
24
|
|
|
25
|
+
Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
|
|
26
|
+
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
27
|
+
until real fixtures provide byte-for-byte validation.
|
|
28
|
+
|
|
23
29
|
## Requirements
|
|
24
30
|
|
|
25
31
|
- Python 3.9 or newer.
|
|
@@ -56,11 +56,20 @@ pyreuser3 export \
|
|
|
56
56
|
-p <il2cpp_dump.json>
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
导出完整 repack JSON,然后封回 `.user.3`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pyreuser3 export \
|
|
63
|
+
-i <输入的-user3-文件或目录> \
|
|
64
|
+
-s <RE_RSZ-schema.json> \
|
|
65
|
+
-o <repack-JSON-输出目录> \
|
|
66
|
+
-p <il2cpp_dump.json> \
|
|
67
|
+
--json-format repack
|
|
68
|
+
```
|
|
60
69
|
|
|
61
70
|
```bash
|
|
62
71
|
pyreuser3 pack \
|
|
63
|
-
-j <输入的-JSON-文件或目录> \
|
|
72
|
+
-j <输入的-repack-JSON-文件或目录> \
|
|
64
73
|
-s <RE_RSZ-schema.json> \
|
|
65
74
|
-o <user3-输出目录> \
|
|
66
75
|
-p <il2cpp_dump.json>
|
|
@@ -118,8 +127,14 @@ converter.export_file(
|
|
|
118
127
|
"json/OtomonData.user.3.json",
|
|
119
128
|
)
|
|
120
129
|
|
|
130
|
+
# 封包只接受完整 repack 文档;readable 导出仅供读取。
|
|
131
|
+
converter.export_file(
|
|
132
|
+
"input/OtomonData.user.3",
|
|
133
|
+
"json/OtomonData.user.3.pack.json",
|
|
134
|
+
json_format="repack",
|
|
135
|
+
)
|
|
121
136
|
converter.pack_file(
|
|
122
|
-
"json/OtomonData.user.3.json",
|
|
137
|
+
"json/OtomonData.user.3.pack.json",
|
|
123
138
|
"mod/OtomonData.user.3",
|
|
124
139
|
)
|
|
125
140
|
```
|
|
@@ -138,7 +153,18 @@ repack_data = converter.user3_to_json(
|
|
|
138
153
|
)
|
|
139
154
|
```
|
|
140
155
|
|
|
141
|
-
使用 `json_format="readable"` 时返回与 `export_file()`
|
|
156
|
+
使用 `json_format="readable"` 时返回与 `export_file()` 一致的只读导出结构;使用
|
|
157
|
+
`json_format="repack"` 时返回可传给 `pack()` 的完整实例表结构。封回器会拒绝 readable JSON。
|
|
158
|
+
|
|
159
|
+
枚举字段会按照真实底层存储宽度输出为 `[数值] 名称`。标量位标志枚举输出为标签数组;
|
|
160
|
+
``ace.Bitset`1<T>`` 输出为枚举索引标签,并保留 `_MaxElement` 与 `_WordCount`,因此未知位和
|
|
161
|
+
填充词也可以无损封回。新的 repack 文档格式为 `re_user3_pack_v3`,它会分别记录自动探测到的
|
|
162
|
+
USR 外层布局、RSZ 头族和文件中的真实 RSZ 版本,并保留 resource 与 userdata 依赖表。布局
|
|
163
|
+
候选及其读取/回封能力状态集中声明在 `pyreuser3/usr_layouts.py`,RSZ 字段定义仍来自传入的
|
|
164
|
+
REFramework 兼容模板。已验证的现代头族接受通过完整结构校验的 RSZ v4+ 文件,并原样保留
|
|
165
|
+
版本号,不再固定为 MHWS 的版本 16。实验性的物理 H28 与 legacy RSZ v3 候选暂时只读,获得
|
|
166
|
+
真实样本并完成逐字节回封验证后才能启用 repack。v1 和 v2 文档仍可识别以便诊断,但由于缺少
|
|
167
|
+
必要的布局元数据,封回前必须从源文件重新导出为 v3。
|
|
142
168
|
|
|
143
169
|
批量处理目录:
|
|
144
170
|
|
|
@@ -153,6 +179,7 @@ converter = REUser3Converter(
|
|
|
153
179
|
export_result = converter.export_directory(
|
|
154
180
|
"D:/game/unpacked",
|
|
155
181
|
"D:/game/json",
|
|
182
|
+
json_format="repack",
|
|
156
183
|
)
|
|
157
184
|
|
|
158
185
|
pack_result = converter.pack_directory(
|
|
@@ -73,6 +73,7 @@ class REUser3Converter:
|
|
|
73
73
|
user3_root: str | Path,
|
|
74
74
|
output_root: str | Path,
|
|
75
75
|
exclude_regexes: list[str] | None = None,
|
|
76
|
+
json_format: JsonFormat = "readable",
|
|
76
77
|
) -> dict[str, int]:
|
|
77
78
|
"""Export every selected .user.3 file under a directory or single-file root.
|
|
78
79
|
|
|
@@ -81,33 +82,42 @@ class REUser3Converter:
|
|
|
81
82
|
output_root (str | Path): Directory where generated output is written.
|
|
82
83
|
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
83
84
|
relative paths.
|
|
85
|
+
json_format (JsonFormat): "readable" for read-only exports or "repack"
|
|
86
|
+
for documents accepted by pack().
|
|
84
87
|
|
|
85
88
|
Returns:
|
|
86
89
|
dict[str, int]: Counters describing total, successful, and failed items.
|
|
87
90
|
"""
|
|
88
|
-
exporter = self._new_exporter(
|
|
91
|
+
exporter = self._new_exporter(
|
|
92
|
+
user3_root,
|
|
93
|
+
output_root,
|
|
94
|
+
exclude_regexes,
|
|
95
|
+
json_format=self._normalize_json_format(json_format),
|
|
96
|
+
)
|
|
89
97
|
return exporter.run()
|
|
90
98
|
|
|
91
99
|
def export_file(
|
|
92
100
|
self,
|
|
93
101
|
user3_path: str | Path,
|
|
94
102
|
json_path: str | Path,
|
|
103
|
+
json_format: JsonFormat = "readable",
|
|
95
104
|
) -> Path:
|
|
96
105
|
"""Export one .user.3 file to the requested JSON path.
|
|
97
106
|
|
|
98
107
|
Args:
|
|
99
108
|
user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
|
|
100
109
|
json_path (str | Path): Path to the JSON document read from or written by this workflow.
|
|
110
|
+
json_format (JsonFormat): "readable" for a read-only tree or "repack"
|
|
111
|
+
for a document accepted by pack().
|
|
101
112
|
|
|
102
113
|
Returns:
|
|
103
114
|
Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
|
|
104
115
|
"""
|
|
105
|
-
#
|
|
106
|
-
#
|
|
107
|
-
# remain compatible across workflows.
|
|
116
|
+
# Use the same in-memory format selector as parse_file/parse_pack_file so
|
|
117
|
+
# single-file and batch exports keep identical document shapes.
|
|
108
118
|
tree = self.user3_to_json(
|
|
109
119
|
user3_path,
|
|
110
|
-
json_format=
|
|
120
|
+
json_format=json_format,
|
|
111
121
|
round_floats=True,
|
|
112
122
|
)
|
|
113
123
|
target = Path(json_path)
|
|
@@ -343,6 +353,7 @@ class REUser3Converter:
|
|
|
343
353
|
user3_root: str | Path,
|
|
344
354
|
output_root: str | Path,
|
|
345
355
|
exclude_regexes: list[str] | None,
|
|
356
|
+
json_format: str = "readable",
|
|
346
357
|
) -> User3Exporter:
|
|
347
358
|
"""Create an exporter with this facade's schema, enum metadata, and magic values.
|
|
348
359
|
|
|
@@ -351,6 +362,7 @@ class REUser3Converter:
|
|
|
351
362
|
output_root (str | Path): Directory where generated output is written.
|
|
352
363
|
exclude_regexes (list[str] | None): Regular expressions used to skip matching
|
|
353
364
|
relative paths.
|
|
365
|
+
json_format (str): Export document shape requested by the caller.
|
|
354
366
|
|
|
355
367
|
Returns:
|
|
356
368
|
User3Exporter: Configured object or normalized value returned for the caller to use directly.
|
|
@@ -369,6 +381,7 @@ class REUser3Converter:
|
|
|
369
381
|
il2cpp_dump_path=self.il2cpp_dump_path,
|
|
370
382
|
user_magic=self.user_magic,
|
|
371
383
|
rsz_magic=self.rsz_magic,
|
|
384
|
+
json_format=json_format,
|
|
372
385
|
)
|
|
373
386
|
|
|
374
387
|
def _new_packer(self, output_root: str | Path | None) -> User3Packer:
|
|
@@ -8,9 +8,9 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import argparse
|
|
10
10
|
import json
|
|
11
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
12
11
|
from typing import Sequence
|
|
13
12
|
|
|
13
|
+
from . import __version__
|
|
14
14
|
from .core import RSZ_MAGIC, USR_MAGIC
|
|
15
15
|
|
|
16
16
|
|
|
@@ -33,10 +33,7 @@ def package_version() -> str:
|
|
|
33
33
|
Returns:
|
|
34
34
|
str: Normalized or formatted text.
|
|
35
35
|
"""
|
|
36
|
-
|
|
37
|
-
return version("PyREUser3")
|
|
38
|
-
except PackageNotFoundError:
|
|
39
|
-
return "0.1.0"
|
|
36
|
+
return __version__
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
def normalize_tree_depth(value: str) -> int | str:
|
|
@@ -143,6 +140,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
143
140
|
required=True,
|
|
144
141
|
help="Path to il2cpp_dump.json, used to generate enum labels.",
|
|
145
142
|
)
|
|
143
|
+
export_parser.add_argument(
|
|
144
|
+
"--json-format",
|
|
145
|
+
choices=("readable", "repack"),
|
|
146
|
+
default="readable",
|
|
147
|
+
help="Export read-only readable JSON or packable repack JSON (default: readable).",
|
|
148
|
+
)
|
|
146
149
|
add_magic_args(export_parser)
|
|
147
150
|
export_parser.set_defaults(func=run_export)
|
|
148
151
|
|
|
@@ -154,7 +157,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
154
157
|
"--input-json",
|
|
155
158
|
"-j",
|
|
156
159
|
required=True,
|
|
157
|
-
help="JSON file or root directory that contains
|
|
160
|
+
help="Repack JSON file or root directory that contains repack documents.",
|
|
158
161
|
)
|
|
159
162
|
pack_parser.add_argument(
|
|
160
163
|
"--schema-path",
|
|
@@ -212,6 +215,7 @@ def run_export(args: argparse.Namespace) -> int:
|
|
|
212
215
|
il2cpp_dump_path=args.il2cpp_dump_path,
|
|
213
216
|
user_magic=args.user_magic,
|
|
214
217
|
rsz_magic=args.rsz_magic,
|
|
218
|
+
json_format=args.json_format,
|
|
215
219
|
)
|
|
216
220
|
result = exporter.run()
|
|
217
221
|
console.log("Export complete:", json.dumps(result, ensure_ascii=False))
|
|
@@ -20,7 +20,12 @@ USR_MAGIC = 5395285
|
|
|
20
20
|
RSZ_MAGIC = 5919570
|
|
21
21
|
# Preserve instance numbering and reference identity; RSZ object links depend on these
|
|
22
22
|
# indexes remaining stable.
|
|
23
|
-
|
|
23
|
+
PACK_JSON_FORMAT_V1 = "re_user3_pack_v1"
|
|
24
|
+
PACK_JSON_FORMAT_V2 = "re_user3_pack_v2"
|
|
25
|
+
PACK_JSON_FORMAT = "re_user3_pack_v3"
|
|
26
|
+
PACK_JSON_FORMATS = frozenset(
|
|
27
|
+
{PACK_JSON_FORMAT_V1, PACK_JSON_FORMAT_V2, PACK_JSON_FORMAT}
|
|
28
|
+
)
|
|
24
29
|
# Decode strings and GUID-like values conservatively so invalid data does not corrupt
|
|
25
30
|
# subsequent parsing.
|
|
26
31
|
HEX32_RE = re.compile(r"^[0-9a-fA-F]{32}$")
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Shared enum, flag, and ``ace.Bitset`` conversion helpers.
|
|
2
|
+
|
|
3
|
+
The exporter and packer deliberately share these operations so readable labels remain
|
|
4
|
+
lossless across signed/unsigned storage widths and generic container wrappers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ENUM_LABEL_RE = re.compile(r"^\[(-?\d+)\]\s*(.*)$")
|
|
14
|
+
_GENERIC_RE = re.compile(r"^[^<]+<(.+)>$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_integer_for_storage(value: int, storage_type: str | None) -> int:
|
|
18
|
+
"""Normalize an integer according to an enum's declared storage width/sign."""
|
|
19
|
+
storage = storage_type or "S32"
|
|
20
|
+
widths = {
|
|
21
|
+
"S8": (8, True),
|
|
22
|
+
"U8": (8, False),
|
|
23
|
+
"S16": (16, True),
|
|
24
|
+
"U16": (16, False),
|
|
25
|
+
"S32": (32, True),
|
|
26
|
+
"U32": (32, False),
|
|
27
|
+
"S64": (64, True),
|
|
28
|
+
"U64": (64, False),
|
|
29
|
+
}
|
|
30
|
+
bits, signed = widths.get(storage, (32, True))
|
|
31
|
+
mask = (1 << bits) - 1
|
|
32
|
+
normalized = int(value) & mask
|
|
33
|
+
if signed and normalized >= 1 << (bits - 1):
|
|
34
|
+
normalized -= 1 << bits
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_enum_label(value: Any) -> int | None:
|
|
39
|
+
"""Return the authoritative numeric prefix from ``[N] Name`` labels."""
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return int(value)
|
|
42
|
+
if isinstance(value, int):
|
|
43
|
+
return value
|
|
44
|
+
if not isinstance(value, str):
|
|
45
|
+
return None
|
|
46
|
+
match = ENUM_LABEL_RE.match(value.strip())
|
|
47
|
+
if match:
|
|
48
|
+
return int(match.group(1))
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def enum_member_for_value(
|
|
53
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
54
|
+
enum_type: str,
|
|
55
|
+
value: int,
|
|
56
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
57
|
+
) -> tuple[str, int] | None:
|
|
58
|
+
"""Resolve a value without inventing 32-bit aliases for wider enum types."""
|
|
59
|
+
value_map = enum_lookup.get(enum_type)
|
|
60
|
+
if not value_map:
|
|
61
|
+
return None
|
|
62
|
+
storage = (enum_underlying_types or {}).get(enum_type, "S32")
|
|
63
|
+
target = normalize_integer_for_storage(value, storage)
|
|
64
|
+
direct = value_map.get(value)
|
|
65
|
+
if direct is not None and normalize_integer_for_storage(direct[1], storage) == target:
|
|
66
|
+
return direct
|
|
67
|
+
for member_name, raw_value in value_map.values():
|
|
68
|
+
if normalize_integer_for_storage(raw_value, storage) == target:
|
|
69
|
+
return member_name, raw_value
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generic_arguments(type_name: str) -> list[str]:
|
|
74
|
+
"""Parse the simple, fully-qualified generic type names emitted by il2cpp dumps."""
|
|
75
|
+
if not isinstance(type_name, str):
|
|
76
|
+
return []
|
|
77
|
+
match = _GENERIC_RE.match(type_name)
|
|
78
|
+
if not match:
|
|
79
|
+
return []
|
|
80
|
+
# These metadata names are not assembly-qualified and current RE generic arguments
|
|
81
|
+
# are flat. Keeping this parser intentionally strict prevents ambiguous guesses.
|
|
82
|
+
return [part.strip() for part in match.group(1).split(",") if part.strip()]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def bitset_enum_type(class_name: str) -> str | None:
|
|
86
|
+
"""Return the generic enum argument for one ``ace.Bitset`1`` class."""
|
|
87
|
+
if not isinstance(class_name, str) or not class_name.startswith("ace.Bitset`1<"):
|
|
88
|
+
return None
|
|
89
|
+
args = generic_arguments(class_name)
|
|
90
|
+
return args[0] if len(args) == 1 else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_probable_flags_enum(
|
|
94
|
+
enum_type: str,
|
|
95
|
+
value_map: dict[int, tuple[str, int]],
|
|
96
|
+
storage_type: str | None = None,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""Conservatively identify bit-mask enums from their name and member values."""
|
|
99
|
+
upper = enum_type.upper()
|
|
100
|
+
markers = ("BIT", "BITS", "FLAG", "FLAGS", "MASK", "ATTR")
|
|
101
|
+
if not any(marker in upper for marker in markers):
|
|
102
|
+
return False
|
|
103
|
+
storage = storage_type or "S32"
|
|
104
|
+
width = int(storage[1:])
|
|
105
|
+
mask = (1 << width) - 1
|
|
106
|
+
values = {
|
|
107
|
+
normalize_integer_for_storage(raw, storage) & mask
|
|
108
|
+
for _name, raw in value_map.values()
|
|
109
|
+
if raw != 0
|
|
110
|
+
}
|
|
111
|
+
basis = {value for value in values if value and value & (value - 1) == 0}
|
|
112
|
+
if len(basis) < 2:
|
|
113
|
+
return False
|
|
114
|
+
known_mask = 0
|
|
115
|
+
for value in basis:
|
|
116
|
+
known_mask |= value
|
|
117
|
+
return all(value & ~known_mask == 0 for value in values)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def decode_flags(
|
|
121
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
122
|
+
enum_type: str,
|
|
123
|
+
value: int,
|
|
124
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
125
|
+
) -> list[str]:
|
|
126
|
+
"""Expand a scalar mask to stable labels, preserving unknown bits explicitly."""
|
|
127
|
+
storage = (enum_underlying_types or {}).get(enum_type, "S32")
|
|
128
|
+
normalized = normalize_integer_for_storage(value, storage)
|
|
129
|
+
bits = normalized if normalized >= 0 else normalized & ((1 << int(storage[1:])) - 1)
|
|
130
|
+
value_map = enum_lookup.get(enum_type, {})
|
|
131
|
+
if bits == 0:
|
|
132
|
+
zero = enum_member_for_value(enum_lookup, enum_type, 0, enum_underlying_types)
|
|
133
|
+
return [f"[0] {zero[0]}"] if zero else []
|
|
134
|
+
labels: list[str] = []
|
|
135
|
+
remaining = bits
|
|
136
|
+
width_mask = (1 << int(storage[1:])) - 1
|
|
137
|
+
members: list[tuple[int, str, int]] = []
|
|
138
|
+
for name, raw in value_map.values():
|
|
139
|
+
normalized_raw = normalize_integer_for_storage(raw, storage)
|
|
140
|
+
mask_value = normalized_raw & width_mask
|
|
141
|
+
if mask_value and mask_value & (mask_value - 1) == 0:
|
|
142
|
+
members.append((mask_value, name, raw))
|
|
143
|
+
for mask_value, name, raw in sorted(members):
|
|
144
|
+
if remaining & mask_value:
|
|
145
|
+
labels.append(f"[{raw}] {name}")
|
|
146
|
+
remaining &= ~mask_value
|
|
147
|
+
bit_index = 0
|
|
148
|
+
while remaining:
|
|
149
|
+
if remaining & 1:
|
|
150
|
+
unknown_value = 1 << bit_index
|
|
151
|
+
labels.append(f"[{unknown_value}] <unknown>")
|
|
152
|
+
remaining >>= 1
|
|
153
|
+
bit_index += 1
|
|
154
|
+
return labels
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def decode_bitset(
|
|
158
|
+
words: list[int],
|
|
159
|
+
enum_type: str,
|
|
160
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
161
|
+
max_element: int | None = None,
|
|
162
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Decode 32-bit words into enum-index labels."""
|
|
165
|
+
labels: list[str] = []
|
|
166
|
+
for word_index, raw_word in enumerate(words):
|
|
167
|
+
word = int(raw_word) & 0xFFFFFFFF
|
|
168
|
+
for bit in range(32):
|
|
169
|
+
index = word_index * 32 + bit
|
|
170
|
+
if not (word & (1 << bit)):
|
|
171
|
+
continue
|
|
172
|
+
member = enum_member_for_value(
|
|
173
|
+
enum_lookup, enum_type, index, enum_underlying_types
|
|
174
|
+
)
|
|
175
|
+
labels.append(f"[{index}] {member[0] if member else '<unknown>'}")
|
|
176
|
+
return labels
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def encode_bitset(
|
|
180
|
+
labels: list[Any],
|
|
181
|
+
enum_type: str,
|
|
182
|
+
member_lookup: dict[str, dict[str, int]],
|
|
183
|
+
max_element: int | None = None,
|
|
184
|
+
word_count: int | None = None,
|
|
185
|
+
) -> list[int]:
|
|
186
|
+
"""Encode enum-index labels into 32-bit words, preserving requested padding."""
|
|
187
|
+
indexes: list[int] = []
|
|
188
|
+
members = member_lookup.get(enum_type, {})
|
|
189
|
+
for item in labels:
|
|
190
|
+
index = parse_enum_label(item)
|
|
191
|
+
if index is None and isinstance(item, str):
|
|
192
|
+
index = members.get(item.strip())
|
|
193
|
+
if index is None or index < 0:
|
|
194
|
+
raise ValueError(f"invalid {enum_type} bitset member: {item!r}")
|
|
195
|
+
if (
|
|
196
|
+
max_element is not None
|
|
197
|
+
and index >= max_element
|
|
198
|
+
and (word_count is None or index >= word_count * 32)
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"bitset index {index} exceeds _MaxElement {max_element} for {enum_type}"
|
|
202
|
+
)
|
|
203
|
+
indexes.append(index)
|
|
204
|
+
selected_minimum = (max(indexes) + 32) // 32 if indexes else 0
|
|
205
|
+
derived_count = max(
|
|
206
|
+
selected_minimum,
|
|
207
|
+
(max_element + 31) // 32 if max_element else 0,
|
|
208
|
+
)
|
|
209
|
+
count = derived_count if word_count is None else word_count
|
|
210
|
+
if count < selected_minimum:
|
|
211
|
+
raise ValueError(f"_WordCount {count} is too small for {enum_type} bitset")
|
|
212
|
+
words = [0] * count
|
|
213
|
+
for index in indexes:
|
|
214
|
+
words[index // 32] |= 1 << (index % 32)
|
|
215
|
+
return words
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def encode_flags(
|
|
219
|
+
values: list[Any], enum_type: str, member_lookup: dict[str, dict[str, int]]
|
|
220
|
+
) -> int:
|
|
221
|
+
"""Combine readable scalar flag labels back into one integer mask."""
|
|
222
|
+
result = 0
|
|
223
|
+
members = member_lookup.get(enum_type, {})
|
|
224
|
+
for item in values:
|
|
225
|
+
numeric = parse_enum_label(item)
|
|
226
|
+
if numeric is None and isinstance(item, str):
|
|
227
|
+
numeric = members.get(item.strip())
|
|
228
|
+
if numeric is None:
|
|
229
|
+
raise ValueError(f"invalid {enum_type} flag member: {item!r}")
|
|
230
|
+
result |= numeric
|
|
231
|
+
return result
|