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.
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PKG-INFO +18 -1
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/PKG-INFO +18 -1
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/SOURCES.txt +1 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/README.md +83 -4
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/docs/PYPI.md +17 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/docs/README.zh-CN.md +70 -3
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyproject.toml +1 -1
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/__init__.py +7 -1
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/api.py +120 -14
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/cli.py +52 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/base.py +18 -6
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/fields.py +79 -4
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/metadata.py +13 -7
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/user3.py +43 -1
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/base.py +15 -2
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/models.py +8 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/plan.py +32 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/writer.py +90 -5
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/usr_container.py +307 -27
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/usr_layouts.py +50 -3
- pyreuser3-0.7.1/tests/test_api_cache.py +120 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_usr_container.py +405 -2
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/LICENSE +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/MANIFEST.in +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/core.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/enum_codec.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/enums.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/postprocess.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/setup.cfg +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_enum_binary.py +0 -0
- {pyreuser3-0.7.0 → pyreuser3-0.7.1}/tests/test_enum_codec.py +0 -0
- {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.
|
|
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.
|
|
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:
|
|
@@ -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.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
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` 文件应来自同一个游戏版本;
|
|
@@ -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.
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
-
|
|
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":
|
|
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
|
-
|
|
423
|
-
|
|
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]:
|