PyREUser3 0.7.2__tar.gz → 0.8.1__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.7.2 → pyreuser3-0.8.1}/PKG-INFO +6 -4
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/PKG-INFO +6 -4
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/SOURCES.txt +3 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/README.md +23 -7
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/docs/PYPI.md +5 -3
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/docs/README.zh-CN.md +15 -4
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyproject.toml +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/__init__.py +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/api.py +7 -9
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/cli.py +10 -4
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/core.py +43 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/base.py +12 -11
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/enums.py +93 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/fields.py +17 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/metadata.py +6 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/postprocess.py +52 -4
- pyreuser3-0.8.1/pyreuser3/native_structs.py +368 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/base.py +27 -5
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/models.py +8 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/values.py +65 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/writer.py +59 -6
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/usr_container.py +5 -9
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/page.py +13 -13
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/runners.py +7 -1
- pyreuser3-0.8.1/tests/test_discovery_and_web_depth.py +189 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/tests/test_enum_binary.py +78 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/tests/test_export_enums.py +229 -0
- pyreuser3-0.8.1/tests/test_native_structs.py +339 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/LICENSE +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/MANIFEST.in +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/enum_codec.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/export/user3.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/container.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/pack/plan.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/usr_layouts.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/setup.cfg +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/tests/test_api_cache.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/tests/test_enum_codec.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.1}/tests/test_usr_container.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
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
|
|
@@ -43,7 +43,7 @@ pip install pyreuser3
|
|
|
43
43
|
|
|
44
44
|
## What Is Included
|
|
45
45
|
|
|
46
|
-
- `.user.3
|
|
46
|
+
- `.user.3` and decorated `.user.3.*` export.
|
|
47
47
|
- `JSON -> .user.3` packing.
|
|
48
48
|
- A reusable Python API through `REUser3Converter`.
|
|
49
49
|
- CLI commands through `pyreuser3`.
|
|
@@ -51,6 +51,8 @@ pip install pyreuser3
|
|
|
51
51
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
52
52
|
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
53
53
|
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
54
|
+
- il2cpp-validated readable codecs for `via.Int2`, `via.Uint2`, `via.Range`,
|
|
55
|
+
`via.RangeI`, and `via.Sphere`, with scalar/array round trips and raw fallback.
|
|
54
56
|
|
|
55
57
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
56
58
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
@@ -71,8 +73,8 @@ of Python scalar objects.
|
|
|
71
73
|
|
|
72
74
|
- Python 3.9 or newer.
|
|
73
75
|
- A RE_RSZ schema JSON file for the target game/version.
|
|
74
|
-
- An `il2cpp_dump.json` file when exporting readable enum labels.
|
|
75
|
-
- One or more unpacked `.user.3` files.
|
|
76
|
+
- An `il2cpp_dump.json` file when exporting readable enum labels and validated native structures.
|
|
77
|
+
- One or more unpacked `.user.3` or `.user.3.*` files.
|
|
76
78
|
|
|
77
79
|
`pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
|
|
78
80
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.1
|
|
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
|
|
@@ -43,7 +43,7 @@ pip install pyreuser3
|
|
|
43
43
|
|
|
44
44
|
## What Is Included
|
|
45
45
|
|
|
46
|
-
- `.user.3
|
|
46
|
+
- `.user.3` and decorated `.user.3.*` export.
|
|
47
47
|
- `JSON -> .user.3` packing.
|
|
48
48
|
- A reusable Python API through `REUser3Converter`.
|
|
49
49
|
- CLI commands through `pyreuser3`.
|
|
@@ -51,6 +51,8 @@ pip install pyreuser3
|
|
|
51
51
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
52
52
|
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
53
53
|
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
54
|
+
- il2cpp-validated readable codecs for `via.Int2`, `via.Uint2`, `via.Range`,
|
|
55
|
+
`via.RangeI`, and `via.Sphere`, with scalar/array round trips and raw fallback.
|
|
54
56
|
|
|
55
57
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
56
58
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
@@ -71,8 +73,8 @@ of Python scalar objects.
|
|
|
71
73
|
|
|
72
74
|
- Python 3.9 or newer.
|
|
73
75
|
- A RE_RSZ schema JSON file for the target game/version.
|
|
74
|
-
- An `il2cpp_dump.json` file when exporting readable enum labels.
|
|
75
|
-
- One or more unpacked `.user.3` files.
|
|
76
|
+
- An `il2cpp_dump.json` file when exporting readable enum labels and validated native structures.
|
|
77
|
+
- One or more unpacked `.user.3` or `.user.3.*` files.
|
|
76
78
|
|
|
77
79
|
`pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
|
|
78
80
|
|
|
@@ -16,6 +16,7 @@ pyreuser3/api.py
|
|
|
16
16
|
pyreuser3/cli.py
|
|
17
17
|
pyreuser3/core.py
|
|
18
18
|
pyreuser3/enum_codec.py
|
|
19
|
+
pyreuser3/native_structs.py
|
|
19
20
|
pyreuser3/rich_ui.py
|
|
20
21
|
pyreuser3/schema.py
|
|
21
22
|
pyreuser3/usr_container.py
|
|
@@ -45,7 +46,9 @@ pyreuser3/web/runners.py
|
|
|
45
46
|
pyreuser3/web/server.py
|
|
46
47
|
pyreuser3/web/settings.py
|
|
47
48
|
tests/test_api_cache.py
|
|
49
|
+
tests/test_discovery_and_web_depth.py
|
|
48
50
|
tests/test_enum_binary.py
|
|
49
51
|
tests/test_enum_codec.py
|
|
50
52
|
tests/test_export_enums.py
|
|
53
|
+
tests/test_native_structs.py
|
|
51
54
|
tests/test_usr_container.py
|
|
@@ -27,7 +27,7 @@ from pyreuser3 import REUser3Converter
|
|
|
27
27
|
|
|
28
28
|
## What Is Included
|
|
29
29
|
|
|
30
|
-
- `.user.3
|
|
30
|
+
- `.user.3` and decorated `.user.3.*` (for example `.user.3.X64`) export.
|
|
31
31
|
- `JSON -> .user.3` packing.
|
|
32
32
|
- A reusable Python API through `REUser3Converter`.
|
|
33
33
|
- CLI commands through `pyreuser3`.
|
|
@@ -41,8 +41,8 @@ or repository-specific helper scripts.
|
|
|
41
41
|
|
|
42
42
|
- Python 3.9 or newer.
|
|
43
43
|
- A RE_RSZ schema JSON file for the target game/version.
|
|
44
|
-
- An `il2cpp_dump.json` file when exporting readable enum labels.
|
|
45
|
-
- One or more unpacked `.user.3` files.
|
|
44
|
+
- An `il2cpp_dump.json` file when exporting readable enum labels and validated native structures.
|
|
45
|
+
- One or more unpacked `.user.3` or `.user.3.*` files.
|
|
46
46
|
|
|
47
47
|
Layout probing is the exception: `pyreuser3 probe` only inspects the USR/RSZ
|
|
48
48
|
container and does not require a schema or `il2cpp_dump.json`.
|
|
@@ -78,7 +78,9 @@ pyreuser3 pack \
|
|
|
78
78
|
-p <il2cpp_dump.json>
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
The `-p/--il2cpp-dump-path` option is required for export and optional for pack.
|
|
81
|
+
The `-p/--il2cpp-dump-path` option is required for export and optional for pack. Pass it
|
|
82
|
+
during pack when enum names or readable native structures need to be resolved back to
|
|
83
|
+
binary values. Legacy `{"raw": "..."}` structure values remain packable without it.
|
|
82
84
|
|
|
83
85
|
Probe a file or directory without loading game metadata:
|
|
84
86
|
|
|
@@ -99,7 +101,9 @@ Start the local `.user.3` export Web UI:
|
|
|
99
101
|
pyreuser3-web --port 8765
|
|
100
102
|
```
|
|
101
103
|
|
|
102
|
-
The Web UI
|
|
104
|
+
The Web UI handles `.user.3` and `.user.3.*` export. It does not pack files.
|
|
105
|
+
Its **Tree Depth** field accepts `auto` or a non-negative integer; an explicit
|
|
106
|
+
integer is passed through to readable reference-tree expansion.
|
|
103
107
|
|
|
104
108
|
## Python API
|
|
105
109
|
|
|
@@ -169,8 +173,10 @@ Use `json_format="readable"` for the same shape produced by `export_file()`. Thi
|
|
|
169
173
|
shape is read-only. Use `json_format="repack"` for the full document accepted by
|
|
170
174
|
`pack()`; the packer rejects readable JSON.
|
|
171
175
|
|
|
172
|
-
|
|
173
|
-
width
|
|
176
|
+
Readable enum fields are rendered as `[numeric] Name` labels using the enum's actual
|
|
177
|
+
storage width and signedness, so signed Fixed IDs match their in-game runtime values.
|
|
178
|
+
Repack documents retain canonical unsigned Fixed prefixes for stable machine editing.
|
|
179
|
+
Scalar flag enums are rendered as arrays of labels. ``ace.Bitset`1<T>`` values are
|
|
174
180
|
rendered as enum-index labels together with `_MaxElement` and `_WordCount`, so unknown
|
|
175
181
|
bits and padded word arrays remain reversible. Repack exports use
|
|
176
182
|
`re_user3_pack_v3`, which records the independently detected USR outer layout, RSZ
|
|
@@ -189,6 +195,16 @@ fixtures validate byte-for-byte repacking. V1 and v2 documents are recognized fo
|
|
|
189
195
|
diagnostics but must be re-exported as v3 before packing because they do not record
|
|
190
196
|
the required layout metadata.
|
|
191
197
|
|
|
198
|
+
Fixed-width RE Engine value types use a game-independent, declarative codec registry.
|
|
199
|
+
The first supported codecs are `via.Int2`, `via.Uint2`, `via.Range`, `via.RangeI`,
|
|
200
|
+
and `via.Sphere`, including scalar and array fields. A codec is enabled only when the
|
|
201
|
+
active il2cpp dump exactly validates the value type's parent, boxed size, member types,
|
|
202
|
+
and member offsets, and the schema independently agrees on the original type and payload
|
|
203
|
+
size. Matching values are exported as named JSON objects, for example
|
|
204
|
+
`{"x": 10, "y": 20}` or `{"pos": [1.0, 2.0, 3.0], "r": 4.0}`.
|
|
205
|
+
Any absent or mismatched signature fails closed to the existing raw representation;
|
|
206
|
+
legacy raw scalar and array JSON remains accepted for lossless repacking.
|
|
207
|
+
|
|
192
208
|
Modern files may use signed `-1` as an explicit null `Object` reference; repack
|
|
193
209
|
preserves that sentinel while continuing to reject every other missing instance
|
|
194
210
|
ID. Fixed-width arrays above one million elements are kept compactly as
|
|
@@ -10,7 +10,7 @@ pip install pyreuser3
|
|
|
10
10
|
|
|
11
11
|
## What Is Included
|
|
12
12
|
|
|
13
|
-
- `.user.3
|
|
13
|
+
- `.user.3` and decorated `.user.3.*` export.
|
|
14
14
|
- `JSON -> .user.3` packing.
|
|
15
15
|
- A reusable Python API through `REUser3Converter`.
|
|
16
16
|
- CLI commands through `pyreuser3`.
|
|
@@ -18,6 +18,8 @@ pip install pyreuser3
|
|
|
18
18
|
- A local `.user.3` export Web UI through `pyreuser3-web`.
|
|
19
19
|
- Automatic separation of the USR outer layout from the embedded RSZ header family;
|
|
20
20
|
modern RSZ v4+ files preserve their original numeric version during repack.
|
|
21
|
+
- il2cpp-validated readable codecs for `via.Int2`, `via.Uint2`, `via.Range`,
|
|
22
|
+
`via.RangeI`, and `via.Sphere`, with scalar/array round trips and raw fallback.
|
|
21
23
|
|
|
22
24
|
The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
|
|
23
25
|
`il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
|
|
@@ -38,8 +40,8 @@ of Python scalar objects.
|
|
|
38
40
|
|
|
39
41
|
- Python 3.9 or newer.
|
|
40
42
|
- A RE_RSZ schema JSON file for the target game/version.
|
|
41
|
-
- An `il2cpp_dump.json` file when exporting readable enum labels.
|
|
42
|
-
- One or more unpacked `.user.3` files.
|
|
43
|
+
- An `il2cpp_dump.json` file when exporting readable enum labels and validated native structures.
|
|
44
|
+
- One or more unpacked `.user.3` or `.user.3.*` files.
|
|
43
45
|
|
|
44
46
|
`pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
|
|
45
47
|
|
|
@@ -29,7 +29,7 @@ from pyreuser3 import REUser3Converter
|
|
|
29
29
|
|
|
30
30
|
PyREUser3 当前提供:
|
|
31
31
|
|
|
32
|
-
- `.user.3
|
|
32
|
+
- `.user.3` 以及 `.user.3.X64` 这类 `.user.3.*` 变体导出;
|
|
33
33
|
- `JSON -> .user.3` 封包;
|
|
34
34
|
- 面向其他项目调用的 `REUser3Converter` Python API;
|
|
35
35
|
- `pyreuser3` 命令行工具;
|
|
@@ -42,8 +42,8 @@ PyPI 包不会包含游戏资源、游戏 dump、RE_RSZ 模板、`il2cpp_dump.js
|
|
|
42
42
|
|
|
43
43
|
- Python 3.9 或更高版本;
|
|
44
44
|
- 与目标游戏和版本匹配的 RE_RSZ schema JSON;
|
|
45
|
-
-
|
|
46
|
-
- 一个或多个已解包的 `.user.3` 文件。
|
|
45
|
+
- 导出可读枚举标签和经验证的原生结构时需要 `il2cpp_dump.json`;
|
|
46
|
+
- 一个或多个已解包的 `.user.3` 或 `.user.3.*` 文件。
|
|
47
47
|
|
|
48
48
|
布局探测是例外:`pyreuser3 probe` 只检查 USR/RSZ 容器,不需要 schema 或
|
|
49
49
|
`il2cpp_dump.json`。
|
|
@@ -82,7 +82,8 @@ pyreuser3 pack \
|
|
|
82
82
|
说明:
|
|
83
83
|
|
|
84
84
|
- `export` 时 `-p/--il2cpp-dump-path` 是必填项,用于生成可读枚举标签;
|
|
85
|
-
- `pack` 时 `-p/--il2cpp-dump-path`
|
|
85
|
+
- `pack` 时 `-p/--il2cpp-dump-path` 是可选项;如果 JSON 中包含枚举名称或可读原生结构,
|
|
86
|
+
需要传入。旧版 `{"raw": "..."}` 结构值无需 il2cpp 仍可封回;
|
|
86
87
|
- `-s/--schema-path` 必须指向具体 schema JSON 文件,不应传目录;
|
|
87
88
|
- `-i`、`-j` 都可以传单个文件或目录,目录会递归处理。
|
|
88
89
|
|
|
@@ -117,6 +118,9 @@ pyreuser3 export \
|
|
|
117
118
|
pyreuser3-web --port 8765
|
|
118
119
|
```
|
|
119
120
|
|
|
121
|
+
Web 界面的“树深度”可填写 `auto` 或非负整数;显式整数会直接用于 readable
|
|
122
|
+
引用树展开。
|
|
123
|
+
|
|
120
124
|
默认地址:
|
|
121
125
|
|
|
122
126
|
```text
|
|
@@ -205,6 +209,13 @@ API 则把它们报告为结构化警告;repack 导出会把同一诊断写入
|
|
|
205
209
|
获得真实样本并完成逐字节回封验证后才能启用 repack。v1 和 v2 文档仍可识别以便诊断,但由于
|
|
206
210
|
缺少必要的布局元数据,封回前必须从源文件重新导出为 v3。
|
|
207
211
|
|
|
212
|
+
定长 RE Engine 值类型由与游戏无关的声明式 codec 注册表处理。首批支持
|
|
213
|
+
`via.Int2`、`via.Uint2`、`via.Range`、`via.RangeI` 和 `via.Sphere`,同时覆盖标量与数组。
|
|
214
|
+
只有当前 il2cpp dump 精确验证其值类型父类、boxed size、成员类型和成员偏移,并且 schema
|
|
215
|
+
中的原始类型与 payload size 也一致时,codec 才会启用。匹配的数据会导出为命名 JSON 对象,
|
|
216
|
+
例如 `{"x": 10, "y": 20}` 或 `{"pos": [1.0, 2.0, 3.0], "r": 4.0}`。
|
|
217
|
+
签名缺失或不匹配时会保守退回原有 raw 表示;旧版 raw 标量与数组 JSON 继续支持无损封回。
|
|
218
|
+
|
|
208
219
|
部分现代文件会用有符号 `-1` 明确表示空 `Object` 引用;repack 会原样保留这个哨兵,同时继续
|
|
209
220
|
拒绝其他不存在的实例 ID。超过一百万项的定长数组会紧凑保存为 `_raw_array_count` 和
|
|
210
221
|
`_raw_array_hex`。这种表示不提供逐项可读性,但能够无损回封、避免展开数百万个 Python 整数,
|
|
@@ -14,7 +14,12 @@ import threading
|
|
|
14
14
|
from pathlib import Path
|
|
15
15
|
from typing import Any, Callable, Literal, Optional
|
|
16
16
|
|
|
17
|
-
from .core import
|
|
17
|
+
from .core import (
|
|
18
|
+
RSZ_MAGIC,
|
|
19
|
+
USR_MAGIC,
|
|
20
|
+
discover_user3_files,
|
|
21
|
+
resolve_schema_path,
|
|
22
|
+
)
|
|
18
23
|
from .export import User3Exporter
|
|
19
24
|
from .pack import User3Packer
|
|
20
25
|
from .schema import TypeDB
|
|
@@ -547,14 +552,7 @@ class REUser3Converter:
|
|
|
547
552
|
Raises:
|
|
548
553
|
FileNotFoundError: A required file or directory was missing.
|
|
549
554
|
"""
|
|
550
|
-
|
|
551
|
-
return [user3_root]
|
|
552
|
-
if not user3_root.is_dir():
|
|
553
|
-
raise FileNotFoundError(f"user3 root not found: {user3_root}")
|
|
554
|
-
files = sorted(user3_root.rglob("*.user.3"))
|
|
555
|
-
if not files:
|
|
556
|
-
raise FileNotFoundError(f"no *.user.3 found under: {user3_root}")
|
|
557
|
-
return files
|
|
555
|
+
return discover_user3_files(user3_root)
|
|
558
556
|
|
|
559
557
|
@staticmethod
|
|
560
558
|
def _run_callback(
|
|
@@ -105,7 +105,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
105
105
|
"--input-dir",
|
|
106
106
|
"-i",
|
|
107
107
|
required=True,
|
|
108
|
-
help="Root directory or single .user.3 file to export.",
|
|
108
|
+
help="Root directory or single .user.3 / .user.3.* file to export.",
|
|
109
109
|
)
|
|
110
110
|
export_parser.add_argument(
|
|
111
111
|
"--schema-path",
|
|
@@ -139,7 +139,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
139
139
|
"--il2cpp-dump-path",
|
|
140
140
|
"-p",
|
|
141
141
|
required=True,
|
|
142
|
-
help=
|
|
142
|
+
help=(
|
|
143
|
+
"Path to il2cpp_dump.json, used for enum labels and validated "
|
|
144
|
+
"native structures."
|
|
145
|
+
),
|
|
143
146
|
)
|
|
144
147
|
export_parser.add_argument(
|
|
145
148
|
"--json-format",
|
|
@@ -178,7 +181,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
178
181
|
"--il2cpp-dump-path",
|
|
179
182
|
"-p",
|
|
180
183
|
default="",
|
|
181
|
-
help=
|
|
184
|
+
help=(
|
|
185
|
+
"Optional il2cpp_dump.json path, used for enum lookup and readable "
|
|
186
|
+
"native structures."
|
|
187
|
+
),
|
|
182
188
|
)
|
|
183
189
|
pack_parser.add_argument(
|
|
184
190
|
"--exclude-regex",
|
|
@@ -198,7 +204,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
198
204
|
"--input-dir",
|
|
199
205
|
"-i",
|
|
200
206
|
required=True,
|
|
201
|
-
help="Root directory or single .user.3 file to inspect.",
|
|
207
|
+
help="Root directory or single .user.3 / .user.3.* file to inspect.",
|
|
202
208
|
)
|
|
203
209
|
probe_parser.add_argument(
|
|
204
210
|
"--json-report",
|
|
@@ -32,6 +32,7 @@ HEX32_RE = re.compile(r"^[0-9a-fA-F]{32}$")
|
|
|
32
32
|
# Register enum values through the shared lookup tables so readable labels and numeric
|
|
33
33
|
# packing stay reversible.
|
|
34
34
|
ENUM_UNUSED_KEY = "value__"
|
|
35
|
+
USER3_NAME_MARKER = ".user.3"
|
|
35
36
|
|
|
36
37
|
ENUM_UNDERLYING_TYPE_MAP = {
|
|
37
38
|
"byte": "U8",
|
|
@@ -76,6 +77,48 @@ class ParseError(RuntimeError):
|
|
|
76
77
|
pass
|
|
77
78
|
|
|
78
79
|
|
|
80
|
+
def is_user3_source_path(path: str | Path) -> bool:
|
|
81
|
+
"""Return whether a filename is a binary ``.user.3`` source variant.
|
|
82
|
+
|
|
83
|
+
RE Engine resources may append platform or build qualifiers after the normal
|
|
84
|
+
suffix, for example ``foo.user.3.X64``. Generated JSON files deliberately do
|
|
85
|
+
not qualify, even though their names retain the source filename.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
name = Path(path).name.casefold()
|
|
89
|
+
marker_index = name.rfind(USER3_NAME_MARKER)
|
|
90
|
+
if marker_index < 0 or name.endswith(".json"):
|
|
91
|
+
return False
|
|
92
|
+
tail = name[marker_index + len(USER3_NAME_MARKER) :]
|
|
93
|
+
return not tail or tail.startswith(".")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def discover_user3_files(root: str | Path) -> list[Path]:
|
|
97
|
+
"""Discover standard and decorated ``.user.3`` files below a root path.
|
|
98
|
+
|
|
99
|
+
An explicitly supplied file is preserved for backward compatibility. Directory
|
|
100
|
+
discovery accepts both ``*.user.3`` and ``*.user.3.*`` (case-insensitively), while
|
|
101
|
+
excluding generated ``*.json`` export and pack documents.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
source_root = Path(root)
|
|
105
|
+
if source_root.is_file():
|
|
106
|
+
return [source_root]
|
|
107
|
+
if not source_root.is_dir():
|
|
108
|
+
raise FileNotFoundError(f"user3 root not found: {source_root}")
|
|
109
|
+
|
|
110
|
+
files = sorted(
|
|
111
|
+
path
|
|
112
|
+
for path in source_root.rglob("*")
|
|
113
|
+
if path.is_file() and is_user3_source_path(path)
|
|
114
|
+
)
|
|
115
|
+
if not files:
|
|
116
|
+
raise FileNotFoundError(
|
|
117
|
+
f"no .user.3 or .user.3.* files found under: {source_root}"
|
|
118
|
+
)
|
|
119
|
+
return files
|
|
120
|
+
|
|
121
|
+
|
|
79
122
|
def align(value: int, alignment: int) -> int:
|
|
80
123
|
"""Round an offset up to the requested byte alignment.
|
|
81
124
|
|
|
@@ -16,7 +16,12 @@ from .metadata import ExporterMetadataMixin
|
|
|
16
16
|
from .postprocess import ExporterPostprocessMixin
|
|
17
17
|
from .tree import ExporterTreeMixin
|
|
18
18
|
from .user3 import ExporterUser3ParserMixin
|
|
19
|
-
from ..core import
|
|
19
|
+
from ..core import (
|
|
20
|
+
RSZ_MAGIC,
|
|
21
|
+
USR_MAGIC,
|
|
22
|
+
discover_user3_files,
|
|
23
|
+
resolve_schema_path,
|
|
24
|
+
)
|
|
20
25
|
from ..rich_ui import BatchProgress
|
|
21
26
|
from ..schema import TypeDB
|
|
22
27
|
|
|
@@ -106,6 +111,7 @@ class User3Exporter(
|
|
|
106
111
|
self.bitset_rules: dict[str, str] = {}
|
|
107
112
|
self.param_type_default_enum: dict[str, str] = {}
|
|
108
113
|
self.enum_underlying_types: dict[str, str] = {}
|
|
114
|
+
self.native_struct_layouts: dict[str, dict] = {}
|
|
109
115
|
self.enum_flags: set[str] = set()
|
|
110
116
|
self.enum_member_to_types: dict[str, list[str]] = {}
|
|
111
117
|
self._pending_enum_context: dict | None = None
|
|
@@ -139,7 +145,7 @@ class User3Exporter(
|
|
|
139
145
|
with BatchProgress(
|
|
140
146
|
"Exporting user3", total=len(files), unit="file"
|
|
141
147
|
) as progress:
|
|
142
|
-
progress.log(f"Found {len(files)}
|
|
148
|
+
progress.log(f"Found {len(files)} user3 source file(s).")
|
|
143
149
|
progress.log(f"Schema: {self.schema_path}")
|
|
144
150
|
progress.log(f"Output directory: {self.output_root}")
|
|
145
151
|
for user3_file in files:
|
|
@@ -237,14 +243,7 @@ class User3Exporter(
|
|
|
237
243
|
Raises:
|
|
238
244
|
FileNotFoundError: A required file or directory was missing.
|
|
239
245
|
"""
|
|
240
|
-
|
|
241
|
-
files = [self.user3_root]
|
|
242
|
-
else:
|
|
243
|
-
if not self.user3_root.is_dir():
|
|
244
|
-
raise FileNotFoundError(f"user3 root not found: {self.user3_root}")
|
|
245
|
-
files = sorted(self.user3_root.rglob("*.user.3"))
|
|
246
|
-
if not files:
|
|
247
|
-
raise FileNotFoundError(f"no *.user.3 found under: {self.user3_root}")
|
|
246
|
+
files = discover_user3_files(self.user3_root)
|
|
248
247
|
if not self._exclude_patterns:
|
|
249
248
|
return files
|
|
250
249
|
|
|
@@ -260,7 +259,9 @@ class User3Exporter(
|
|
|
260
259
|
continue
|
|
261
260
|
kept.append(file_path)
|
|
262
261
|
if not kept:
|
|
263
|
-
raise FileNotFoundError(
|
|
262
|
+
raise FileNotFoundError(
|
|
263
|
+
"all .user.3 and .user.3.* files were excluded by regex filters"
|
|
264
|
+
)
|
|
264
265
|
return kept
|
|
265
266
|
|
|
266
267
|
def _output_path_for(self, user3_file: Path) -> Path:
|
|
@@ -13,11 +13,15 @@ from pathlib import Path
|
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
15
|
from ..core import ENUM_UNUSED_KEY, normalize_enum_storage_type
|
|
16
|
+
from ..native_structs import collect_validated_native_struct_layout
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
_FIXED_ENUM_RE = re.compile(r"[A-Za-z0-9_.+]+_Fixed")
|
|
19
20
|
_PROPERTY_METHOD_RE = re.compile(r"^(?:get|set)_(?P<name>.+?)(?:\d+)?$")
|
|
20
21
|
_BACKING_FIELD_RE = re.compile(r"^<(?P<name>[^>]+)>")
|
|
22
|
+
_INTEGER_STORAGE_TYPES = frozenset(
|
|
23
|
+
{"S8", "U8", "S16", "U16", "S32", "U32", "S64", "U64"}
|
|
24
|
+
)
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
class ExporterEnumSourceMixin:
|
|
@@ -104,6 +108,39 @@ class ExporterEnumSourceMixin:
|
|
|
104
108
|
for candidate in cls._field_name_variants(field_name):
|
|
105
109
|
field_map.setdefault(candidate, fixed_type)
|
|
106
110
|
|
|
111
|
+
@classmethod
|
|
112
|
+
def _add_fixed_backing_candidate(
|
|
113
|
+
cls,
|
|
114
|
+
candidates: dict[str, set[str]],
|
|
115
|
+
field_name: Any,
|
|
116
|
+
type_name: Any,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""Record an integer ``*_Fixed`` backing field by its runtime property name."""
|
|
119
|
+
if not isinstance(field_name, str):
|
|
120
|
+
return
|
|
121
|
+
storage_type = normalize_enum_storage_type(type_name)
|
|
122
|
+
if storage_type not in _INTEGER_STORAGE_TYPES:
|
|
123
|
+
return
|
|
124
|
+
for field_variant in cls._field_name_variants(field_name):
|
|
125
|
+
normalized_name = field_variant.lstrip("_")
|
|
126
|
+
if not normalized_name.endswith("_Fixed"):
|
|
127
|
+
continue
|
|
128
|
+
property_name = normalized_name[: -len("_Fixed")]
|
|
129
|
+
if property_name:
|
|
130
|
+
candidates.setdefault(property_name, set()).add(field_variant)
|
|
131
|
+
candidates.setdefault(normalized_name, set()).add(field_variant)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def _method_return_type(method: Any) -> str | None:
|
|
135
|
+
"""Return one concrete method return type when metadata exposes it."""
|
|
136
|
+
if not isinstance(method, dict):
|
|
137
|
+
return None
|
|
138
|
+
returns = method.get("returns")
|
|
139
|
+
if not isinstance(returns, dict):
|
|
140
|
+
return None
|
|
141
|
+
return_type = returns.get("type")
|
|
142
|
+
return return_type if isinstance(return_type, str) and return_type else None
|
|
143
|
+
|
|
107
144
|
@staticmethod
|
|
108
145
|
def _method_matches_property(method_name: str, getter_or_setter: str) -> bool:
|
|
109
146
|
"""Return whether an il2cpp method key represents the named property accessor."""
|
|
@@ -262,8 +299,10 @@ class ExporterEnumSourceMixin:
|
|
|
262
299
|
"bitset_rules": {},
|
|
263
300
|
"generic_candidates": {},
|
|
264
301
|
"enum_underlying_types": {},
|
|
302
|
+
"native_struct_layouts": {},
|
|
265
303
|
"enum_types": set(),
|
|
266
304
|
"serializable_fallback_candidates": [],
|
|
305
|
+
"fixed_backing_candidates": [],
|
|
267
306
|
}
|
|
268
307
|
|
|
269
308
|
@classmethod
|
|
@@ -274,6 +313,11 @@ class ExporterEnumSourceMixin:
|
|
|
274
313
|
if not isinstance(class_name, str) or not isinstance(obj, dict):
|
|
275
314
|
return
|
|
276
315
|
|
|
316
|
+
native_layout = collect_validated_native_struct_layout(class_name, obj)
|
|
317
|
+
if native_layout is not None:
|
|
318
|
+
type_name, descriptor = native_layout
|
|
319
|
+
state["native_struct_layouts"][type_name] = descriptor
|
|
320
|
+
|
|
277
321
|
if obj.get("parent") == "System.Enum":
|
|
278
322
|
state["enum_types"].add(class_name)
|
|
279
323
|
if obj.get("parent") == "System.Enum":
|
|
@@ -282,6 +326,7 @@ class ExporterEnumSourceMixin:
|
|
|
282
326
|
state["enum_underlying_types"][class_name] = storage_type
|
|
283
327
|
|
|
284
328
|
field_map: dict[str, str] = {}
|
|
329
|
+
fixed_backing_fields: dict[str, set[str]] = {}
|
|
285
330
|
fields_obj = obj.get("fields")
|
|
286
331
|
if isinstance(fields_obj, dict):
|
|
287
332
|
for field_name, field_info in fields_obj.items():
|
|
@@ -290,6 +335,9 @@ class ExporterEnumSourceMixin:
|
|
|
290
335
|
cls._add_field_fixed_type(
|
|
291
336
|
field_map, field_name, field_info.get("type")
|
|
292
337
|
)
|
|
338
|
+
cls._add_fixed_backing_candidate(
|
|
339
|
+
fixed_backing_fields, field_name, field_info.get("type")
|
|
340
|
+
)
|
|
293
341
|
|
|
294
342
|
# Older dumps expose RE_RSZ field hints through an explicit RSZ array.
|
|
295
343
|
rsz_fields = obj.get("RSZ")
|
|
@@ -303,6 +351,9 @@ class ExporterEnumSourceMixin:
|
|
|
303
351
|
cls._add_field_fixed_type(
|
|
304
352
|
field_map, potential_name, rsz_field.get("type")
|
|
305
353
|
)
|
|
354
|
+
cls._add_fixed_backing_candidate(
|
|
355
|
+
fixed_backing_fields, potential_name, rsz_field.get("type")
|
|
356
|
+
)
|
|
306
357
|
|
|
307
358
|
# Newer dumps can omit RSZ entirely but keep property/type hints here.
|
|
308
359
|
reflection_props = obj.get("reflection_properties")
|
|
@@ -311,8 +362,12 @@ class ExporterEnumSourceMixin:
|
|
|
311
362
|
if not isinstance(prop_name, str) or not isinstance(prop_info, dict):
|
|
312
363
|
continue
|
|
313
364
|
cls._add_field_fixed_type(field_map, prop_name, prop_info.get("type"))
|
|
365
|
+
cls._add_fixed_backing_candidate(
|
|
366
|
+
fixed_backing_fields, prop_name, prop_info.get("type")
|
|
367
|
+
)
|
|
314
368
|
|
|
315
369
|
methods_obj = obj.get("methods")
|
|
370
|
+
runtime_property_types: dict[str, set[str]] = {}
|
|
316
371
|
if isinstance(methods_obj, dict):
|
|
317
372
|
properties_obj = obj.get("properties")
|
|
318
373
|
if isinstance(properties_obj, dict):
|
|
@@ -326,6 +381,11 @@ class ExporterEnumSourceMixin:
|
|
|
326
381
|
continue
|
|
327
382
|
if not cls._method_matches_property(method_name, getter):
|
|
328
383
|
continue
|
|
384
|
+
return_type = cls._method_return_type(method)
|
|
385
|
+
if return_type is not None:
|
|
386
|
+
runtime_property_types.setdefault(
|
|
387
|
+
prop_name, set()
|
|
388
|
+
).add(return_type)
|
|
329
389
|
fixed_type = cls._fixed_type_from_method_return(method)
|
|
330
390
|
if fixed_type is not None:
|
|
331
391
|
cls._add_field_fixed_type(
|
|
@@ -350,6 +410,12 @@ class ExporterEnumSourceMixin:
|
|
|
350
410
|
method_prop_name = cls._property_name_from_method(method_name)
|
|
351
411
|
if method_prop_name is None:
|
|
352
412
|
continue
|
|
413
|
+
if method_name.startswith("get_"):
|
|
414
|
+
return_type = cls._method_return_type(method)
|
|
415
|
+
if return_type is not None:
|
|
416
|
+
runtime_property_types.setdefault(
|
|
417
|
+
method_prop_name, set()
|
|
418
|
+
).add(return_type)
|
|
353
419
|
fixed_type = None
|
|
354
420
|
if method_name.startswith("get_"):
|
|
355
421
|
fixed_type = cls._fixed_type_from_method_return(method)
|
|
@@ -358,6 +424,18 @@ class ExporterEnumSourceMixin:
|
|
|
358
424
|
if fixed_type is not None:
|
|
359
425
|
cls._add_field_fixed_type(field_map, method_prop_name, fixed_type)
|
|
360
426
|
|
|
427
|
+
for property_name, field_names in fixed_backing_fields.items():
|
|
428
|
+
runtime_types = runtime_property_types.get(property_name, set())
|
|
429
|
+
if len(runtime_types) != 1:
|
|
430
|
+
continue
|
|
431
|
+
state["fixed_backing_candidates"].append(
|
|
432
|
+
(
|
|
433
|
+
class_name,
|
|
434
|
+
tuple(sorted(field_names)),
|
|
435
|
+
next(iter(runtime_types)),
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
361
439
|
if field_map:
|
|
362
440
|
state["class_field_fixed_types"][class_name] = field_map
|
|
363
441
|
|
|
@@ -411,6 +489,20 @@ class ExporterEnumSourceMixin:
|
|
|
411
489
|
if len(fallback_types) == 1:
|
|
412
490
|
serializable_to_fixed[class_name] = fallback_types[0]
|
|
413
491
|
|
|
492
|
+
for class_name, field_names, runtime_type in state[
|
|
493
|
+
"fixed_backing_candidates"
|
|
494
|
+
]:
|
|
495
|
+
fixed_type = (
|
|
496
|
+
runtime_type
|
|
497
|
+
if runtime_type.endswith("_Fixed")
|
|
498
|
+
else f"{runtime_type}_Fixed"
|
|
499
|
+
)
|
|
500
|
+
if fixed_type not in enum_types:
|
|
501
|
+
continue
|
|
502
|
+
field_map = state["class_field_fixed_types"].setdefault(class_name, {})
|
|
503
|
+
for field_name in field_names:
|
|
504
|
+
field_map.setdefault(field_name, fixed_type)
|
|
505
|
+
|
|
414
506
|
for class_name, raw_types in state["generic_candidates"].items():
|
|
415
507
|
enum_args = list(dict.fromkeys(t for t in raw_types if t in enum_types))
|
|
416
508
|
param_args = list(dict.fromkeys(t for t in raw_types if t not in enum_types))
|
|
@@ -436,6 +528,7 @@ class ExporterEnumSourceMixin:
|
|
|
436
528
|
"generic_scalar_rules": state["generic_scalar_rules"],
|
|
437
529
|
"bitset_rules": state["bitset_rules"],
|
|
438
530
|
"enum_underlying_types": state["enum_underlying_types"],
|
|
531
|
+
"native_struct_layouts": state["native_struct_layouts"],
|
|
439
532
|
}
|
|
440
533
|
|
|
441
534
|
@classmethod
|
|
@@ -18,6 +18,11 @@ from ..core import (
|
|
|
18
18
|
enum_storage_size,
|
|
19
19
|
enum_storage_type_from_size,
|
|
20
20
|
)
|
|
21
|
+
from ..native_structs import (
|
|
22
|
+
NativeStructValueError,
|
|
23
|
+
decode_native_struct,
|
|
24
|
+
resolve_native_struct_codec,
|
|
25
|
+
)
|
|
21
26
|
from ..schema import ClassDef, FieldDef
|
|
22
27
|
|
|
23
28
|
|
|
@@ -141,6 +146,18 @@ class ExporterFieldParserMixin:
|
|
|
141
146
|
count = max(field.size // 4, 1)
|
|
142
147
|
return [reader.read_f32() for _ in range(count)]
|
|
143
148
|
|
|
149
|
+
codec = resolve_native_struct_codec(
|
|
150
|
+
field, getattr(self, "native_struct_layouts", {})
|
|
151
|
+
)
|
|
152
|
+
if codec is not None and reader.size - reader.tell() >= codec.payload_size:
|
|
153
|
+
payload = reader.read(codec.payload_size)
|
|
154
|
+
try:
|
|
155
|
+
return decode_native_struct(codec, payload)
|
|
156
|
+
except NativeStructValueError:
|
|
157
|
+
# Non-finite float payloads and any future non-lossless cases stay raw
|
|
158
|
+
# so a JSON round trip cannot silently alter their bit patterns.
|
|
159
|
+
return {"raw": payload.hex(), "type": t}
|
|
160
|
+
|
|
144
161
|
if field.size <= 0:
|
|
145
162
|
return None
|
|
146
163
|
to_read = max(0, min(field.size, reader.size - reader.tell()))
|