PyREUser3 0.7.0__tar.gz → 0.7.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.
Files changed (51) hide show
  1. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PKG-INFO +18 -1
  2. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/PKG-INFO +18 -1
  3. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/SOURCES.txt +1 -0
  4. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/README.md +83 -4
  5. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/docs/PYPI.md +17 -0
  6. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/docs/README.zh-CN.md +70 -3
  7. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyproject.toml +1 -1
  8. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/__init__.py +7 -1
  9. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/api.py +120 -14
  10. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/cli.py +52 -0
  11. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/base.py +18 -6
  12. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/fields.py +79 -4
  13. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/metadata.py +13 -7
  14. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/user3.py +43 -1
  15. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/base.py +15 -2
  16. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/models.py +8 -0
  17. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/plan.py +32 -0
  18. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/writer.py +90 -5
  19. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/usr_container.py +307 -27
  20. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/usr_layouts.py +50 -3
  21. pyreuser3-0.7.1/tests/test_api_cache.py +120 -0
  22. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_usr_container.py +405 -2
  23. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/LICENSE +0 -0
  24. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/MANIFEST.in +0 -0
  25. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/dependency_links.txt +0 -0
  26. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/entry_points.txt +0 -0
  27. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/requires.txt +0 -0
  28. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/top_level.txt +0 -0
  29. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/__main__.py +0 -0
  30. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/core.py +0 -0
  31. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/enum_codec.py +0 -0
  32. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/__init__.py +0 -0
  33. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/enums.py +0 -0
  34. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/postprocess.py +0 -0
  35. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/tree.py +0 -0
  36. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/__init__.py +0 -0
  37. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/rich_ui.py +0 -0
  38. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/schema.py +0 -0
  39. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/__init__.py +0 -0
  40. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/__main__.py +0 -0
  41. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/handler.py +0 -0
  42. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/jobs.py +0 -0
  43. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/page.py +0 -0
  44. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/picker.py +0 -0
  45. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/runners.py +0 -0
  46. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/server.py +0 -0
  47. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/settings.py +0 -0
  48. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/setup.cfg +0 -0
  49. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_enum_binary.py +0 -0
  50. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_enum_codec.py +0 -0
  51. {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_export_enums.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.7.0
3
+ Version: 0.7.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
@@ -47,6 +47,7 @@ pip install pyreuser3
47
47
  - `JSON -> .user.3` packing.
48
48
  - A reusable Python API through `REUser3Converter`.
49
49
  - CLI commands through `pyreuser3`.
50
+ - Schema-free layout probing through `pyreuser3 probe` and the public probe API.
50
51
  - A local `.user.3` export Web UI through `pyreuser3-web`.
51
52
  - Automatic separation of the USR outer layout from the embedded RSZ header family;
52
53
  modern RSZ v4+ files preserve their original numeric version during repack.
@@ -59,6 +60,20 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
59
60
  RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
60
61
  until real fixtures provide byte-for-byte validation.
61
62
 
63
+ Version 0.7.1 fixes the 0.7.0 modern RSZ alignment regression. Modern userdata and
64
+ data targets use absolute 16-byte file alignment even though their stored offsets
65
+ remain RSZ-relative. Readable parsing accepts alignment-only deviations without
66
+ hiding structural corruption, while the probe API reports them explicitly. Repack
67
+ output blocks layouts carrying unverified diagnostics. The strict schema-free
68
+ probe can validate large local corpora.
69
+
70
+ Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
71
+ with automatic file-signature invalidation and an explicit
72
+ `clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
73
+ packer. Signed `-1` null object references and very large fixed-width arrays are
74
+ preserved losslessly; large arrays use a compact raw payload instead of millions
75
+ of Python scalar objects.
76
+
62
77
  ## Requirements
63
78
 
64
79
  - Python 3.9 or newer.
@@ -66,6 +81,8 @@ until real fixtures provide byte-for-byte validation.
66
81
  - An `il2cpp_dump.json` file when exporting readable enum labels.
67
82
  - One or more unpacked `.user.3` files.
68
83
 
84
+ `pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
85
+
69
86
  ## Usage
70
87
 
71
88
  Usage details may change as the package evolves. For the latest command-line and Python API examples, read the GitHub README:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.7.0
3
+ Version: 0.7.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
@@ -47,6 +47,7 @@ pip install pyreuser3
47
47
  - `JSON -> .user.3` packing.
48
48
  - A reusable Python API through `REUser3Converter`.
49
49
  - CLI commands through `pyreuser3`.
50
+ - Schema-free layout probing through `pyreuser3 probe` and the public probe API.
50
51
  - A local `.user.3` export Web UI through `pyreuser3-web`.
51
52
  - Automatic separation of the USR outer layout from the embedded RSZ header family;
52
53
  modern RSZ v4+ files preserve their original numeric version during repack.
@@ -59,6 +60,20 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
59
60
  RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
60
61
  until real fixtures provide byte-for-byte validation.
61
62
 
63
+ Version 0.7.1 fixes the 0.7.0 modern RSZ alignment regression. Modern userdata and
64
+ data targets use absolute 16-byte file alignment even though their stored offsets
65
+ remain RSZ-relative. Readable parsing accepts alignment-only deviations without
66
+ hiding structural corruption, while the probe API reports them explicitly. Repack
67
+ output blocks layouts carrying unverified diagnostics. The strict schema-free
68
+ probe can validate large local corpora.
69
+
70
+ Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
71
+ with automatic file-signature invalidation and an explicit
72
+ `clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
73
+ packer. Signed `-1` null object references and very large fixed-width arrays are
74
+ preserved losslessly; large arrays use a compact raw payload instead of millions
75
+ of Python scalar objects.
76
+
62
77
  ## Requirements
63
78
 
64
79
  - Python 3.9 or newer.
@@ -66,6 +81,8 @@ until real fixtures provide byte-for-byte validation.
66
81
  - An `il2cpp_dump.json` file when exporting readable enum labels.
67
82
  - One or more unpacked `.user.3` files.
68
83
 
84
+ `pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
85
+
69
86
  ## Usage
70
87
 
71
88
  Usage details may change as the package evolves. For the latest command-line and Python API examples, read the GitHub README:
@@ -42,6 +42,7 @@ pyreuser3/web/picker.py
42
42
  pyreuser3/web/runners.py
43
43
  pyreuser3/web/server.py
44
44
  pyreuser3/web/settings.py
45
+ tests/test_api_cache.py
45
46
  tests/test_enum_binary.py
46
47
  tests/test_enum_codec.py
47
48
  tests/test_export_enums.py
@@ -31,6 +31,7 @@ from pyreuser3 import REUser3Converter
31
31
  - `JSON -> .user.3` packing.
32
32
  - A reusable Python API through `REUser3Converter`.
33
33
  - CLI commands through `pyreuser3`.
34
+ - Schema-free layout probing for individual files and whole corpora.
34
35
  - A local `.user.3` export Web UI through `pyreuser3-web`.
35
36
 
36
37
  This PyPI package intentionally does not include game resources, dumped game data, RE_RSZ templates, `il2cpp_dump.json`,
@@ -43,6 +44,9 @@ or repository-specific helper scripts.
43
44
  - An `il2cpp_dump.json` file when exporting readable enum labels.
44
45
  - One or more unpacked `.user.3` files.
45
46
 
47
+ Layout probing is the exception: `pyreuser3 probe` only inspects the USR/RSZ
48
+ container and does not require a schema or `il2cpp_dump.json`.
49
+
46
50
  ## Command Line
47
51
 
48
52
  Export `.user.3` files to JSON:
@@ -76,6 +80,19 @@ pyreuser3 pack \
76
80
 
77
81
  The `-p/--il2cpp-dump-path` option is required for export and optional for pack. Passing it during pack is recommended when enum names need to be resolved back to numeric values.
78
82
 
83
+ Probe a file or directory without loading game metadata:
84
+
85
+ ```bash
86
+ pyreuser3 probe -i <input-user3-file-or-directory>
87
+ pyreuser3 probe -i <input-user3-file-or-directory> --strict -o layout-report.json
88
+ ```
89
+
90
+ The default probe uses the same safety policy as readable export: structural
91
+ corruption remains fatal, while non-canonical alignment is reported as a warning.
92
+ `--strict` rejects every layout deviation and is intended for corpus validation.
93
+ The optional JSON report contains totals grouped by detected layout, RSZ version,
94
+ and diagnostic code.
95
+
79
96
  Start the local `.user.3` export Web UI:
80
97
 
81
98
  ```bash
@@ -111,6 +128,29 @@ converter.pack_file(
111
128
  )
112
129
  ```
113
130
 
131
+ `REUser3Converter` loads schema and il2cpp metadata lazily and caches it for the
132
+ lifetime of that converter. Repeated readable, repack, pack, and patch calls no
133
+ longer rescan large metadata files. Cache entries are invalidated automatically
134
+ when the source path, size, or nanosecond modification time changes; callers that
135
+ replace metadata in an unusual way can force a reload with:
136
+
137
+ ```python
138
+ converter.clear_metadata_cache()
139
+ ```
140
+
141
+ `patch_directory()` also reuses one prepared exporter and packer for the complete
142
+ batch instead of constructing them once per file.
143
+
144
+ Container layout probing is also available without constructing a converter or
145
+ loading a schema:
146
+
147
+ ```python
148
+ from pyreuser3 import probe_usr_file, probe_usr_path
149
+
150
+ one_file = probe_usr_file("input/example.user.3")
151
+ whole_tree = probe_usr_path("input/natives", policy="strict_probe")
152
+ ```
153
+
114
154
  Convert a `.user.3` file to an in-memory JSON-compatible Python object without writing a JSON file:
115
155
 
116
156
  ```python
@@ -139,10 +179,22 @@ dependency tables. Layout candidates and their read/repack capability status are
139
179
  declared in `pyreuser3/usr_layouts.py`; RSZ field definitions still come from the
140
180
  supplied REFramework-compatible schema. The verified modern header family accepts
141
181
  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.
182
+ forcing MHWS version 16. For this family, offset values remain relative to the RSZ
183
+ section, but the userdata and data targets are aligned to absolute 16-byte file
184
+ positions. Readable export accepts alignment-only deviations, and the probe API
185
+ reports them as structured warnings. Repack export records the same diagnostics in
186
+ `_warnings` and blocks packing through `_unsupported` until the layout is verified.
187
+ Experimental physical H28 and legacy RSZ v3 candidates are read-only until real
188
+ fixtures validate byte-for-byte repacking. V1 and v2 documents are recognized for
189
+ diagnostics but must be re-exported as v3 before packing because they do not record
190
+ the required layout metadata.
191
+
192
+ Modern files may use signed `-1` as an explicit null `Object` reference; repack
193
+ preserves that sentinel while continuing to reject every other missing instance
194
+ ID. Fixed-width arrays above one million elements are kept compactly as
195
+ `_raw_array_count` plus `_raw_array_hex`. This representation is intentionally
196
+ opaque but lossless, avoids expanding millions of Python integers, and validates
197
+ the count/payload length before packing.
146
198
 
147
199
  For stable patch-and-repack workflows, use `patch_file()` or `parse_pack_file()`:
148
200
 
@@ -165,6 +217,33 @@ converter.patch_file(
165
217
  )
166
218
  ```
167
219
 
220
+ ## Compatibility Validation
221
+
222
+ The repository includes a schema-free corpus runner. Game files are not committed;
223
+ point it at a locally unpacked `natives` tree:
224
+
225
+ ```bash
226
+ python tests/corpus_probe.py D:/game/natives \
227
+ --expected-total 62768 \
228
+ --expected-version 16 \
229
+ --report layout-report.json
230
+ ```
231
+
232
+ Version 0.7.1 fixes modern RSZ alignment regression found in 0.7.0 and folds the
233
+ planned compatibility work into the bug-fix release: staged layout detection,
234
+ structured diagnostics, safe readable parsing, verified-only repacking, and the
235
+ schema-free probe command. The modern rule was validated against 62,768 MHWS
236
+ `.user.3` files; four SystemSetting fixtures additionally passed readable export,
237
+ repack export, byte-identical packing, and reparse checks.
238
+
239
+ A second corpus from Monster Hunter Stories 3 was also validated: all 42,945 files
240
+ passed strict layout detection and schema-driven repack export. All 42,945 rebuilt
241
+ without exceptions; 38,548 were byte-identical. The remaining 4,397 are successful
242
+ rebuilds but not byte-identical; observed causes include nonzero padding and
243
+ alternate empty-string encodings, so they are not claimed as byte-identical
244
+ fixtures. Two voxel files use the compact large-array representation; both rebuilt
245
+ byte-identically, including the 6.55 MB `dg100_Root` payload.
246
+
168
247
  ## Build From Source
169
248
 
170
249
  ```bash
@@ -14,6 +14,7 @@ pip install pyreuser3
14
14
  - `JSON -> .user.3` packing.
15
15
  - A reusable Python API through `REUser3Converter`.
16
16
  - CLI commands through `pyreuser3`.
17
+ - Schema-free layout probing through `pyreuser3 probe` and the public probe API.
17
18
  - A local `.user.3` export Web UI through `pyreuser3-web`.
18
19
  - Automatic separation of the USR outer layout from the embedded RSZ header family;
19
20
  modern RSZ v4+ files preserve their original numeric version during repack.
@@ -26,6 +27,20 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
26
27
  RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
27
28
  until real fixtures provide byte-for-byte validation.
28
29
 
30
+ Version 0.7.1 fixes the 0.7.0 modern RSZ alignment regression. Modern userdata and
31
+ data targets use absolute 16-byte file alignment even though their stored offsets
32
+ remain RSZ-relative. Readable parsing accepts alignment-only deviations without
33
+ hiding structural corruption, while the probe API reports them explicitly. Repack
34
+ output blocks layouts carrying unverified diagnostics. The strict schema-free
35
+ probe can validate large local corpora.
36
+
37
+ Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
38
+ with automatic file-signature invalidation and an explicit
39
+ `clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
40
+ packer. Signed `-1` null object references and very large fixed-width arrays are
41
+ preserved losslessly; large arrays use a compact raw payload instead of millions
42
+ of Python scalar objects.
43
+
29
44
  ## Requirements
30
45
 
31
46
  - Python 3.9 or newer.
@@ -33,6 +48,8 @@ until real fixtures provide byte-for-byte validation.
33
48
  - An `il2cpp_dump.json` file when exporting readable enum labels.
34
49
  - One or more unpacked `.user.3` files.
35
50
 
51
+ `pyreuser3 probe` does not require a schema or `il2cpp_dump.json`.
52
+
36
53
  ## Usage
37
54
 
38
55
  Usage details may change as the package evolves. For the latest command-line and Python API examples, read the GitHub README:
@@ -33,6 +33,7 @@ PyREUser3 当前提供:
33
33
  - `JSON -> .user.3` 封包;
34
34
  - 面向其他项目调用的 `REUser3Converter` Python API;
35
35
  - `pyreuser3` 命令行工具;
36
+ - 无需 schema 的单文件与整批布局探测;
36
37
  - `pyreuser3-web` 本地 Web 导出界面。
37
38
 
38
39
  PyPI 包不会包含游戏资源、游戏 dump、RE_RSZ 模板、`il2cpp_dump.json`、或特定仓库脚本。调用方需要自己准备与目标游戏版本匹配的数据文件。
@@ -44,6 +45,9 @@ PyPI 包不会包含游戏资源、游戏 dump、RE_RSZ 模板、`il2cpp_dump.js
44
45
  - 导出可读枚举标签时需要 `il2cpp_dump.json`;
45
46
  - 一个或多个已解包的 `.user.3` 文件。
46
47
 
48
+ 布局探测是例外:`pyreuser3 probe` 只检查 USR/RSZ 容器,不需要 schema 或
49
+ `il2cpp_dump.json`。
50
+
47
51
  ## 命令行使用
48
52
 
49
53
  导出 `.user.3` 为 JSON:
@@ -82,6 +86,17 @@ pyreuser3 pack \
82
86
  - `-s/--schema-path` 必须指向具体 schema JSON 文件,不应传目录;
83
87
  - `-i`、`-j` 都可以传单个文件或目录,目录会递归处理。
84
88
 
89
+ 无需加载游戏元数据即可探测单个文件或整个目录:
90
+
91
+ ```bash
92
+ pyreuser3 probe -i <输入的-user3-文件或目录>
93
+ pyreuser3 probe -i <输入的-user3-文件或目录> --strict -o layout-report.json
94
+ ```
95
+
96
+ 默认探测采用与 readable 导出相同的安全策略:结构损坏仍会直接失败,只有非规范对齐会记录为
97
+ 警告。`--strict` 会拒绝任何布局偏差,适合整批语料验证。可选的 JSON 报告会按布局、RSZ
98
+ 版本和诊断代码汇总结果。
99
+
85
100
  可用 `--user-magic` 和 `--rsz-magic` 覆盖默认 magic,例如:
86
101
 
87
102
  ```bash
@@ -139,6 +154,27 @@ converter.pack_file(
139
154
  )
140
155
  ```
141
156
 
157
+ `REUser3Converter` 会延迟加载 schema 和 il2cpp metadata,并在该 converter 的生命周期内复用。
158
+ 连续执行 readable、repack、pack 或 patch 时,不再反复扫描大型 metadata 文件。源文件的绝对
159
+ 路径、大小或纳秒修改时间变化后缓存会自动失效;如果调用方以特殊方式原地替换文件,也可以强制
160
+ 清理:
161
+
162
+ ```python
163
+ converter.clear_metadata_cache()
164
+ ```
165
+
166
+ `patch_directory()` 现在也会让整批文件复用同一个已准备好的 exporter 和 packer,不再为每个
167
+ 文件重复构造。
168
+
169
+ 也可以不创建 converter、不加载 schema,直接调用容器布局探测 API:
170
+
171
+ ```python
172
+ from pyreuser3 import probe_usr_file, probe_usr_path
173
+
174
+ one_file = probe_usr_file("input/example.user.3")
175
+ whole_tree = probe_usr_path("input/natives", policy="strict_probe")
176
+ ```
177
+
142
178
  不写入 JSON 文件,直接把 `.user.3` 转成内存中的 JSON 兼容 Python 对象:
143
179
 
144
180
  ```python
@@ -162,9 +198,17 @@ repack_data = converter.user3_to_json(
162
198
  USR 外层布局、RSZ 头族和文件中的真实 RSZ 版本,并保留 resource 与 userdata 依赖表。布局
163
199
  候选及其读取/回封能力状态集中声明在 `pyreuser3/usr_layouts.py`,RSZ 字段定义仍来自传入的
164
200
  REFramework 兼容模板。已验证的现代头族接受通过完整结构校验的 RSZ v4+ 文件,并原样保留
165
- 版本号,不再固定为 MHWS 的版本 16。实验性的物理 H28 legacy RSZ v3 候选暂时只读,获得
166
- 真实样本并完成逐字节回封验证后才能启用 repack。v1 v2 文档仍可识别以便诊断,但由于缺少
167
- 必要的布局元数据,封回前必须从源文件重新导出为 v3。
201
+ 版本号,不再固定为 MHWS 的版本 16。这个头族中的 offset 数值仍以 RSZ 段为基准,但 userdata
202
+ data 的目标位置按整个文件的绝对 16 字节边界对齐。readable 导出会接受仅对齐偏差,probe
203
+ API 则把它们报告为结构化警告;repack 导出会把同一诊断写入 `_warnings`,并通过
204
+ `_unsupported` 阻止未经验证的布局被封回。实验性的物理 H28 与 legacy RSZ v3 候选暂时只读,
205
+ 获得真实样本并完成逐字节回封验证后才能启用 repack。v1 和 v2 文档仍可识别以便诊断,但由于
206
+ 缺少必要的布局元数据,封回前必须从源文件重新导出为 v3。
207
+
208
+ 部分现代文件会用有符号 `-1` 明确表示空 `Object` 引用;repack 会原样保留这个哨兵,同时继续
209
+ 拒绝其他不存在的实例 ID。超过一百万项的定长数组会紧凑保存为 `_raw_array_count` 和
210
+ `_raw_array_hex`。这种表示不提供逐项可读性,但能够无损回封、避免展开数百万个 Python 整数,
211
+ 并会在封包前校验 count 与 payload 长度是否匹配。
168
212
 
169
213
  批量处理目录:
170
214
 
@@ -212,6 +256,29 @@ converter.patch_file(
212
256
  )
213
257
  ```
214
258
 
259
+ ## 兼容性验证
260
+
261
+ 仓库内提供无需 schema 的语料测试脚本。游戏文件不会提交到仓库,请让脚本指向本地已解包的
262
+ `natives` 目录:
263
+
264
+ ```bash
265
+ python tests/corpus_probe.py D:/game/natives \
266
+ --expected-total 62768 \
267
+ --expected-version 16 \
268
+ --report layout-report.json
269
+ ```
270
+
271
+ 0.7.1 修复了 0.7.0 引入的现代 RSZ 对齐回归,并把原计划中的兼容性工作合并进本次 bugfix:
272
+ 分阶段布局探测、结构化诊断、安全 readable 解析、仅验证布局可 repack,以及无需 schema 的
273
+ probe 命令。新的现代布局规则已用 62,768 个 MHWS `.user.3` 文件验证;其中四个
274
+ SystemSetting 样本还完成了 readable 导出、repack 导出、逐字节一致回封和二次解析验证。
275
+
276
+ 另外还验证了 Monster Hunter Stories 3 语料:42,945 个文件全部通过严格 layout 探测和
277
+ schema 驱动的 repack 导出;42,945 个文件全部能够无异常地重新构建,其中 38,548 个逐字节
278
+ 一致。其余 4,397 个可以成功重建但并非逐字节一致;已观察到的原因包括非零 padding、空字符串
279
+ 的另一种物理编码,因此不把它们宣称为逐字节 fixture。两个使用紧凑超大数组表示的 voxel 文件
280
+ 都能逐字节一致回封,其中包括 6.55 MB 的 `dg100_Root` payload。
281
+
215
282
  ## 常见注意事项
216
283
 
217
284
  - schema JSON、`il2cpp_dump.json` 和 `.user.3` 文件应来自同一个游戏版本;
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyREUser3"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Pure Python tools for converting RE Engine .user.3 files to and from JSON."
9
9
  readme = "docs/PYPI.md"
10
10
  requires-python = ">=3.9"
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  from importlib import import_module
10
10
  from typing import Any
11
11
 
12
- __version__ = "0.7.0"
12
+ __version__ = "0.7.1"
13
13
 
14
14
  __all__ = [
15
15
  "__version__",
@@ -18,9 +18,12 @@ __all__ = [
18
18
  "FieldDef",
19
19
  "PackError",
20
20
  "ParseError",
21
+ "LayoutValidationIssue",
21
22
  "TypeDB",
22
23
  "REUser3Converter",
23
24
  "RSZ_MAGIC",
25
+ "probe_usr_file",
26
+ "probe_usr_path",
24
27
  "User3Exporter",
25
28
  "User3Packer",
26
29
  "USR_MAGIC",
@@ -31,6 +34,9 @@ _EXPORT_MODULES = {
31
34
  "ParseError": ".core",
32
35
  "RSZ_MAGIC": ".core",
33
36
  "USR_MAGIC": ".core",
37
+ "LayoutValidationIssue": ".usr_container",
38
+ "probe_usr_file": ".usr_container",
39
+ "probe_usr_path": ".usr_container",
34
40
  "ClassDef": ".schema",
35
41
  "FieldDef": ".schema",
36
42
  "TypeDB": ".schema",
@@ -10,12 +10,15 @@ from __future__ import annotations
10
10
  import inspect
11
11
  import json
12
12
  import re
13
+ import threading
13
14
  from pathlib import Path
14
15
  from typing import Any, Callable, Literal, Optional
15
16
 
16
- from .core import RSZ_MAGIC, USR_MAGIC
17
+ from .core import RSZ_MAGIC, USR_MAGIC, resolve_schema_path
17
18
  from .export import User3Exporter
18
19
  from .pack import User3Packer
20
+ from .schema import TypeDB
21
+ from .usr_container import probe_usr_file, probe_usr_path
19
22
 
20
23
  # Preserve the exported JSON structure so external scripts and hand-edited files remain
21
24
  # compatible across workflows.
@@ -67,6 +70,11 @@ class REUser3Converter:
67
70
  self.tree_depth = tree_depth
68
71
  self.user_magic = int(user_magic)
69
72
  self.rsz_magic = int(rsz_magic)
73
+ self._metadata_cache_lock = threading.RLock()
74
+ self._typedb_cache: tuple[tuple[str, int, int], TypeDB] | None = None
75
+ self._il2cpp_metadata_cache: (
76
+ tuple[tuple[str, int, int], tuple[dict, dict]] | None
77
+ ) = None
70
78
 
71
79
  def export_directory(
72
80
  self,
@@ -94,6 +102,7 @@ class REUser3Converter:
94
102
  exclude_regexes,
95
103
  json_format=self._normalize_json_format(json_format),
96
104
  )
105
+ self._prepare_exporter_metadata(exporter)
97
106
  return exporter.run()
98
107
 
99
108
  def export_file(
@@ -189,10 +198,42 @@ class REUser3Converter:
189
198
  Returns:
190
199
  JsonTree: JSON-compatible tree used by export, editing, or packing workflows.
191
200
  """
192
- exporter = self._new_exporter(user3_path, Path.cwd(), [])
201
+ exporter = self._new_exporter(
202
+ user3_path,
203
+ Path.cwd(),
204
+ [],
205
+ json_format="repack",
206
+ )
193
207
  self._prepare_exporter_metadata(exporter)
194
208
  return exporter._parse_user3_pack(Path(user3_path))
195
209
 
210
+ def probe_user3(self, user3_path: str | Path, strict: bool = False) -> dict[str, Any]:
211
+ """Inspect one container without loading schema or enum metadata."""
212
+
213
+ return probe_usr_file(
214
+ user3_path,
215
+ user_magic=self.user_magic,
216
+ rsz_magic=self.rsz_magic,
217
+ policy="strict_probe" if strict else "safe_read",
218
+ )
219
+
220
+ def probe_directory(
221
+ self,
222
+ user3_root: str | Path,
223
+ *,
224
+ strict: bool = False,
225
+ include_successes: bool = False,
226
+ ) -> dict[str, Any]:
227
+ """Inspect a file tree without loading schema or enum metadata."""
228
+
229
+ return probe_usr_path(
230
+ user3_root,
231
+ user_magic=self.user_magic,
232
+ rsz_magic=self.rsz_magic,
233
+ policy="strict_probe" if strict else "safe_read",
234
+ include_successes=include_successes,
235
+ )
236
+
196
237
  @staticmethod
197
238
  def _normalize_json_format(json_format: str) -> str:
198
239
  """Normalize the public in-memory JSON format selector.
@@ -310,7 +351,8 @@ class REUser3Converter:
310
351
  include_patterns = [re.compile(p) for p in (include_regexes or [])]
311
352
  exclude_patterns = [re.compile(p) for p in (exclude_regexes or [])]
312
353
 
313
- total = success = failed = skipped = 0
354
+ selected: list[tuple[Path, Path]] = []
355
+ skipped = 0
314
356
  for file_path in files:
315
357
  # Resolve and validate paths at the boundary so later code never guesses
316
358
  # relative to a surprising working directory.
@@ -327,22 +369,36 @@ class REUser3Converter:
327
369
  if any(pattern.search(rel) for pattern in exclude_patterns):
328
370
  skipped += 1
329
371
  continue
372
+ selected.append((file_path, Path(rel)))
330
373
 
331
- total += 1
332
- output_path = target_root / (
333
- file_path.name
334
- if source_root.is_file()
335
- else file_path.relative_to(source_root)
336
- )
374
+ if not selected:
375
+ return {"total": 0, "success": 0, "failed": 0, "skipped": skipped}
376
+
377
+ exporter = self._new_exporter(
378
+ source_root,
379
+ target_root,
380
+ [],
381
+ json_format="repack",
382
+ )
383
+ self._prepare_exporter_metadata(exporter)
384
+ packer = self._new_packer(target_root)
385
+ success = failed = 0
386
+ for file_path, relative_path in selected:
387
+ output_path = target_root / relative_path
337
388
  try:
338
389
  # Treat each file independently so one malformed resource is reported
339
390
  # but does not stop the rest of the batch.
340
- self.patch_file(file_path, output_path, callback)
391
+ data = exporter._parse_user3_pack(file_path)
392
+ modified = self._run_callback(callback, data, file_path)
393
+ if modified is None:
394
+ modified = data
395
+ output_path.parent.mkdir(parents=True, exist_ok=True)
396
+ output_path.write_bytes(packer.pack(modified))
341
397
  success += 1
342
398
  except Exception:
343
399
  failed += 1
344
400
  return {
345
- "total": total,
401
+ "total": len(selected),
346
402
  "success": success,
347
403
  "failed": failed,
348
404
  "skipped": skipped,
@@ -372,6 +428,8 @@ class REUser3Converter:
372
428
  """
373
429
  if self.il2cpp_dump_path is None:
374
430
  raise FileNotFoundError("il2cpp_dump_path is required for exporting JSON")
431
+ typedb = self._get_cached_typedb()
432
+ il2cpp_metadata = self._get_cached_il2cpp_metadata()
375
433
  return User3Exporter(
376
434
  user3_root=user3_root,
377
435
  schema_dir=self.schema_path,
@@ -382,6 +440,8 @@ class REUser3Converter:
382
440
  user_magic=self.user_magic,
383
441
  rsz_magic=self.rsz_magic,
384
442
  json_format=json_format,
443
+ preloaded_typedb=typedb,
444
+ preloaded_il2cpp_metadata=il2cpp_metadata,
385
445
  )
386
446
 
387
447
  def _new_packer(self, output_root: str | Path | None) -> User3Packer:
@@ -399,6 +459,8 @@ class REUser3Converter:
399
459
  output_root=output_root,
400
460
  user_magic=self.user_magic,
401
461
  rsz_magic=self.rsz_magic,
462
+ preloaded_typedb=self._get_cached_typedb(),
463
+ preloaded_il2cpp_metadata=self._get_cached_il2cpp_metadata(),
402
464
  )
403
465
 
404
466
  def _prepare_exporter_metadata(self, exporter: User3Exporter) -> None:
@@ -419,14 +481,58 @@ class REUser3Converter:
419
481
  # parsing keeps the same lookup only in memory.
420
482
  # Keep the generated metadata attached to the exporter so field parsing can
421
483
  # format enum names consistently.
422
- enums_internal, enum_context = exporter.export_il2cpp_metadata_from_path(
423
- self.il2cpp_dump_path
424
- )
484
+ metadata = self._get_cached_il2cpp_metadata()
485
+ if metadata is None:
486
+ raise FileNotFoundError("il2cpp_dump_path is required for parsing JSON")
487
+ enums_internal, enum_context = metadata
425
488
  exporter.enum_lookup = exporter._build_enum_lookup_from_enums_internal(
426
489
  enums_internal
427
490
  )
428
491
  exporter._apply_enum_context(enum_context)
429
492
  exporter._ensure_enum_lookup()
493
+ exporter._metadata_prepared = True
494
+
495
+ @staticmethod
496
+ def _metadata_file_signature(path: Path) -> tuple[str, int, int]:
497
+ """Return an inexpensive cache key that changes when a metadata file changes."""
498
+
499
+ resolved = path.resolve(strict=True)
500
+ stat = resolved.stat()
501
+ return str(resolved), stat.st_size, stat.st_mtime_ns
502
+
503
+ def _get_cached_typedb(self) -> TypeDB:
504
+ """Load the schema once per converter and invalidate it after file changes."""
505
+
506
+ schema_path = resolve_schema_path(self.schema_path)
507
+ signature = self._metadata_file_signature(schema_path)
508
+ with self._metadata_cache_lock:
509
+ if self._typedb_cache is None or self._typedb_cache[0] != signature:
510
+ self._typedb_cache = (signature, TypeDB.load(schema_path))
511
+ return self._typedb_cache[1]
512
+
513
+ def _get_cached_il2cpp_metadata(self) -> tuple[dict, dict] | None:
514
+ """Stream the il2cpp dump once per converter and invalidate after changes."""
515
+
516
+ if self.il2cpp_dump_path is None:
517
+ return None
518
+ signature = self._metadata_file_signature(self.il2cpp_dump_path)
519
+ with self._metadata_cache_lock:
520
+ if (
521
+ self._il2cpp_metadata_cache is None
522
+ or self._il2cpp_metadata_cache[0] != signature
523
+ ):
524
+ metadata = User3Exporter.export_il2cpp_metadata_from_path(
525
+ self.il2cpp_dump_path
526
+ )
527
+ self._il2cpp_metadata_cache = (signature, metadata)
528
+ return self._il2cpp_metadata_cache[1]
529
+
530
+ def clear_metadata_cache(self) -> None:
531
+ """Discard cached schema and il2cpp metadata for this converter."""
532
+
533
+ with self._metadata_cache_lock:
534
+ self._typedb_cache = None
535
+ self._il2cpp_metadata_cache = None
430
536
 
431
537
  @staticmethod
432
538
  def _discover_user3_files(user3_root: Path) -> list[Path]: