PyREUser3 0.7.1__tar.gz → 0.8.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.
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PKG-INFO +1 -8
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/PKG-INFO +1 -8
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/SOURCES.txt +2 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/README.md +4 -29
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/docs/PYPI.md +0 -7
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/docs/README.zh-CN.md +0 -23
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyproject.toml +1 -1
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/__init__.py +1 -1
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/enums.py +85 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/postprocess.py +62 -12
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/pack/base.py +4 -28
- pyreuser3-0.8.0/pyreuser3/pack/container.py +388 -0
- pyreuser3-0.8.0/pyreuser3/pack/plan.py +454 -0
- pyreuser3-0.8.0/pyreuser3/pack/values.py +278 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/pack/writer.py +44 -39
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/tests/test_enum_binary.py +107 -2
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/tests/test_enum_codec.py +27 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/tests/test_export_enums.py +229 -0
- pyreuser3-0.7.1/pyreuser3/pack/plan.py +0 -1040
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/LICENSE +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/MANIFEST.in +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/api.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/cli.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/core.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/enum_codec.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/base.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/fields.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/metadata.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/export/user3.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/pack/models.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/usr_container.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/usr_layouts.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/setup.cfg +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/tests/test_api_cache.py +0 -0
- {pyreuser3-0.7.1 → pyreuser3-0.8.0}/tests/test_usr_container.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -60,13 +60,6 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
|
|
|
60
60
|
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
61
61
|
until real fixtures provide byte-for-byte validation.
|
|
62
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
63
|
Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
|
|
71
64
|
with automatic file-signature invalidation and an explicit
|
|
72
65
|
`clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyREUser3
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.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
|
|
@@ -60,13 +60,6 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
|
|
|
60
60
|
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
61
61
|
until real fixtures provide byte-for-byte validation.
|
|
62
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
63
|
Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
|
|
71
64
|
with automatic file-signature invalidation and an explicit
|
|
72
65
|
`clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
|
|
@@ -30,8 +30,10 @@ pyreuser3/export/tree.py
|
|
|
30
30
|
pyreuser3/export/user3.py
|
|
31
31
|
pyreuser3/pack/__init__.py
|
|
32
32
|
pyreuser3/pack/base.py
|
|
33
|
+
pyreuser3/pack/container.py
|
|
33
34
|
pyreuser3/pack/models.py
|
|
34
35
|
pyreuser3/pack/plan.py
|
|
36
|
+
pyreuser3/pack/values.py
|
|
35
37
|
pyreuser3/pack/writer.py
|
|
36
38
|
pyreuser3/web/__init__.py
|
|
37
39
|
pyreuser3/web/__main__.py
|
|
@@ -169,8 +169,10 @@ Use `json_format="readable"` for the same shape produced by `export_file()`. Thi
|
|
|
169
169
|
shape is read-only. Use `json_format="repack"` for the full document accepted by
|
|
170
170
|
`pack()`; the packer rejects readable JSON.
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
width
|
|
172
|
+
Readable enum fields are rendered as `[numeric] Name` labels using the enum's actual
|
|
173
|
+
storage width and signedness, so signed Fixed IDs match their in-game runtime values.
|
|
174
|
+
Repack documents retain canonical unsigned Fixed prefixes for stable machine editing.
|
|
175
|
+
Scalar flag enums are rendered as arrays of labels. ``ace.Bitset`1<T>`` values are
|
|
174
176
|
rendered as enum-index labels together with `_MaxElement` and `_WordCount`, so unknown
|
|
175
177
|
bits and padded word arrays remain reversible. Repack exports use
|
|
176
178
|
`re_user3_pack_v3`, which records the independently detected USR outer layout, RSZ
|
|
@@ -217,33 +219,6 @@ converter.patch_file(
|
|
|
217
219
|
)
|
|
218
220
|
```
|
|
219
221
|
|
|
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
|
-
|
|
247
222
|
## Build From Source
|
|
248
223
|
|
|
249
224
|
```bash
|
|
@@ -27,13 +27,6 @@ Verified H30/modern layouts support repacking. Experimental physical H28 and leg
|
|
|
27
27
|
RSZ v3 layouts are readable for analysis but intentionally blocked from repacking
|
|
28
28
|
until real fixtures provide byte-for-byte validation.
|
|
29
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
30
|
Repeated operations on one `REUser3Converter` reuse schema and il2cpp metadata,
|
|
38
31
|
with automatic file-signature invalidation and an explicit
|
|
39
32
|
`clear_metadata_cache()` escape hatch. Batch patching reuses one exporter and
|
|
@@ -256,29 +256,6 @@ converter.patch_file(
|
|
|
256
256
|
)
|
|
257
257
|
```
|
|
258
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
|
-
|
|
282
259
|
## 常见注意事项
|
|
283
260
|
|
|
284
261
|
- schema JSON、`il2cpp_dump.json` 和 `.user.3` 文件应来自同一个游戏版本;
|
|
@@ -18,6 +18,9 @@ from ..core import ENUM_UNUSED_KEY, normalize_enum_storage_type
|
|
|
18
18
|
_FIXED_ENUM_RE = re.compile(r"[A-Za-z0-9_.+]+_Fixed")
|
|
19
19
|
_PROPERTY_METHOD_RE = re.compile(r"^(?:get|set)_(?P<name>.+?)(?:\d+)?$")
|
|
20
20
|
_BACKING_FIELD_RE = re.compile(r"^<(?P<name>[^>]+)>")
|
|
21
|
+
_INTEGER_STORAGE_TYPES = frozenset(
|
|
22
|
+
{"S8", "U8", "S16", "U16", "S32", "U32", "S64", "U64"}
|
|
23
|
+
)
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class ExporterEnumSourceMixin:
|
|
@@ -104,6 +107,39 @@ class ExporterEnumSourceMixin:
|
|
|
104
107
|
for candidate in cls._field_name_variants(field_name):
|
|
105
108
|
field_map.setdefault(candidate, fixed_type)
|
|
106
109
|
|
|
110
|
+
@classmethod
|
|
111
|
+
def _add_fixed_backing_candidate(
|
|
112
|
+
cls,
|
|
113
|
+
candidates: dict[str, set[str]],
|
|
114
|
+
field_name: Any,
|
|
115
|
+
type_name: Any,
|
|
116
|
+
) -> None:
|
|
117
|
+
"""Record an integer ``*_Fixed`` backing field by its runtime property name."""
|
|
118
|
+
if not isinstance(field_name, str):
|
|
119
|
+
return
|
|
120
|
+
storage_type = normalize_enum_storage_type(type_name)
|
|
121
|
+
if storage_type not in _INTEGER_STORAGE_TYPES:
|
|
122
|
+
return
|
|
123
|
+
for field_variant in cls._field_name_variants(field_name):
|
|
124
|
+
normalized_name = field_variant.lstrip("_")
|
|
125
|
+
if not normalized_name.endswith("_Fixed"):
|
|
126
|
+
continue
|
|
127
|
+
property_name = normalized_name[: -len("_Fixed")]
|
|
128
|
+
if property_name:
|
|
129
|
+
candidates.setdefault(property_name, set()).add(field_variant)
|
|
130
|
+
candidates.setdefault(normalized_name, set()).add(field_variant)
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def _method_return_type(method: Any) -> str | None:
|
|
134
|
+
"""Return one concrete method return type when metadata exposes it."""
|
|
135
|
+
if not isinstance(method, dict):
|
|
136
|
+
return None
|
|
137
|
+
returns = method.get("returns")
|
|
138
|
+
if not isinstance(returns, dict):
|
|
139
|
+
return None
|
|
140
|
+
return_type = returns.get("type")
|
|
141
|
+
return return_type if isinstance(return_type, str) and return_type else None
|
|
142
|
+
|
|
107
143
|
@staticmethod
|
|
108
144
|
def _method_matches_property(method_name: str, getter_or_setter: str) -> bool:
|
|
109
145
|
"""Return whether an il2cpp method key represents the named property accessor."""
|
|
@@ -264,6 +300,7 @@ class ExporterEnumSourceMixin:
|
|
|
264
300
|
"enum_underlying_types": {},
|
|
265
301
|
"enum_types": set(),
|
|
266
302
|
"serializable_fallback_candidates": [],
|
|
303
|
+
"fixed_backing_candidates": [],
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
@classmethod
|
|
@@ -282,6 +319,7 @@ class ExporterEnumSourceMixin:
|
|
|
282
319
|
state["enum_underlying_types"][class_name] = storage_type
|
|
283
320
|
|
|
284
321
|
field_map: dict[str, str] = {}
|
|
322
|
+
fixed_backing_fields: dict[str, set[str]] = {}
|
|
285
323
|
fields_obj = obj.get("fields")
|
|
286
324
|
if isinstance(fields_obj, dict):
|
|
287
325
|
for field_name, field_info in fields_obj.items():
|
|
@@ -290,6 +328,9 @@ class ExporterEnumSourceMixin:
|
|
|
290
328
|
cls._add_field_fixed_type(
|
|
291
329
|
field_map, field_name, field_info.get("type")
|
|
292
330
|
)
|
|
331
|
+
cls._add_fixed_backing_candidate(
|
|
332
|
+
fixed_backing_fields, field_name, field_info.get("type")
|
|
333
|
+
)
|
|
293
334
|
|
|
294
335
|
# Older dumps expose RE_RSZ field hints through an explicit RSZ array.
|
|
295
336
|
rsz_fields = obj.get("RSZ")
|
|
@@ -303,6 +344,9 @@ class ExporterEnumSourceMixin:
|
|
|
303
344
|
cls._add_field_fixed_type(
|
|
304
345
|
field_map, potential_name, rsz_field.get("type")
|
|
305
346
|
)
|
|
347
|
+
cls._add_fixed_backing_candidate(
|
|
348
|
+
fixed_backing_fields, potential_name, rsz_field.get("type")
|
|
349
|
+
)
|
|
306
350
|
|
|
307
351
|
# Newer dumps can omit RSZ entirely but keep property/type hints here.
|
|
308
352
|
reflection_props = obj.get("reflection_properties")
|
|
@@ -311,8 +355,12 @@ class ExporterEnumSourceMixin:
|
|
|
311
355
|
if not isinstance(prop_name, str) or not isinstance(prop_info, dict):
|
|
312
356
|
continue
|
|
313
357
|
cls._add_field_fixed_type(field_map, prop_name, prop_info.get("type"))
|
|
358
|
+
cls._add_fixed_backing_candidate(
|
|
359
|
+
fixed_backing_fields, prop_name, prop_info.get("type")
|
|
360
|
+
)
|
|
314
361
|
|
|
315
362
|
methods_obj = obj.get("methods")
|
|
363
|
+
runtime_property_types: dict[str, set[str]] = {}
|
|
316
364
|
if isinstance(methods_obj, dict):
|
|
317
365
|
properties_obj = obj.get("properties")
|
|
318
366
|
if isinstance(properties_obj, dict):
|
|
@@ -326,6 +374,11 @@ class ExporterEnumSourceMixin:
|
|
|
326
374
|
continue
|
|
327
375
|
if not cls._method_matches_property(method_name, getter):
|
|
328
376
|
continue
|
|
377
|
+
return_type = cls._method_return_type(method)
|
|
378
|
+
if return_type is not None:
|
|
379
|
+
runtime_property_types.setdefault(
|
|
380
|
+
prop_name, set()
|
|
381
|
+
).add(return_type)
|
|
329
382
|
fixed_type = cls._fixed_type_from_method_return(method)
|
|
330
383
|
if fixed_type is not None:
|
|
331
384
|
cls._add_field_fixed_type(
|
|
@@ -350,6 +403,12 @@ class ExporterEnumSourceMixin:
|
|
|
350
403
|
method_prop_name = cls._property_name_from_method(method_name)
|
|
351
404
|
if method_prop_name is None:
|
|
352
405
|
continue
|
|
406
|
+
if method_name.startswith("get_"):
|
|
407
|
+
return_type = cls._method_return_type(method)
|
|
408
|
+
if return_type is not None:
|
|
409
|
+
runtime_property_types.setdefault(
|
|
410
|
+
method_prop_name, set()
|
|
411
|
+
).add(return_type)
|
|
353
412
|
fixed_type = None
|
|
354
413
|
if method_name.startswith("get_"):
|
|
355
414
|
fixed_type = cls._fixed_type_from_method_return(method)
|
|
@@ -358,6 +417,18 @@ class ExporterEnumSourceMixin:
|
|
|
358
417
|
if fixed_type is not None:
|
|
359
418
|
cls._add_field_fixed_type(field_map, method_prop_name, fixed_type)
|
|
360
419
|
|
|
420
|
+
for property_name, field_names in fixed_backing_fields.items():
|
|
421
|
+
runtime_types = runtime_property_types.get(property_name, set())
|
|
422
|
+
if len(runtime_types) != 1:
|
|
423
|
+
continue
|
|
424
|
+
state["fixed_backing_candidates"].append(
|
|
425
|
+
(
|
|
426
|
+
class_name,
|
|
427
|
+
tuple(sorted(field_names)),
|
|
428
|
+
next(iter(runtime_types)),
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
|
|
361
432
|
if field_map:
|
|
362
433
|
state["class_field_fixed_types"][class_name] = field_map
|
|
363
434
|
|
|
@@ -411,6 +482,20 @@ class ExporterEnumSourceMixin:
|
|
|
411
482
|
if len(fallback_types) == 1:
|
|
412
483
|
serializable_to_fixed[class_name] = fallback_types[0]
|
|
413
484
|
|
|
485
|
+
for class_name, field_names, runtime_type in state[
|
|
486
|
+
"fixed_backing_candidates"
|
|
487
|
+
]:
|
|
488
|
+
fixed_type = (
|
|
489
|
+
runtime_type
|
|
490
|
+
if runtime_type.endswith("_Fixed")
|
|
491
|
+
else f"{runtime_type}_Fixed"
|
|
492
|
+
)
|
|
493
|
+
if fixed_type not in enum_types:
|
|
494
|
+
continue
|
|
495
|
+
field_map = state["class_field_fixed_types"].setdefault(class_name, {})
|
|
496
|
+
for field_name in field_names:
|
|
497
|
+
field_map.setdefault(field_name, fixed_type)
|
|
498
|
+
|
|
414
499
|
for class_name, raw_types in state["generic_candidates"].items():
|
|
415
500
|
enum_args = list(dict.fromkeys(t for t in raw_types if t in enum_types))
|
|
416
501
|
param_args = list(dict.fromkeys(t for t in raw_types if t not in enum_types))
|
|
@@ -13,6 +13,7 @@ from ..enum_codec import (
|
|
|
13
13
|
decode_bitset,
|
|
14
14
|
decode_flags,
|
|
15
15
|
enum_member_for_value,
|
|
16
|
+
normalize_integer_for_storage,
|
|
16
17
|
)
|
|
17
18
|
|
|
18
19
|
|
|
@@ -68,9 +69,18 @@ class ExporterPostprocessMixin:
|
|
|
68
69
|
return candidate
|
|
69
70
|
return type_name
|
|
70
71
|
|
|
71
|
-
def _format_enum_value(
|
|
72
|
+
def _format_enum_value(
|
|
73
|
+
self,
|
|
74
|
+
fixed_enum_type: str,
|
|
75
|
+
value: int,
|
|
76
|
+
*,
|
|
77
|
+
runtime_signedness: bool = True,
|
|
78
|
+
) -> Any:
|
|
72
79
|
"""Format enum value.
|
|
73
80
|
|
|
81
|
+
Exact declared members stay intact; flagged enums are decomposed only when the
|
|
82
|
+
numeric value has no exact member.
|
|
83
|
+
|
|
74
84
|
The method keeps parsing, metadata lookup, and JSON shaping explicit so incomplete
|
|
75
85
|
templates can still produce inspectable output.
|
|
76
86
|
|
|
@@ -86,7 +96,22 @@ class ExporterPostprocessMixin:
|
|
|
86
96
|
value_map = self.enum_lookup.get(fixed_enum_type)
|
|
87
97
|
if value_map is None:
|
|
88
98
|
return value
|
|
89
|
-
|
|
99
|
+
matched = enum_member_for_value(
|
|
100
|
+
self.enum_lookup,
|
|
101
|
+
fixed_enum_type,
|
|
102
|
+
value,
|
|
103
|
+
getattr(self, "enum_underlying_types", {}),
|
|
104
|
+
)
|
|
105
|
+
if matched is not None:
|
|
106
|
+
member_name, fixed_value = matched
|
|
107
|
+
return self._id_formatter(
|
|
108
|
+
member_name,
|
|
109
|
+
self._normalize_enum_display_value(
|
|
110
|
+
fixed_enum_type,
|
|
111
|
+
fixed_value,
|
|
112
|
+
runtime_signedness=runtime_signedness,
|
|
113
|
+
),
|
|
114
|
+
)
|
|
90
115
|
if fixed_enum_type in getattr(self, "enum_flags", set()):
|
|
91
116
|
return decode_flags(
|
|
92
117
|
self.enum_lookup,
|
|
@@ -94,16 +119,37 @@ class ExporterPostprocessMixin:
|
|
|
94
119
|
value,
|
|
95
120
|
getattr(self, "enum_underlying_types", {}),
|
|
96
121
|
)
|
|
97
|
-
|
|
98
|
-
self.enum_lookup,
|
|
99
|
-
fixed_enum_type,
|
|
100
|
-
value,
|
|
101
|
-
getattr(self, "enum_underlying_types", {}),
|
|
102
|
-
)
|
|
103
|
-
if matched is None:
|
|
122
|
+
if not fixed_enum_type.endswith("_Fixed"):
|
|
104
123
|
return value
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
return self._id_formatter(
|
|
125
|
+
"<unknown>",
|
|
126
|
+
self._normalize_enum_display_value(
|
|
127
|
+
fixed_enum_type,
|
|
128
|
+
value,
|
|
129
|
+
runtime_signedness=runtime_signedness,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _normalize_enum_display_value(
|
|
134
|
+
self,
|
|
135
|
+
enum_type: str,
|
|
136
|
+
value: int,
|
|
137
|
+
*,
|
|
138
|
+
runtime_signedness: bool,
|
|
139
|
+
) -> int:
|
|
140
|
+
"""Normalize IDs to the integer domain exposed by the runtime enum."""
|
|
141
|
+
storage_type = getattr(self, "enum_underlying_types", {}).get(enum_type)
|
|
142
|
+
if (
|
|
143
|
+
not runtime_signedness
|
|
144
|
+
and enum_type.endswith("_Fixed")
|
|
145
|
+
and storage_type in {"S8", "S16", "S32", "S64"}
|
|
146
|
+
):
|
|
147
|
+
storage_type = f"U{storage_type[1:]}"
|
|
148
|
+
return (
|
|
149
|
+
normalize_integer_for_storage(value, storage_type)
|
|
150
|
+
if storage_type is not None
|
|
151
|
+
else value
|
|
152
|
+
)
|
|
107
153
|
|
|
108
154
|
@staticmethod
|
|
109
155
|
def _looks_like_class_name(text: str) -> bool:
|
|
@@ -425,7 +471,11 @@ class ExporterPostprocessMixin:
|
|
|
425
471
|
if isinstance(value, int) and scalar_enum_hint is not None:
|
|
426
472
|
# Register enum values through the shared lookup tables so readable labels
|
|
427
473
|
# and numeric packing stay reversible.
|
|
428
|
-
return self._format_enum_value(
|
|
474
|
+
return self._format_enum_value(
|
|
475
|
+
scalar_enum_hint,
|
|
476
|
+
value,
|
|
477
|
+
runtime_signedness=output_mode != "repack",
|
|
478
|
+
)
|
|
429
479
|
return value
|
|
430
480
|
|
|
431
481
|
def _finalize_export_tree(self, value: Any) -> Any:
|
|
@@ -11,12 +11,10 @@ import re
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from .container import RepackContainer
|
|
14
15
|
from .models import (
|
|
15
16
|
ExternalUserdataSpec,
|
|
16
17
|
InstanceSpec,
|
|
17
|
-
RszUserdataSpec,
|
|
18
|
-
UsrResourceSpec,
|
|
19
|
-
UsrUserdataSpec,
|
|
20
18
|
)
|
|
21
19
|
from .plan import PackerPlanMixin
|
|
22
20
|
from .writer import PackerWriterMixin
|
|
@@ -25,12 +23,6 @@ from ..enum_codec import is_probable_flags_enum
|
|
|
25
23
|
from ..export import User3Exporter
|
|
26
24
|
from ..rich_ui import BatchProgress
|
|
27
25
|
from ..schema import TypeDB
|
|
28
|
-
from ..usr_layouts import (
|
|
29
|
-
DEFAULT_RSZ_HEADER_LAYOUT_ID,
|
|
30
|
-
DEFAULT_USR_LAYOUT_ID,
|
|
31
|
-
get_rsz_header_layout,
|
|
32
|
-
get_usr_layout,
|
|
33
|
-
)
|
|
34
26
|
|
|
35
27
|
|
|
36
28
|
class User3Packer(PackerPlanMixin, PackerWriterMixin):
|
|
@@ -97,25 +89,9 @@ class User3Packer(PackerPlanMixin, PackerWriterMixin):
|
|
|
97
89
|
)
|
|
98
90
|
}
|
|
99
91
|
self.instances: list[InstanceSpec | ExternalUserdataSpec | None] = []
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.rsz_header_layout = get_rsz_header_layout(
|
|
104
|
-
DEFAULT_RSZ_HEADER_LAYOUT_ID
|
|
105
|
-
)
|
|
106
|
-
if self.rsz_header_layout is None:
|
|
107
|
-
raise RuntimeError(
|
|
108
|
-
"default RSZ header layout is not registered: "
|
|
109
|
-
f"{DEFAULT_RSZ_HEADER_LAYOUT_ID}"
|
|
110
|
-
)
|
|
111
|
-
self.usr_header_padding = b"\x00" * self.usr_layout.header_padding_size
|
|
112
|
-
self.usr_resources: list[UsrResourceSpec] = []
|
|
113
|
-
self.usr_userdata: list[UsrUserdataSpec] = []
|
|
114
|
-
# A numeric RSZ version must be loaded from repack metadata. The modern
|
|
115
|
-
# header family is shared across games and must not imply MHWS version 16.
|
|
116
|
-
self.rsz_version: int | None = None
|
|
117
|
-
self.rsz_reserved = 0
|
|
118
|
-
self.rsz_userdata: list[RszUserdataSpec] = []
|
|
92
|
+
# The container is committed atomically only after one complete repack document
|
|
93
|
+
# has passed layout, table, instance, and resource validation.
|
|
94
|
+
self.container: RepackContainer | None = None
|
|
119
95
|
|
|
120
96
|
def pack_json_file(self, json_path: str | Path, output_path: str | Path) -> Path:
|
|
121
97
|
"""Pack json file.
|