PyREUser3 0.7.2__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.2 → pyreuser3-0.8.0}/PKG-INFO +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/PKG-INFO +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/README.md +4 -2
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyproject.toml +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/__init__.py +1 -1
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/enums.py +85 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/postprocess.py +52 -4
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/writer.py +32 -5
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/tests/test_enum_binary.py +78 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/tests/test_export_enums.py +229 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/LICENSE +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/MANIFEST.in +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/SOURCES.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/docs/PYPI.md +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/docs/README.zh-CN.md +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/api.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/cli.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/core.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/enum_codec.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/base.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/fields.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/metadata.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/tree.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/export/user3.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/base.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/container.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/models.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/plan.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/pack/values.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/schema.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/usr_container.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/usr_layouts.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/setup.cfg +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/tests/test_api_cache.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/tests/test_enum_codec.py +0 -0
- {pyreuser3-0.7.2 → pyreuser3-0.8.0}/tests/test_usr_container.py +0 -0
|
@@ -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
|
|
@@ -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,7 +69,13 @@ 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
|
|
|
74
81
|
Exact declared members stay intact; flagged enums are decomposed only when the
|
|
@@ -97,7 +104,14 @@ class ExporterPostprocessMixin:
|
|
|
97
104
|
)
|
|
98
105
|
if matched is not None:
|
|
99
106
|
member_name, fixed_value = matched
|
|
100
|
-
return self._id_formatter(
|
|
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
|
+
)
|
|
101
115
|
if fixed_enum_type in getattr(self, "enum_flags", set()):
|
|
102
116
|
return decode_flags(
|
|
103
117
|
self.enum_lookup,
|
|
@@ -105,7 +119,37 @@ class ExporterPostprocessMixin:
|
|
|
105
119
|
value,
|
|
106
120
|
getattr(self, "enum_underlying_types", {}),
|
|
107
121
|
)
|
|
108
|
-
|
|
122
|
+
if not fixed_enum_type.endswith("_Fixed"):
|
|
123
|
+
return value
|
|
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
|
+
)
|
|
109
153
|
|
|
110
154
|
@staticmethod
|
|
111
155
|
def _looks_like_class_name(text: str) -> bool:
|
|
@@ -427,7 +471,11 @@ class ExporterPostprocessMixin:
|
|
|
427
471
|
if isinstance(value, int) and scalar_enum_hint is not None:
|
|
428
472
|
# Register enum values through the shared lookup tables so readable labels
|
|
429
473
|
# and numeric packing stay reversible.
|
|
430
|
-
return self._format_enum_value(
|
|
474
|
+
return self._format_enum_value(
|
|
475
|
+
scalar_enum_hint,
|
|
476
|
+
value,
|
|
477
|
+
runtime_signedness=output_mode != "repack",
|
|
478
|
+
)
|
|
431
479
|
return value
|
|
432
480
|
|
|
433
481
|
def _finalize_export_tree(self, value: Any) -> Any:
|
|
@@ -20,7 +20,7 @@ from .models import (
|
|
|
20
20
|
StructValue,
|
|
21
21
|
)
|
|
22
22
|
from ..core import align, enum_storage_size, enum_storage_type_from_size
|
|
23
|
-
from ..enum_codec import ENUM_LABEL_RE
|
|
23
|
+
from ..enum_codec import ENUM_LABEL_RE, normalize_integer_for_storage
|
|
24
24
|
from ..schema import FieldDef
|
|
25
25
|
|
|
26
26
|
class PackerWriterMixin:
|
|
@@ -437,19 +437,28 @@ class PackerWriterMixin:
|
|
|
437
437
|
writer.write_struct("<B", 1 if bool(value) else 0)
|
|
438
438
|
return
|
|
439
439
|
if t == "S8":
|
|
440
|
-
writer.write_struct(
|
|
440
|
+
writer.write_struct(
|
|
441
|
+
"<b",
|
|
442
|
+
self._coerce_signed_storage(value, field_def, "S8"),
|
|
443
|
+
)
|
|
441
444
|
return
|
|
442
445
|
if t == "U8":
|
|
443
446
|
writer.write_struct("<B", self._coerce_int(value, field_def) & 0xFF)
|
|
444
447
|
return
|
|
445
448
|
if t == "S16":
|
|
446
|
-
writer.write_struct(
|
|
449
|
+
writer.write_struct(
|
|
450
|
+
"<h",
|
|
451
|
+
self._coerce_signed_storage(value, field_def, "S16"),
|
|
452
|
+
)
|
|
447
453
|
return
|
|
448
454
|
if t == "U16":
|
|
449
455
|
writer.write_struct("<H", self._coerce_int(value, field_def) & 0xFFFF)
|
|
450
456
|
return
|
|
451
457
|
if t in {"S32", "Sfix"}:
|
|
452
|
-
writer.write_struct(
|
|
458
|
+
writer.write_struct(
|
|
459
|
+
"<i",
|
|
460
|
+
self._coerce_signed_storage(value, field_def, "S32"),
|
|
461
|
+
)
|
|
453
462
|
return
|
|
454
463
|
if t == "Enum":
|
|
455
464
|
self._write_enum_value(writer, field_def, value)
|
|
@@ -458,7 +467,10 @@ class PackerWriterMixin:
|
|
|
458
467
|
writer.write_struct("<I", self._coerce_int(value, field_def) & 0xFFFFFFFF)
|
|
459
468
|
return
|
|
460
469
|
if t == "S64":
|
|
461
|
-
writer.write_struct(
|
|
470
|
+
writer.write_struct(
|
|
471
|
+
"<q",
|
|
472
|
+
self._coerce_signed_storage(value, field_def, "S64"),
|
|
473
|
+
)
|
|
462
474
|
return
|
|
463
475
|
if t == "U64":
|
|
464
476
|
writer.write_struct(
|
|
@@ -617,6 +629,21 @@ class PackerWriterMixin:
|
|
|
617
629
|
# remain binary-compatible.
|
|
618
630
|
writer.write(b"\x00" * (value.declared_size - consumed))
|
|
619
631
|
|
|
632
|
+
def _coerce_signed_storage(
|
|
633
|
+
self, value: Any, field_def: FieldDef, storage_type: str
|
|
634
|
+
) -> int:
|
|
635
|
+
"""Accept a signed runtime value or its canonical unsigned bit pattern."""
|
|
636
|
+
number = self._coerce_int(value, field_def)
|
|
637
|
+
bits = int(storage_type[1:])
|
|
638
|
+
signed_minimum = -(1 << (bits - 1))
|
|
639
|
+
unsigned_maximum = (1 << bits) - 1
|
|
640
|
+
if number < signed_minimum or number > unsigned_maximum:
|
|
641
|
+
raise PackError(
|
|
642
|
+
f"value {number} is outside the signed or canonical unsigned "
|
|
643
|
+
f"{storage_type} range for field {field_def.name}"
|
|
644
|
+
)
|
|
645
|
+
return normalize_integer_for_storage(number, storage_type)
|
|
646
|
+
|
|
620
647
|
def _coerce_int(self, value: Any, field_def: FieldDef) -> int:
|
|
621
648
|
"""Coerce int.
|
|
622
649
|
|
|
@@ -139,6 +139,84 @@ class EnumBinaryTests(unittest.TestCase):
|
|
|
139
139
|
|
|
140
140
|
self.assertEqual(bytes(writer.data), b"\x02\x00\x00\x00")
|
|
141
141
|
|
|
142
|
+
def test_erased_fixed_field_accepts_unsigned_numeric_label(self):
|
|
143
|
+
writer_mixin = EnumFieldWriter()
|
|
144
|
+
writer = BinaryWriter()
|
|
145
|
+
field = FieldDef(
|
|
146
|
+
name="_StageID_Fixed",
|
|
147
|
+
field_type="S32",
|
|
148
|
+
original_type="System.Int32",
|
|
149
|
+
size=4,
|
|
150
|
+
align=4,
|
|
151
|
+
is_array=False,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
writer_mixin._write_scalar(writer, field, "[3068809728] ST101")
|
|
155
|
+
|
|
156
|
+
self.assertEqual(bytes(writer.data), bytes.fromhex("00 52 ea b6"))
|
|
157
|
+
|
|
158
|
+
def test_erased_fixed_field_accepts_signed_runtime_label(self):
|
|
159
|
+
writer_mixin = EnumFieldWriter()
|
|
160
|
+
writer = BinaryWriter()
|
|
161
|
+
field = FieldDef(
|
|
162
|
+
name="_StageID_Fixed",
|
|
163
|
+
field_type="S32",
|
|
164
|
+
original_type="System.Int32",
|
|
165
|
+
size=4,
|
|
166
|
+
align=4,
|
|
167
|
+
is_array=False,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
writer_mixin._write_scalar(writer, field, "[-1226157568] ST101")
|
|
171
|
+
|
|
172
|
+
self.assertEqual(bytes(writer.data), bytes.fromhex("00 52 ea b6"))
|
|
173
|
+
|
|
174
|
+
def test_erased_fixed_fields_accept_runtime_and_canonical_signed_widths(self):
|
|
175
|
+
writer_mixin = EnumFieldWriter()
|
|
176
|
+
cases = (
|
|
177
|
+
("S8", "System.SByte", 1, "[255] MinusOne"),
|
|
178
|
+
("S16", "System.Int16", 2, "[65535] MinusOne"),
|
|
179
|
+
("S32", "System.Int32", 4, "[4294967295] MinusOne"),
|
|
180
|
+
(
|
|
181
|
+
"S64",
|
|
182
|
+
"System.Int64",
|
|
183
|
+
8,
|
|
184
|
+
"[18446744073709551615] MinusOne",
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
for field_type, original_type, size, unsigned_label in cases:
|
|
189
|
+
with self.subTest(field_type=field_type):
|
|
190
|
+
field = FieldDef(
|
|
191
|
+
name="_Value_Fixed",
|
|
192
|
+
field_type=field_type,
|
|
193
|
+
original_type=original_type,
|
|
194
|
+
size=size,
|
|
195
|
+
align=size,
|
|
196
|
+
is_array=False,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
for label in ("[-1] MinusOne", unsigned_label):
|
|
200
|
+
writer = BinaryWriter()
|
|
201
|
+
|
|
202
|
+
writer_mixin._write_scalar(writer, field, label)
|
|
203
|
+
|
|
204
|
+
self.assertEqual(bytes(writer.data), b"\xff" * size)
|
|
205
|
+
|
|
206
|
+
def test_signed_field_rejects_values_outside_both_numeric_domains(self):
|
|
207
|
+
writer_mixin = EnumFieldWriter()
|
|
208
|
+
field = FieldDef(
|
|
209
|
+
name="_Value_Fixed",
|
|
210
|
+
field_type="S8",
|
|
211
|
+
original_type="System.SByte",
|
|
212
|
+
size=1,
|
|
213
|
+
align=1,
|
|
214
|
+
is_array=False,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
with self.assertRaisesRegex(PackError, "outside .* S8 range"):
|
|
218
|
+
writer_mixin._write_scalar(BinaryWriter(), field, "[256] Invalid")
|
|
219
|
+
|
|
142
220
|
def test_runtime_type_reader_uses_c8_string_layout(self):
|
|
143
221
|
parser = EnumFieldParser()
|
|
144
222
|
reader = BinaryReader(b"\x04\x00\x00\x00abc\x00tail")
|
|
@@ -54,6 +54,104 @@ class ExporterEnumSourceMixinTests(unittest.TestCase):
|
|
|
54
54
|
{"v2_Interpolation": "[1] SmoothStep"},
|
|
55
55
|
)
|
|
56
56
|
|
|
57
|
+
self.assertEqual(
|
|
58
|
+
postprocessor._postprocess_enum_nodes(
|
|
59
|
+
{"v2_Interpolation": 99},
|
|
60
|
+
current_class="via.ColorRampKey",
|
|
61
|
+
),
|
|
62
|
+
{"v2_Interpolation": 99},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def test_postprocess_respects_unsigned_fixed_storage(self):
|
|
66
|
+
postprocessor = EnumPostprocessor()
|
|
67
|
+
fixed_type = "app.FieldDef.STAGE_Fixed"
|
|
68
|
+
postprocessor.enum_lookup[fixed_type] = {
|
|
69
|
+
3068809728: ("ST101", 3068809728),
|
|
70
|
+
}
|
|
71
|
+
postprocessor.enum_underlying_types[fixed_type] = "U32"
|
|
72
|
+
postprocessor.class_field_fixed_types = {
|
|
73
|
+
"app.user_data.GrassCullingSetting.cStageData": {
|
|
74
|
+
"_StageID_Fixed": fixed_type,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
postprocessor._postprocess_enum_nodes(
|
|
80
|
+
{"_StageID_Fixed": -1226157568},
|
|
81
|
+
current_class="app.user_data.GrassCullingSetting.cStageData",
|
|
82
|
+
),
|
|
83
|
+
{"_StageID_Fixed": "[3068809728] ST101"},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_postprocess_displays_signed_runtime_fixed_id(self):
|
|
87
|
+
postprocessor = EnumPostprocessor()
|
|
88
|
+
fixed_type = "app.FieldDef.STAGE_Fixed"
|
|
89
|
+
postprocessor.enum_lookup[fixed_type] = {
|
|
90
|
+
3068809728: ("ST101", 3068809728),
|
|
91
|
+
}
|
|
92
|
+
postprocessor.enum_underlying_types[fixed_type] = "S32"
|
|
93
|
+
postprocessor.class_field_fixed_types = {
|
|
94
|
+
"app.user_data.GrassCullingSetting.cStageData": {
|
|
95
|
+
"_StageID_Fixed": fixed_type,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
self.assertEqual(
|
|
100
|
+
postprocessor._postprocess_enum_nodes(
|
|
101
|
+
{"_StageID_Fixed": -1226157568},
|
|
102
|
+
current_class="app.user_data.GrassCullingSetting.cStageData",
|
|
103
|
+
),
|
|
104
|
+
{"_StageID_Fixed": "[-1226157568] ST101"},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def test_repack_keeps_canonical_unsigned_fixed_id(self):
|
|
108
|
+
postprocessor = EnumPostprocessor()
|
|
109
|
+
fixed_type = "app.FieldDef.STAGE_Fixed"
|
|
110
|
+
postprocessor.enum_lookup[fixed_type] = {
|
|
111
|
+
3068809728: ("ST101", 3068809728),
|
|
112
|
+
}
|
|
113
|
+
postprocessor.enum_underlying_types[fixed_type] = "S32"
|
|
114
|
+
postprocessor.class_field_fixed_types = {
|
|
115
|
+
"app.user_data.GrassCullingSetting.cStageData": {
|
|
116
|
+
"_StageID_Fixed": fixed_type,
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
self.assertEqual(
|
|
121
|
+
postprocessor._postprocess_enum_nodes(
|
|
122
|
+
{"_StageID_Fixed": -1226157568},
|
|
123
|
+
current_class="app.user_data.GrassCullingSetting.cStageData",
|
|
124
|
+
output_mode="repack",
|
|
125
|
+
),
|
|
126
|
+
{"_StageID_Fixed": "[3068809728] ST101"},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def test_unsigned_fixed_type_keeps_unknown_value_as_structured_label(self):
|
|
130
|
+
postprocessor = EnumPostprocessor()
|
|
131
|
+
fixed_type = "app.FieldDef.STAGE_Fixed"
|
|
132
|
+
postprocessor.enum_lookup[fixed_type] = {
|
|
133
|
+
3068809728: ("ST101", 3068809728),
|
|
134
|
+
}
|
|
135
|
+
postprocessor.enum_underlying_types[fixed_type] = "U32"
|
|
136
|
+
|
|
137
|
+
self.assertEqual(
|
|
138
|
+
postprocessor._format_enum_value(fixed_type, -1),
|
|
139
|
+
"[4294967295] <unknown>",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def test_signed_fixed_enum_unknown_value_uses_runtime_fixed_id(self):
|
|
143
|
+
postprocessor = EnumPostprocessor()
|
|
144
|
+
fixed_type = "app.StageDef.StageID_Fixed"
|
|
145
|
+
postprocessor.enum_lookup[fixed_type] = {
|
|
146
|
+
884165440: ("st200", 884165440),
|
|
147
|
+
}
|
|
148
|
+
postprocessor.enum_underlying_types[fixed_type] = "S32"
|
|
149
|
+
|
|
150
|
+
self.assertEqual(
|
|
151
|
+
postprocessor._format_enum_value(fixed_type, -521343680),
|
|
152
|
+
"[-521343680] <unknown>",
|
|
153
|
+
)
|
|
154
|
+
|
|
57
155
|
def test_export_enums_internal_skips_incomplete_enum_entries(self):
|
|
58
156
|
dump = {
|
|
59
157
|
"app.Mode_Fixed": {
|
|
@@ -153,6 +251,137 @@ class ExporterEnumSourceMixinTests(unittest.TestCase):
|
|
|
153
251
|
},
|
|
154
252
|
)
|
|
155
253
|
|
|
254
|
+
def test_enum_context_infers_integer_fixed_backing_field_from_getter(self):
|
|
255
|
+
dump = {
|
|
256
|
+
"app.FieldDef.STAGE": {
|
|
257
|
+
"parent": "System.Enum",
|
|
258
|
+
"fields": {
|
|
259
|
+
"value__": {"type": "System.Int32"},
|
|
260
|
+
"ST101": {"default": 0},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
"app.FieldDef.STAGE_Fixed": {
|
|
264
|
+
"parent": "System.Enum",
|
|
265
|
+
"fields": {
|
|
266
|
+
"value__": {"type": "System.UInt32"},
|
|
267
|
+
"ST101": {"default": 3068809728},
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
"app.user_data.GrassCullingSetting.cStageData": {
|
|
271
|
+
"fields": {
|
|
272
|
+
"_StageID_Fixed": {"type": "System.Int32"},
|
|
273
|
+
},
|
|
274
|
+
"RSZ": [
|
|
275
|
+
{
|
|
276
|
+
"potential_name": "_StageID_Fixed",
|
|
277
|
+
"type": "System.Int32",
|
|
278
|
+
}
|
|
279
|
+
],
|
|
280
|
+
"methods": {
|
|
281
|
+
"get_StageID1185265": {
|
|
282
|
+
"returns": {"type": "app.FieldDef.STAGE"},
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
context = ExporterEnumSourceMixin.export_enum_context_internal(dump)
|
|
289
|
+
|
|
290
|
+
self.assertEqual(
|
|
291
|
+
context["class_field_fixed_types"][
|
|
292
|
+
"app.user_data.GrassCullingSetting.cStageData"
|
|
293
|
+
],
|
|
294
|
+
{"_StageID_Fixed": "app.FieldDef.STAGE_Fixed"},
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def test_enum_context_does_not_guess_fixed_type_without_enum_pair(self):
|
|
298
|
+
dump = {
|
|
299
|
+
"app.FieldDef.STAGE": {
|
|
300
|
+
"parent": "System.Enum",
|
|
301
|
+
"fields": {"ST101": {"default": 0}},
|
|
302
|
+
},
|
|
303
|
+
"app.Owner": {
|
|
304
|
+
"fields": {"_StageID_Fixed": {"type": "System.Int32"}},
|
|
305
|
+
"methods": {
|
|
306
|
+
"get_StageID1": {
|
|
307
|
+
"returns": {"type": "app.FieldDef.STAGE"},
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
context = ExporterEnumSourceMixin.export_enum_context_internal(dump)
|
|
314
|
+
|
|
315
|
+
self.assertNotIn("app.Owner", context["class_field_fixed_types"])
|
|
316
|
+
|
|
317
|
+
def test_enum_context_infers_integer_backing_field_from_fixed_getter(self):
|
|
318
|
+
dump = {
|
|
319
|
+
"app.StageDef.StageID_Fixed": {
|
|
320
|
+
"parent": "System.Enum",
|
|
321
|
+
"fields": {
|
|
322
|
+
"value__": {"type": "System.UInt32"},
|
|
323
|
+
"st100": {"default": 1769129856},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
"app.user_data.WindGlobalAnimationData.cData": {
|
|
327
|
+
"fields": {
|
|
328
|
+
"_StageID_Fixed": {"type": "System.Int32"},
|
|
329
|
+
},
|
|
330
|
+
"methods": {
|
|
331
|
+
"get_StageID_Fixed332574": {
|
|
332
|
+
"returns": {"type": "app.StageDef.StageID_Fixed"},
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
context = ExporterEnumSourceMixin.export_enum_context_internal(dump)
|
|
339
|
+
|
|
340
|
+
self.assertEqual(
|
|
341
|
+
context["class_field_fixed_types"][
|
|
342
|
+
"app.user_data.WindGlobalAnimationData.cData"
|
|
343
|
+
]["_StageID_Fixed"],
|
|
344
|
+
"app.StageDef.StageID_Fixed",
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def test_enum_context_preserves_digits_from_explicit_property_name(self):
|
|
348
|
+
dump = {
|
|
349
|
+
"app.LOD": {
|
|
350
|
+
"parent": "System.Enum",
|
|
351
|
+
"fields": {
|
|
352
|
+
"value__": {"type": "System.Int32"},
|
|
353
|
+
"LOD0": {"default": 0},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
"app.LOD_Fixed": {
|
|
357
|
+
"parent": "System.Enum",
|
|
358
|
+
"fields": {
|
|
359
|
+
"value__": {"type": "System.Int32"},
|
|
360
|
+
"LOD0": {"default": 123},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
"app.Owner": {
|
|
364
|
+
"fields": {
|
|
365
|
+
"_LOD0_Fixed": {"type": "System.Int32"},
|
|
366
|
+
},
|
|
367
|
+
"properties": {
|
|
368
|
+
"LOD0": {"getter": "get_LOD0"},
|
|
369
|
+
},
|
|
370
|
+
"methods": {
|
|
371
|
+
"get_LOD0123456": {
|
|
372
|
+
"returns": {"type": "app.LOD"},
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
context = ExporterEnumSourceMixin.export_enum_context_internal(dump)
|
|
379
|
+
|
|
380
|
+
self.assertEqual(
|
|
381
|
+
context["class_field_fixed_types"]["app.Owner"]["_LOD0_Fixed"],
|
|
382
|
+
"app.LOD_Fixed",
|
|
383
|
+
)
|
|
384
|
+
|
|
156
385
|
def test_generic_context_supports_ordinary_enum_wrappers(self):
|
|
157
386
|
dump = {
|
|
158
387
|
"app.Mode": {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|