PyREUser3 0.8.0__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.8.0 → pyreuser3-0.8.1}/PKG-INFO +6 -4
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/PKG-INFO +6 -4
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/SOURCES.txt +3 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/README.md +19 -5
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/docs/PYPI.md +5 -3
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/docs/README.zh-CN.md +15 -4
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyproject.toml +1 -1
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/__init__.py +1 -1
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/api.py +7 -9
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/cli.py +10 -4
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/core.py +43 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/base.py +12 -11
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/enums.py +8 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/fields.py +17 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/metadata.py +6 -0
- pyreuser3-0.8.1/pyreuser3/native_structs.py +368 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/base.py +27 -5
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/models.py +8 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/values.py +65 -1
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/writer.py +27 -1
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/usr_container.py +5 -9
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/page.py +13 -13
- {pyreuser3-0.8.0 → 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.8.1/tests/test_native_structs.py +339 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/LICENSE +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/MANIFEST.in +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/enum_codec.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/postprocess.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/export/user3.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/container.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/pack/plan.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/usr_layouts.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/setup.cfg +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/tests/test_api_cache.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/tests/test_enum_binary.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/tests/test_enum_codec.py +0 -0
- {pyreuser3-0.8.0 → pyreuser3-0.8.1}/tests/test_export_enums.py +0 -0
- {pyreuser3-0.8.0 → 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.8.
|
|
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.8.
|
|
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
|
|
|
@@ -191,6 +195,16 @@ fixtures validate byte-for-byte repacking. V1 and v2 documents are recognized fo
|
|
|
191
195
|
diagnostics but must be re-exported as v3 before packing because they do not record
|
|
192
196
|
the required layout metadata.
|
|
193
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
|
+
|
|
194
208
|
Modern files may use signed `-1` as an explicit null `Object` reference; repack
|
|
195
209
|
preserves that sentinel while continuing to reject every other missing instance
|
|
196
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,6 +13,7 @@ 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")
|
|
@@ -298,6 +299,7 @@ class ExporterEnumSourceMixin:
|
|
|
298
299
|
"bitset_rules": {},
|
|
299
300
|
"generic_candidates": {},
|
|
300
301
|
"enum_underlying_types": {},
|
|
302
|
+
"native_struct_layouts": {},
|
|
301
303
|
"enum_types": set(),
|
|
302
304
|
"serializable_fallback_candidates": [],
|
|
303
305
|
"fixed_backing_candidates": [],
|
|
@@ -311,6 +313,11 @@ class ExporterEnumSourceMixin:
|
|
|
311
313
|
if not isinstance(class_name, str) or not isinstance(obj, dict):
|
|
312
314
|
return
|
|
313
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
|
+
|
|
314
321
|
if obj.get("parent") == "System.Enum":
|
|
315
322
|
state["enum_types"].add(class_name)
|
|
316
323
|
if obj.get("parent") == "System.Enum":
|
|
@@ -521,6 +528,7 @@ class ExporterEnumSourceMixin:
|
|
|
521
528
|
"generic_scalar_rules": state["generic_scalar_rules"],
|
|
522
529
|
"bitset_rules": state["bitset_rules"],
|
|
523
530
|
"enum_underlying_types": state["enum_underlying_types"],
|
|
531
|
+
"native_struct_layouts": state["native_struct_layouts"],
|
|
524
532
|
}
|
|
525
533
|
|
|
526
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()))
|
|
@@ -12,6 +12,7 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from ..core import ParseError, enum_storage_type_from_size
|
|
14
14
|
from ..enum_codec import enum_member_for_value, is_probable_flags_enum
|
|
15
|
+
from ..native_structs import normalize_native_struct_layouts
|
|
15
16
|
from ..rich_ui import get_console
|
|
16
17
|
|
|
17
18
|
|
|
@@ -242,8 +243,13 @@ class ExporterMetadataMixin:
|
|
|
242
243
|
self.bitset_rules = {}
|
|
243
244
|
self.param_type_default_enum = {}
|
|
244
245
|
self.enum_underlying_types = {}
|
|
246
|
+
self.native_struct_layouts = {}
|
|
245
247
|
self.enum_flags = set()
|
|
246
248
|
|
|
249
|
+
self.native_struct_layouts = normalize_native_struct_layouts(
|
|
250
|
+
raw.get("native_struct_layouts")
|
|
251
|
+
)
|
|
252
|
+
|
|
247
253
|
class_field_fixed_types = raw.get("class_field_fixed_types")
|
|
248
254
|
if isinstance(class_field_fixed_types, dict):
|
|
249
255
|
for cls_name, field_map in class_field_fixed_types.items():
|