PyREUser3 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PKG-INFO +7 -1
  2. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/PKG-INFO +7 -1
  3. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/SOURCES.txt +4 -1
  4. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/README.md +31 -6
  5. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/docs/PYPI.md +6 -0
  6. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/docs/README.zh-CN.md +28 -6
  7. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyproject.toml +1 -1
  8. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/__init__.py +3 -0
  9. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/api.py +18 -5
  10. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/cli.py +10 -6
  11. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/core.py +5 -2
  12. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/base.py +16 -5
  13. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/user3.py +113 -97
  14. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/pack/base.py +63 -19
  15. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/pack/models.py +46 -0
  16. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/pack/plan.py +286 -9
  17. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/pack/writer.py +262 -40
  18. pyreuser3-0.7.0/pyreuser3/usr_container.py +339 -0
  19. pyreuser3-0.7.0/pyreuser3/usr_layouts.py +321 -0
  20. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/tests/test_enum_binary.py +1 -0
  21. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/tests/test_enum_codec.py +11 -3
  22. pyreuser3-0.7.0/tests/test_usr_container.py +377 -0
  23. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/LICENSE +0 -0
  24. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/MANIFEST.in +0 -0
  25. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
  26. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/entry_points.txt +0 -0
  27. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/requires.txt +0 -0
  28. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/PyREUser3.egg-info/top_level.txt +0 -0
  29. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/__main__.py +0 -0
  30. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/enum_codec.py +0 -0
  31. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/__init__.py +0 -0
  32. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/enums.py +0 -0
  33. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/fields.py +0 -0
  34. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/metadata.py +0 -0
  35. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/postprocess.py +0 -0
  36. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/export/tree.py +0 -0
  37. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/pack/__init__.py +0 -0
  38. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/rich_ui.py +0 -0
  39. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/schema.py +0 -0
  40. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/__init__.py +0 -0
  41. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/__main__.py +0 -0
  42. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/handler.py +0 -0
  43. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/jobs.py +0 -0
  44. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/page.py +0 -0
  45. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/picker.py +0 -0
  46. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/runners.py +0 -0
  47. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/server.py +0 -0
  48. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/pyreuser3/web/settings.py +0 -0
  49. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/setup.cfg +0 -0
  50. {pyreuser3-0.6.0 → pyreuser3-0.7.0}/tests/test_export_enums.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
5
5
  Author: Egg Targaryen
6
6
  License-Expression: MIT
@@ -48,11 +48,17 @@ pip install pyreuser3
48
48
  - A reusable Python API through `REUser3Converter`.
49
49
  - CLI commands through `pyreuser3`.
50
50
  - A local `.user.3` export Web UI through `pyreuser3-web`.
51
+ - Automatic separation of the USR outer layout from the embedded RSZ header family;
52
+ modern RSZ v4+ files preserve their original numeric version during repack.
51
53
 
52
54
  The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
53
55
  `il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
54
56
  version.
55
57
 
58
+ Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
59
+ RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
60
+ until real fixtures provide byte-for-byte validation.
61
+
56
62
  ## Requirements
57
63
 
58
64
  - Python 3.9 or newer.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyREUser3
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Pure Python tools for converting RE Engine .user.3 files to and from JSON.
5
5
  Author: Egg Targaryen
6
6
  License-Expression: MIT
@@ -48,11 +48,17 @@ pip install pyreuser3
48
48
  - A reusable Python API through `REUser3Converter`.
49
49
  - CLI commands through `pyreuser3`.
50
50
  - A local `.user.3` export Web UI through `pyreuser3-web`.
51
+ - Automatic separation of the USR outer layout from the embedded RSZ header family;
52
+ modern RSZ v4+ files preserve their original numeric version during repack.
51
53
 
52
54
  The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
53
55
  `il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
54
56
  version.
55
57
 
58
+ Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
59
+ RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
60
+ until real fixtures provide byte-for-byte validation.
61
+
56
62
  ## Requirements
57
63
 
58
64
  - Python 3.9 or newer.
@@ -18,6 +18,8 @@ pyreuser3/core.py
18
18
  pyreuser3/enum_codec.py
19
19
  pyreuser3/rich_ui.py
20
20
  pyreuser3/schema.py
21
+ pyreuser3/usr_container.py
22
+ pyreuser3/usr_layouts.py
21
23
  pyreuser3/export/__init__.py
22
24
  pyreuser3/export/base.py
23
25
  pyreuser3/export/enums.py
@@ -42,4 +44,5 @@ pyreuser3/web/server.py
42
44
  pyreuser3/web/settings.py
43
45
  tests/test_enum_binary.py
44
46
  tests/test_enum_codec.py
45
- tests/test_export_enums.py
47
+ tests/test_export_enums.py
48
+ tests/test_usr_container.py
@@ -55,11 +55,20 @@ pyreuser3 export \
55
55
  -p <il2cpp_dump.json>
56
56
  ```
57
57
 
58
- Pack JSON back to `.user.3`:
58
+ Export full repack JSON, then pack it back to `.user.3`:
59
+
60
+ ```bash
61
+ pyreuser3 export \
62
+ -i <input-user3-file-or-directory> \
63
+ -s <RE_RSZ-schema.json> \
64
+ -o <repack-json-output-directory> \
65
+ -p <il2cpp_dump.json> \
66
+ --json-format repack
67
+ ```
59
68
 
60
69
  ```bash
61
70
  pyreuser3 pack \
62
- -j <input-json-file-or-directory> \
71
+ -j <input-repack-json-file-or-directory> \
63
72
  -s <RE_RSZ-schema.json> \
64
73
  -o <user3-output-directory> \
65
74
  -p <il2cpp_dump.json>
@@ -90,8 +99,14 @@ converter.export_file(
90
99
  "json/OtomonData.user.3.json",
91
100
  )
92
101
 
102
+ # Packing only accepts the full repack document. Readable exports are read-only.
103
+ converter.export_file(
104
+ "input/OtomonData.user.3",
105
+ "json/OtomonData.user.3.pack.json",
106
+ json_format="repack",
107
+ )
93
108
  converter.pack_file(
94
- "json/OtomonData.user.3.json",
109
+ "json/OtomonData.user.3.pack.json",
95
110
  "mod/OtomonData.user.3",
96
111
  )
97
112
  ```
@@ -110,14 +125,24 @@ repack_data = converter.user3_to_json(
110
125
  )
111
126
  ```
112
127
 
113
- Use `json_format="readable"` for the same shape produced by `export_file()`, or `json_format="repack"` for the full instance-table document accepted by `pack()`.
128
+ Use `json_format="readable"` for the same shape produced by `export_file()`. This
129
+ shape is read-only. Use `json_format="repack"` for the full document accepted by
130
+ `pack()`; the packer rejects readable JSON.
114
131
 
115
132
  Enum fields are rendered as `[numeric] Name` labels using the enum's actual storage
116
133
  width. Scalar flag enums are rendered as arrays of labels. ``ace.Bitset`1<T>`` values are
117
134
  rendered as enum-index labels together with `_MaxElement` and `_WordCount`, so unknown
118
135
  bits and padded word arrays remain reversible. Repack exports use
119
- `re_user3_pack_v2`; the packer continues to accept v1 documents with raw numeric Bitset
120
- word arrays.
136
+ `re_user3_pack_v3`, which records the independently detected USR outer layout, RSZ
137
+ header family, and numeric RSZ version while preserving resource and userdata
138
+ dependency tables. Layout candidates and their read/repack capability status are
139
+ declared in `pyreuser3/usr_layouts.py`; RSZ field definitions still come from the
140
+ supplied REFramework-compatible schema. The verified modern header family accepts
141
+ structurally valid RSZ v4+ files and preserves their original version instead of
142
+ forcing MHWS version 16. Experimental physical H28 and legacy RSZ v3 candidates are
143
+ read-only until real fixtures validate byte-for-byte repacking. V1 and v2 documents
144
+ are recognized for diagnostics but must be re-exported as v3 before packing because
145
+ they do not record the required layout metadata.
121
146
 
122
147
  For stable patch-and-repack workflows, use `patch_file()` or `parse_pack_file()`:
123
148
 
@@ -15,11 +15,17 @@ pip install pyreuser3
15
15
  - A reusable Python API through `REUser3Converter`.
16
16
  - CLI commands through `pyreuser3`.
17
17
  - A local `.user.3` export Web UI through `pyreuser3-web`.
18
+ - Automatic separation of the USR outer layout from the embedded RSZ header family;
19
+ modern RSZ v4+ files preserve their original numeric version during repack.
18
20
 
19
21
  The published package intentionally does not include game resources, dumped game data, RE_RSZ templates,
20
22
  `il2cpp_dump.json`, or repository-specific helper scripts. You need to provide data files that match the target game and
21
23
  version.
22
24
 
25
+ Verified H30/modern layouts support repacking. Experimental physical H28 and legacy
26
+ RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
27
+ until real fixtures provide byte-for-byte validation.
28
+
23
29
  ## Requirements
24
30
 
25
31
  - Python 3.9 or newer.
@@ -56,11 +56,20 @@ pyreuser3 export \
56
56
  -p <il2cpp_dump.json>
57
57
  ```
58
58
 
59
- JSON 封回 `.user.3`:
59
+ 导出完整 repack JSON,然后封回 `.user.3`:
60
+
61
+ ```bash
62
+ pyreuser3 export \
63
+ -i <输入的-user3-文件或目录> \
64
+ -s <RE_RSZ-schema.json> \
65
+ -o <repack-JSON-输出目录> \
66
+ -p <il2cpp_dump.json> \
67
+ --json-format repack
68
+ ```
60
69
 
61
70
  ```bash
62
71
  pyreuser3 pack \
63
- -j <输入的-JSON-文件或目录> \
72
+ -j <输入的-repack-JSON-文件或目录> \
64
73
  -s <RE_RSZ-schema.json> \
65
74
  -o <user3-输出目录> \
66
75
  -p <il2cpp_dump.json>
@@ -118,8 +127,14 @@ converter.export_file(
118
127
  "json/OtomonData.user.3.json",
119
128
  )
120
129
 
130
+ # 封包只接受完整 repack 文档;readable 导出仅供读取。
131
+ converter.export_file(
132
+ "input/OtomonData.user.3",
133
+ "json/OtomonData.user.3.pack.json",
134
+ json_format="repack",
135
+ )
121
136
  converter.pack_file(
122
- "json/OtomonData.user.3.json",
137
+ "json/OtomonData.user.3.pack.json",
123
138
  "mod/OtomonData.user.3",
124
139
  )
125
140
  ```
@@ -138,12 +153,18 @@ repack_data = converter.user3_to_json(
138
153
  )
139
154
  ```
140
155
 
141
- 使用 `json_format="readable"` 时返回与 `export_file()` 一致的可读导出结构;使用 `json_format="repack"` 时返回可传给 `pack()` 的完整实例表结构。
156
+ 使用 `json_format="readable"` 时返回与 `export_file()` 一致的只读导出结构;使用
157
+ `json_format="repack"` 时返回可传给 `pack()` 的完整实例表结构。封回器会拒绝 readable JSON。
142
158
 
143
159
  枚举字段会按照真实底层存储宽度输出为 `[数值] 名称`。标量位标志枚举输出为标签数组;
144
160
  ``ace.Bitset`1<T>`` 输出为枚举索引标签,并保留 `_MaxElement` 与 `_WordCount`,因此未知位和
145
- 填充词也可以无损封回。新的 repack 文档格式为 `re_user3_pack_v2`,封回器仍兼容使用原始
146
- 数值词数组的 v1 文档。
161
+ 填充词也可以无损封回。新的 repack 文档格式为 `re_user3_pack_v3`,它会分别记录自动探测到的
162
+ USR 外层布局、RSZ 头族和文件中的真实 RSZ 版本,并保留 resource 与 userdata 依赖表。布局
163
+ 候选及其读取/回封能力状态集中声明在 `pyreuser3/usr_layouts.py`,RSZ 字段定义仍来自传入的
164
+ REFramework 兼容模板。已验证的现代头族接受通过完整结构校验的 RSZ v4+ 文件,并原样保留
165
+ 版本号,不再固定为 MHWS 的版本 16。实验性的物理 H28 与 legacy RSZ v3 候选暂时只读,获得
166
+ 真实样本并完成逐字节回封验证后才能启用 repack。v1 和 v2 文档仍可识别以便诊断,但由于缺少
167
+ 必要的布局元数据,封回前必须从源文件重新导出为 v3。
147
168
 
148
169
  批量处理目录:
149
170
 
@@ -158,6 +179,7 @@ converter = REUser3Converter(
158
179
  export_result = converter.export_directory(
159
180
  "D:/game/unpacked",
160
181
  "D:/game/json",
182
+ json_format="repack",
161
183
  )
162
184
 
163
185
  pack_result = converter.pack_directory(
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "PyREUser3"
7
- version = "0.6.0"
7
+ version = "0.7.0"
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,10 @@ from __future__ import annotations
9
9
  from importlib import import_module
10
10
  from typing import Any
11
11
 
12
+ __version__ = "0.7.0"
13
+
12
14
  __all__ = [
15
+ "__version__",
13
16
  "BinaryReader",
14
17
  "ClassDef",
15
18
  "FieldDef",
@@ -73,6 +73,7 @@ class REUser3Converter:
73
73
  user3_root: str | Path,
74
74
  output_root: str | Path,
75
75
  exclude_regexes: list[str] | None = None,
76
+ json_format: JsonFormat = "readable",
76
77
  ) -> dict[str, int]:
77
78
  """Export every selected .user.3 file under a directory or single-file root.
78
79
 
@@ -81,33 +82,42 @@ class REUser3Converter:
81
82
  output_root (str | Path): Directory where generated output is written.
82
83
  exclude_regexes (list[str] | None): Regular expressions used to skip matching
83
84
  relative paths.
85
+ json_format (JsonFormat): "readable" for read-only exports or "repack"
86
+ for documents accepted by pack().
84
87
 
85
88
  Returns:
86
89
  dict[str, int]: Counters describing total, successful, and failed items.
87
90
  """
88
- exporter = self._new_exporter(user3_root, output_root, exclude_regexes)
91
+ exporter = self._new_exporter(
92
+ user3_root,
93
+ output_root,
94
+ exclude_regexes,
95
+ json_format=self._normalize_json_format(json_format),
96
+ )
89
97
  return exporter.run()
90
98
 
91
99
  def export_file(
92
100
  self,
93
101
  user3_path: str | Path,
94
102
  json_path: str | Path,
103
+ json_format: JsonFormat = "readable",
95
104
  ) -> Path:
96
105
  """Export one .user.3 file to the requested JSON path.
97
106
 
98
107
  Args:
99
108
  user3_path (str | Path): Path to the .user.3 file being parsed, exported, patched, or packed.
100
109
  json_path (str | Path): Path to the JSON document read from or written by this workflow.
110
+ json_format (JsonFormat): "readable" for a read-only tree or "repack"
111
+ for a document accepted by pack().
101
112
 
102
113
  Returns:
103
114
  Path: Concrete filesystem path returned after the read, write, or resolution step finishes.
104
115
  """
105
- # Reuse parse_file so single-file and batch exports keep the same parsed JSON shape and metadata handling.
106
- # Preserve the exported JSON structure so external scripts and hand-edited files
107
- # remain compatible across workflows.
116
+ # Use the same in-memory format selector as parse_file/parse_pack_file so
117
+ # single-file and batch exports keep identical document shapes.
108
118
  tree = self.user3_to_json(
109
119
  user3_path,
110
- json_format="readable",
120
+ json_format=json_format,
111
121
  round_floats=True,
112
122
  )
113
123
  target = Path(json_path)
@@ -343,6 +353,7 @@ class REUser3Converter:
343
353
  user3_root: str | Path,
344
354
  output_root: str | Path,
345
355
  exclude_regexes: list[str] | None,
356
+ json_format: str = "readable",
346
357
  ) -> User3Exporter:
347
358
  """Create an exporter with this facade's schema, enum metadata, and magic values.
348
359
 
@@ -351,6 +362,7 @@ class REUser3Converter:
351
362
  output_root (str | Path): Directory where generated output is written.
352
363
  exclude_regexes (list[str] | None): Regular expressions used to skip matching
353
364
  relative paths.
365
+ json_format (str): Export document shape requested by the caller.
354
366
 
355
367
  Returns:
356
368
  User3Exporter: Configured object or normalized value returned for the caller to use directly.
@@ -369,6 +381,7 @@ class REUser3Converter:
369
381
  il2cpp_dump_path=self.il2cpp_dump_path,
370
382
  user_magic=self.user_magic,
371
383
  rsz_magic=self.rsz_magic,
384
+ json_format=json_format,
372
385
  )
373
386
 
374
387
  def _new_packer(self, output_root: str | Path | None) -> User3Packer:
@@ -8,9 +8,9 @@ from __future__ import annotations
8
8
 
9
9
  import argparse
10
10
  import json
11
- from importlib.metadata import PackageNotFoundError, version
12
11
  from typing import Sequence
13
12
 
13
+ from . import __version__
14
14
  from .core import RSZ_MAGIC, USR_MAGIC
15
15
 
16
16
 
@@ -33,10 +33,7 @@ def package_version() -> str:
33
33
  Returns:
34
34
  str: Normalized or formatted text.
35
35
  """
36
- try:
37
- return version("PyREUser3")
38
- except PackageNotFoundError:
39
- return "0.1.0"
36
+ return __version__
40
37
 
41
38
 
42
39
  def normalize_tree_depth(value: str) -> int | str:
@@ -143,6 +140,12 @@ def build_parser() -> argparse.ArgumentParser:
143
140
  required=True,
144
141
  help="Path to il2cpp_dump.json, used to generate enum labels.",
145
142
  )
143
+ export_parser.add_argument(
144
+ "--json-format",
145
+ choices=("readable", "repack"),
146
+ default="readable",
147
+ help="Export read-only readable JSON or packable repack JSON (default: readable).",
148
+ )
146
149
  add_magic_args(export_parser)
147
150
  export_parser.set_defaults(func=run_export)
148
151
 
@@ -154,7 +157,7 @@ def build_parser() -> argparse.ArgumentParser:
154
157
  "--input-json",
155
158
  "-j",
156
159
  required=True,
157
- help="JSON file or root directory that contains .user.3.json files.",
160
+ help="Repack JSON file or root directory that contains repack documents.",
158
161
  )
159
162
  pack_parser.add_argument(
160
163
  "--schema-path",
@@ -212,6 +215,7 @@ def run_export(args: argparse.Namespace) -> int:
212
215
  il2cpp_dump_path=args.il2cpp_dump_path,
213
216
  user_magic=args.user_magic,
214
217
  rsz_magic=args.rsz_magic,
218
+ json_format=args.json_format,
215
219
  )
216
220
  result = exporter.run()
217
221
  console.log("Export complete:", json.dumps(result, ensure_ascii=False))
@@ -21,8 +21,11 @@ RSZ_MAGIC = 5919570
21
21
  # Preserve instance numbering and reference identity; RSZ object links depend on these
22
22
  # indexes remaining stable.
23
23
  PACK_JSON_FORMAT_V1 = "re_user3_pack_v1"
24
- PACK_JSON_FORMAT = "re_user3_pack_v2"
25
- PACK_JSON_FORMATS = frozenset({PACK_JSON_FORMAT_V1, PACK_JSON_FORMAT})
24
+ PACK_JSON_FORMAT_V2 = "re_user3_pack_v2"
25
+ PACK_JSON_FORMAT = "re_user3_pack_v3"
26
+ PACK_JSON_FORMATS = frozenset(
27
+ {PACK_JSON_FORMAT_V1, PACK_JSON_FORMAT_V2, PACK_JSON_FORMAT}
28
+ )
26
29
  # Decode strings and GUID-like values conservatively so invalid data does not corrupt
27
30
  # subsequent parsing.
28
31
  HEX32_RE = re.compile(r"^[0-9a-fA-F]{32}$")
@@ -43,6 +43,7 @@ class User3Exporter(
43
43
  il2cpp_dump_path: str | Path = "",
44
44
  user_magic: int = USR_MAGIC,
45
45
  rsz_magic: int = RSZ_MAGIC,
46
+ json_format: str = "readable",
46
47
  ):
47
48
  """Initialize User3Exporter with validated configuration and state.
48
49
 
@@ -57,6 +58,8 @@ class User3Exporter(
57
58
  il2cpp_dump_path (str | Path): Path to il2cpp_dump.json for enum metadata.
58
59
  user_magic (int): Expected magic value for the outer .user.3 container header.
59
60
  rsz_magic (int): Expected magic value for embedded RSZ blocks.
61
+ json_format (str): "readable" for display-only trees or "repack" for
62
+ full pack documents.
60
63
 
61
64
  Returns:
62
65
  None. The method performs its documented side effect in place and raises on invalid input.
@@ -77,6 +80,10 @@ class User3Exporter(
77
80
  self.tree_depth = self._normalize_tree_depth(tree_depth)
78
81
  self.user_magic = int(user_magic)
79
82
  self.rsz_magic = int(rsz_magic)
83
+ normalized_format = str(json_format).strip().lower().replace("-", "_")
84
+ if normalized_format not in {"readable", "repack"}:
85
+ raise ValueError("json_format must be 'readable' or 'repack'")
86
+ self.json_format = normalized_format
80
87
  self.exclude_regexes = exclude_regexes or []
81
88
  self._exclude_patterns = [re.compile(p) for p in self.exclude_regexes]
82
89
  self.schema_path = self._resolve_schema_path(self.schema_dir)
@@ -155,10 +162,13 @@ class User3Exporter(
155
162
  # and numeric packing stay reversible.
156
163
  # Preserve the exported JSON structure so external scripts and hand-edited
157
164
  # files remain compatible across workflows.
158
- tree = self._parse_user3(user3_file)
159
- tree = self._postprocess_enum_nodes(tree)
160
- tree = self._finalize_export_tree(tree)
161
- tree = self._round_export_floats(tree)
165
+ if self.json_format == "repack":
166
+ tree = self._parse_user3_pack(user3_file)
167
+ else:
168
+ tree = self._parse_user3(user3_file)
169
+ tree = self._postprocess_enum_nodes(tree)
170
+ tree = self._finalize_export_tree(tree)
171
+ tree = self._round_export_floats(tree)
162
172
  output_path = self._output_path_for(user3_file)
163
173
  output_path.parent.mkdir(parents=True, exist_ok=True)
164
174
  with output_path.open("w", encoding="utf-8") as f:
@@ -254,5 +264,6 @@ class User3Exporter(
254
264
  relative_parent = Path()
255
265
  else:
256
266
  relative_parent = user3_file.relative_to(self.user3_root).parent
257
- output_name = f"{user3_file.name}.json"
267
+ suffix = ".pack.json" if self.json_format == "repack" else ".json"
268
+ output_name = f"{user3_file.name}{suffix}"
258
269
  return self.output_root / relative_parent / output_name