PyREUser3 0.4.0__tar.gz → 0.6.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.4.0 → pyreuser3-0.6.0}/PKG-INFO +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/PKG-INFO +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/SOURCES.txt +2 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/README.md +7 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/docs/README.zh-CN.md +5 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyproject.toml +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/core.py +3 -1
- pyreuser3-0.6.0/pyreuser3/enum_codec.py +231 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/base.py +3 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/enums.py +36 -20
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/fields.py +7 -4
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/metadata.py +39 -12
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/postprocess.py +97 -7
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/tree.py +11 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/user3.py +16 -7
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/pack/base.py +35 -2
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/pack/plan.py +118 -15
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/pack/writer.py +3 -8
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/schema.py +94 -2
- pyreuser3-0.6.0/tests/test_enum_binary.py +237 -0
- pyreuser3-0.6.0/tests/test_enum_codec.py +200 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/tests/test_export_enums.py +46 -0
- pyreuser3-0.4.0/tests/test_enum_binary.py +0 -92
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/LICENSE +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/MANIFEST.in +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/docs/PYPI.md +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/api.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/cli.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/pack/models.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.6.0}/setup.cfg +0 -0
|
@@ -15,6 +15,7 @@ pyreuser3/__main__.py
|
|
|
15
15
|
pyreuser3/api.py
|
|
16
16
|
pyreuser3/cli.py
|
|
17
17
|
pyreuser3/core.py
|
|
18
|
+
pyreuser3/enum_codec.py
|
|
18
19
|
pyreuser3/rich_ui.py
|
|
19
20
|
pyreuser3/schema.py
|
|
20
21
|
pyreuser3/export/__init__.py
|
|
@@ -40,4 +41,5 @@ pyreuser3/web/runners.py
|
|
|
40
41
|
pyreuser3/web/server.py
|
|
41
42
|
pyreuser3/web/settings.py
|
|
42
43
|
tests/test_enum_binary.py
|
|
44
|
+
tests/test_enum_codec.py
|
|
43
45
|
tests/test_export_enums.py
|
|
@@ -112,6 +112,13 @@ repack_data = converter.user3_to_json(
|
|
|
112
112
|
|
|
113
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()`.
|
|
114
114
|
|
|
115
|
+
Enum fields are rendered as `[numeric] Name` labels using the enum's actual storage
|
|
116
|
+
width. Scalar flag enums are rendered as arrays of labels. ``ace.Bitset`1<T>`` values are
|
|
117
|
+
rendered as enum-index labels together with `_MaxElement` and `_WordCount`, so unknown
|
|
118
|
+
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.
|
|
121
|
+
|
|
115
122
|
For stable patch-and-repack workflows, use `patch_file()` or `parse_pack_file()`:
|
|
116
123
|
|
|
117
124
|
```python
|
|
@@ -140,6 +140,11 @@ repack_data = converter.user3_to_json(
|
|
|
140
140
|
|
|
141
141
|
使用 `json_format="readable"` 时返回与 `export_file()` 一致的可读导出结构;使用 `json_format="repack"` 时返回可传给 `pack()` 的完整实例表结构。
|
|
142
142
|
|
|
143
|
+
枚举字段会按照真实底层存储宽度输出为 `[数值] 名称`。标量位标志枚举输出为标签数组;
|
|
144
|
+
``ace.Bitset`1<T>`` 输出为枚举索引标签,并保留 `_MaxElement` 与 `_WordCount`,因此未知位和
|
|
145
|
+
填充词也可以无损封回。新的 repack 文档格式为 `re_user3_pack_v2`,封回器仍兼容使用原始
|
|
146
|
+
数值词数组的 v1 文档。
|
|
147
|
+
|
|
143
148
|
批量处理目录:
|
|
144
149
|
|
|
145
150
|
```python
|
|
@@ -20,7 +20,9 @@ USR_MAGIC = 5395285
|
|
|
20
20
|
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
26
|
# Decode strings and GUID-like values conservatively so invalid data does not corrupt
|
|
25
27
|
# subsequent parsing.
|
|
26
28
|
HEX32_RE = re.compile(r"^[0-9a-fA-F]{32}$")
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Shared enum, flag, and ``ace.Bitset`` conversion helpers.
|
|
2
|
+
|
|
3
|
+
The exporter and packer deliberately share these operations so readable labels remain
|
|
4
|
+
lossless across signed/unsigned storage widths and generic container wrappers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
ENUM_LABEL_RE = re.compile(r"^\[(-?\d+)\]\s*(.*)$")
|
|
14
|
+
_GENERIC_RE = re.compile(r"^[^<]+<(.+)>$")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def normalize_integer_for_storage(value: int, storage_type: str | None) -> int:
|
|
18
|
+
"""Normalize an integer according to an enum's declared storage width/sign."""
|
|
19
|
+
storage = storage_type or "S32"
|
|
20
|
+
widths = {
|
|
21
|
+
"S8": (8, True),
|
|
22
|
+
"U8": (8, False),
|
|
23
|
+
"S16": (16, True),
|
|
24
|
+
"U16": (16, False),
|
|
25
|
+
"S32": (32, True),
|
|
26
|
+
"U32": (32, False),
|
|
27
|
+
"S64": (64, True),
|
|
28
|
+
"U64": (64, False),
|
|
29
|
+
}
|
|
30
|
+
bits, signed = widths.get(storage, (32, True))
|
|
31
|
+
mask = (1 << bits) - 1
|
|
32
|
+
normalized = int(value) & mask
|
|
33
|
+
if signed and normalized >= 1 << (bits - 1):
|
|
34
|
+
normalized -= 1 << bits
|
|
35
|
+
return normalized
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def parse_enum_label(value: Any) -> int | None:
|
|
39
|
+
"""Return the authoritative numeric prefix from ``[N] Name`` labels."""
|
|
40
|
+
if isinstance(value, bool):
|
|
41
|
+
return int(value)
|
|
42
|
+
if isinstance(value, int):
|
|
43
|
+
return value
|
|
44
|
+
if not isinstance(value, str):
|
|
45
|
+
return None
|
|
46
|
+
match = ENUM_LABEL_RE.match(value.strip())
|
|
47
|
+
if match:
|
|
48
|
+
return int(match.group(1))
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def enum_member_for_value(
|
|
53
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
54
|
+
enum_type: str,
|
|
55
|
+
value: int,
|
|
56
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
57
|
+
) -> tuple[str, int] | None:
|
|
58
|
+
"""Resolve a value without inventing 32-bit aliases for wider enum types."""
|
|
59
|
+
value_map = enum_lookup.get(enum_type)
|
|
60
|
+
if not value_map:
|
|
61
|
+
return None
|
|
62
|
+
storage = (enum_underlying_types or {}).get(enum_type, "S32")
|
|
63
|
+
target = normalize_integer_for_storage(value, storage)
|
|
64
|
+
direct = value_map.get(value)
|
|
65
|
+
if direct is not None and normalize_integer_for_storage(direct[1], storage) == target:
|
|
66
|
+
return direct
|
|
67
|
+
for member_name, raw_value in value_map.values():
|
|
68
|
+
if normalize_integer_for_storage(raw_value, storage) == target:
|
|
69
|
+
return member_name, raw_value
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def generic_arguments(type_name: str) -> list[str]:
|
|
74
|
+
"""Parse the simple, fully-qualified generic type names emitted by il2cpp dumps."""
|
|
75
|
+
if not isinstance(type_name, str):
|
|
76
|
+
return []
|
|
77
|
+
match = _GENERIC_RE.match(type_name)
|
|
78
|
+
if not match:
|
|
79
|
+
return []
|
|
80
|
+
# These metadata names are not assembly-qualified and current RE generic arguments
|
|
81
|
+
# are flat. Keeping this parser intentionally strict prevents ambiguous guesses.
|
|
82
|
+
return [part.strip() for part in match.group(1).split(",") if part.strip()]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def bitset_enum_type(class_name: str) -> str | None:
|
|
86
|
+
"""Return the generic enum argument for one ``ace.Bitset`1`` class."""
|
|
87
|
+
if not isinstance(class_name, str) or not class_name.startswith("ace.Bitset`1<"):
|
|
88
|
+
return None
|
|
89
|
+
args = generic_arguments(class_name)
|
|
90
|
+
return args[0] if len(args) == 1 else None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def is_probable_flags_enum(
|
|
94
|
+
enum_type: str,
|
|
95
|
+
value_map: dict[int, tuple[str, int]],
|
|
96
|
+
storage_type: str | None = None,
|
|
97
|
+
) -> bool:
|
|
98
|
+
"""Conservatively identify bit-mask enums from their name and member values."""
|
|
99
|
+
upper = enum_type.upper()
|
|
100
|
+
markers = ("BIT", "BITS", "FLAG", "FLAGS", "MASK", "ATTR")
|
|
101
|
+
if not any(marker in upper for marker in markers):
|
|
102
|
+
return False
|
|
103
|
+
storage = storage_type or "S32"
|
|
104
|
+
width = int(storage[1:])
|
|
105
|
+
mask = (1 << width) - 1
|
|
106
|
+
values = {
|
|
107
|
+
normalize_integer_for_storage(raw, storage) & mask
|
|
108
|
+
for _name, raw in value_map.values()
|
|
109
|
+
if raw != 0
|
|
110
|
+
}
|
|
111
|
+
basis = {value for value in values if value and value & (value - 1) == 0}
|
|
112
|
+
if len(basis) < 2:
|
|
113
|
+
return False
|
|
114
|
+
known_mask = 0
|
|
115
|
+
for value in basis:
|
|
116
|
+
known_mask |= value
|
|
117
|
+
return all(value & ~known_mask == 0 for value in values)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def decode_flags(
|
|
121
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
122
|
+
enum_type: str,
|
|
123
|
+
value: int,
|
|
124
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
125
|
+
) -> list[str]:
|
|
126
|
+
"""Expand a scalar mask to stable labels, preserving unknown bits explicitly."""
|
|
127
|
+
storage = (enum_underlying_types or {}).get(enum_type, "S32")
|
|
128
|
+
normalized = normalize_integer_for_storage(value, storage)
|
|
129
|
+
bits = normalized if normalized >= 0 else normalized & ((1 << int(storage[1:])) - 1)
|
|
130
|
+
value_map = enum_lookup.get(enum_type, {})
|
|
131
|
+
if bits == 0:
|
|
132
|
+
zero = enum_member_for_value(enum_lookup, enum_type, 0, enum_underlying_types)
|
|
133
|
+
return [f"[0] {zero[0]}"] if zero else []
|
|
134
|
+
labels: list[str] = []
|
|
135
|
+
remaining = bits
|
|
136
|
+
width_mask = (1 << int(storage[1:])) - 1
|
|
137
|
+
members: list[tuple[int, str, int]] = []
|
|
138
|
+
for name, raw in value_map.values():
|
|
139
|
+
normalized_raw = normalize_integer_for_storage(raw, storage)
|
|
140
|
+
mask_value = normalized_raw & width_mask
|
|
141
|
+
if mask_value and mask_value & (mask_value - 1) == 0:
|
|
142
|
+
members.append((mask_value, name, raw))
|
|
143
|
+
for mask_value, name, raw in sorted(members):
|
|
144
|
+
if remaining & mask_value:
|
|
145
|
+
labels.append(f"[{raw}] {name}")
|
|
146
|
+
remaining &= ~mask_value
|
|
147
|
+
bit_index = 0
|
|
148
|
+
while remaining:
|
|
149
|
+
if remaining & 1:
|
|
150
|
+
unknown_value = 1 << bit_index
|
|
151
|
+
labels.append(f"[{unknown_value}] <unknown>")
|
|
152
|
+
remaining >>= 1
|
|
153
|
+
bit_index += 1
|
|
154
|
+
return labels
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def decode_bitset(
|
|
158
|
+
words: list[int],
|
|
159
|
+
enum_type: str,
|
|
160
|
+
enum_lookup: dict[str, dict[int, tuple[str, int]]],
|
|
161
|
+
max_element: int | None = None,
|
|
162
|
+
enum_underlying_types: dict[str, str] | None = None,
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
"""Decode 32-bit words into enum-index labels."""
|
|
165
|
+
labels: list[str] = []
|
|
166
|
+
for word_index, raw_word in enumerate(words):
|
|
167
|
+
word = int(raw_word) & 0xFFFFFFFF
|
|
168
|
+
for bit in range(32):
|
|
169
|
+
index = word_index * 32 + bit
|
|
170
|
+
if not (word & (1 << bit)):
|
|
171
|
+
continue
|
|
172
|
+
member = enum_member_for_value(
|
|
173
|
+
enum_lookup, enum_type, index, enum_underlying_types
|
|
174
|
+
)
|
|
175
|
+
labels.append(f"[{index}] {member[0] if member else '<unknown>'}")
|
|
176
|
+
return labels
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def encode_bitset(
|
|
180
|
+
labels: list[Any],
|
|
181
|
+
enum_type: str,
|
|
182
|
+
member_lookup: dict[str, dict[str, int]],
|
|
183
|
+
max_element: int | None = None,
|
|
184
|
+
word_count: int | None = None,
|
|
185
|
+
) -> list[int]:
|
|
186
|
+
"""Encode enum-index labels into 32-bit words, preserving requested padding."""
|
|
187
|
+
indexes: list[int] = []
|
|
188
|
+
members = member_lookup.get(enum_type, {})
|
|
189
|
+
for item in labels:
|
|
190
|
+
index = parse_enum_label(item)
|
|
191
|
+
if index is None and isinstance(item, str):
|
|
192
|
+
index = members.get(item.strip())
|
|
193
|
+
if index is None or index < 0:
|
|
194
|
+
raise ValueError(f"invalid {enum_type} bitset member: {item!r}")
|
|
195
|
+
if (
|
|
196
|
+
max_element is not None
|
|
197
|
+
and index >= max_element
|
|
198
|
+
and (word_count is None or index >= word_count * 32)
|
|
199
|
+
):
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"bitset index {index} exceeds _MaxElement {max_element} for {enum_type}"
|
|
202
|
+
)
|
|
203
|
+
indexes.append(index)
|
|
204
|
+
selected_minimum = (max(indexes) + 32) // 32 if indexes else 0
|
|
205
|
+
derived_count = max(
|
|
206
|
+
selected_minimum,
|
|
207
|
+
(max_element + 31) // 32 if max_element else 0,
|
|
208
|
+
)
|
|
209
|
+
count = derived_count if word_count is None else word_count
|
|
210
|
+
if count < selected_minimum:
|
|
211
|
+
raise ValueError(f"_WordCount {count} is too small for {enum_type} bitset")
|
|
212
|
+
words = [0] * count
|
|
213
|
+
for index in indexes:
|
|
214
|
+
words[index // 32] |= 1 << (index % 32)
|
|
215
|
+
return words
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def encode_flags(
|
|
219
|
+
values: list[Any], enum_type: str, member_lookup: dict[str, dict[str, int]]
|
|
220
|
+
) -> int:
|
|
221
|
+
"""Combine readable scalar flag labels back into one integer mask."""
|
|
222
|
+
result = 0
|
|
223
|
+
members = member_lookup.get(enum_type, {})
|
|
224
|
+
for item in values:
|
|
225
|
+
numeric = parse_enum_label(item)
|
|
226
|
+
if numeric is None and isinstance(item, str):
|
|
227
|
+
numeric = members.get(item.strip())
|
|
228
|
+
if numeric is None:
|
|
229
|
+
raise ValueError(f"invalid {enum_type} flag member: {item!r}")
|
|
230
|
+
result |= numeric
|
|
231
|
+
return result
|
|
@@ -87,8 +87,11 @@ class User3Exporter(
|
|
|
87
87
|
self.class_field_fixed_types: dict[str, dict[str, str]] = {}
|
|
88
88
|
self.serializable_to_fixed: dict[str, str] = {}
|
|
89
89
|
self.generic_container_rules: dict[str, tuple[str, str]] = {}
|
|
90
|
+
self.generic_scalar_rules: dict[str, str] = {}
|
|
91
|
+
self.bitset_rules: dict[str, str] = {}
|
|
90
92
|
self.param_type_default_enum: dict[str, str] = {}
|
|
91
93
|
self.enum_underlying_types: dict[str, str] = {}
|
|
94
|
+
self.enum_flags: set[str] = set()
|
|
92
95
|
self.enum_member_to_types: dict[str, list[str]] = {}
|
|
93
96
|
self._pending_enum_context: dict | None = None
|
|
94
97
|
|
|
@@ -258,6 +258,9 @@ class ExporterEnumSourceMixin:
|
|
|
258
258
|
"class_field_fixed_types": {},
|
|
259
259
|
"serializable_to_fixed": {},
|
|
260
260
|
"generic_container_rules": {},
|
|
261
|
+
"generic_scalar_rules": {},
|
|
262
|
+
"bitset_rules": {},
|
|
263
|
+
"generic_candidates": {},
|
|
261
264
|
"enum_underlying_types": {},
|
|
262
265
|
"enum_types": set(),
|
|
263
266
|
"serializable_fallback_candidates": [],
|
|
@@ -271,7 +274,7 @@ class ExporterEnumSourceMixin:
|
|
|
271
274
|
if not isinstance(class_name, str) or not isinstance(obj, dict):
|
|
272
275
|
return
|
|
273
276
|
|
|
274
|
-
if
|
|
277
|
+
if obj.get("parent") == "System.Enum":
|
|
275
278
|
state["enum_types"].add(class_name)
|
|
276
279
|
if obj.get("parent") == "System.Enum":
|
|
277
280
|
storage_type = cls._extract_enum_underlying_type(obj)
|
|
@@ -383,25 +386,18 @@ class ExporterEnumSourceMixin:
|
|
|
383
386
|
state["serializable_fallback_candidates"].append(class_name)
|
|
384
387
|
|
|
385
388
|
generic_args = obj.get("generic_arg_types")
|
|
386
|
-
if isinstance(generic_args, list)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
enum_types_unique = list(dict.fromkeys(enum_types_in_args))
|
|
399
|
-
param_types_unique = list(dict.fromkeys(param_types))
|
|
400
|
-
if len(enum_types_unique) == 1 and len(param_types_unique) == 1:
|
|
401
|
-
state["generic_container_rules"][class_name] = {
|
|
402
|
-
"param_type": param_types_unique[0],
|
|
403
|
-
"enum_type": enum_types_unique[0],
|
|
404
|
-
}
|
|
389
|
+
if isinstance(generic_args, list):
|
|
390
|
+
raw_types = [
|
|
391
|
+
arg.get("type")
|
|
392
|
+
for arg in generic_args
|
|
393
|
+
if isinstance(arg, dict)
|
|
394
|
+
and isinstance(arg.get("type"), str)
|
|
395
|
+
and arg.get("type") != "unknown"
|
|
396
|
+
]
|
|
397
|
+
if raw_types:
|
|
398
|
+
# Resolution is deferred until every top-level enum entry has been
|
|
399
|
+
# observed; il2cpp dump ordering is not a semantic contract.
|
|
400
|
+
state["generic_candidates"][class_name] = raw_types
|
|
405
401
|
|
|
406
402
|
@classmethod
|
|
407
403
|
def _finalize_enum_context_state(cls, state: dict[str, Any]) -> dict:
|
|
@@ -415,10 +411,30 @@ class ExporterEnumSourceMixin:
|
|
|
415
411
|
if len(fallback_types) == 1:
|
|
416
412
|
serializable_to_fixed[class_name] = fallback_types[0]
|
|
417
413
|
|
|
414
|
+
for class_name, raw_types in state["generic_candidates"].items():
|
|
415
|
+
enum_args = list(dict.fromkeys(t for t in raw_types if t in enum_types))
|
|
416
|
+
param_args = list(dict.fromkeys(t for t in raw_types if t not in enum_types))
|
|
417
|
+
if len(enum_args) != 1:
|
|
418
|
+
continue
|
|
419
|
+
enum_type = enum_args[0]
|
|
420
|
+
if class_name.startswith("ace.Bitset`1<"):
|
|
421
|
+
state["bitset_rules"][class_name] = enum_type
|
|
422
|
+
elif class_name.startswith("ace.btable.cEditFieldEnum`1<"):
|
|
423
|
+
state["generic_scalar_rules"][class_name] = enum_type
|
|
424
|
+
elif len(param_args) == 1 and (
|
|
425
|
+
enum_type.endswith("_Fixed") or "cEnumerableParam" in class_name
|
|
426
|
+
):
|
|
427
|
+
state["generic_container_rules"][class_name] = {
|
|
428
|
+
"param_type": param_args[0],
|
|
429
|
+
"enum_type": enum_type,
|
|
430
|
+
}
|
|
431
|
+
|
|
418
432
|
return {
|
|
419
433
|
"class_field_fixed_types": state["class_field_fixed_types"],
|
|
420
434
|
"serializable_to_fixed": serializable_to_fixed,
|
|
421
435
|
"generic_container_rules": state["generic_container_rules"],
|
|
436
|
+
"generic_scalar_rules": state["generic_scalar_rules"],
|
|
437
|
+
"bitset_rules": state["bitset_rules"],
|
|
422
438
|
"enum_underlying_types": state["enum_underlying_types"],
|
|
423
439
|
}
|
|
424
440
|
|
|
@@ -74,7 +74,7 @@ class ExporterFieldParserMixin:
|
|
|
74
74
|
return {"ref_instance_id": reader.read_s32()}
|
|
75
75
|
if t in ("String", "Resource"):
|
|
76
76
|
return read_len_utf16(reader)
|
|
77
|
-
if t
|
|
77
|
+
if t in ("C8", "RuntimeType"):
|
|
78
78
|
return read_len_c8(reader)
|
|
79
79
|
if t in ("Guid", "GameObjectRef", "Uri"):
|
|
80
80
|
return read_guid_like(reader)
|
|
@@ -230,7 +230,7 @@ class ExporterFieldParserMixin:
|
|
|
230
230
|
t = field.field_type
|
|
231
231
|
if field.is_array:
|
|
232
232
|
pos += 4
|
|
233
|
-
elif t in ("String", "Resource", "C8"):
|
|
233
|
+
elif t in ("String", "Resource", "C8", "RuntimeType"):
|
|
234
234
|
pos += 4
|
|
235
235
|
elif t in ("Object", "UserData"):
|
|
236
236
|
pos += 4
|
|
@@ -250,7 +250,9 @@ class ExporterFieldParserMixin:
|
|
|
250
250
|
pos += max(field.size, 0)
|
|
251
251
|
return max(pos, 1)
|
|
252
252
|
|
|
253
|
-
def _parse_instance(
|
|
253
|
+
def _parse_instance(
|
|
254
|
+
self, reader: BinaryReader, class_hash: int, crc: int | None = None
|
|
255
|
+
) -> dict[str, Any]:
|
|
254
256
|
"""Parse instance.
|
|
255
257
|
|
|
256
258
|
The method keeps parsing, metadata lookup, and JSON shaping explicit so incomplete
|
|
@@ -259,6 +261,7 @@ class ExporterFieldParserMixin:
|
|
|
259
261
|
Args:
|
|
260
262
|
reader (BinaryReader): BinaryReader positioned at the value to parse.
|
|
261
263
|
class_hash (int): RE_RSZ type hash for a class.
|
|
264
|
+
crc (int | None): Optional CRC used to choose among same-hash layout variants.
|
|
262
265
|
|
|
263
266
|
Returns:
|
|
264
267
|
dict[str, Any]: JSON-compatible dictionary for API or conversion callers.
|
|
@@ -266,7 +269,7 @@ class ExporterFieldParserMixin:
|
|
|
266
269
|
Raises:
|
|
267
270
|
ParseError: Binary data did not match the expected .user.3 or RSZ layout.
|
|
268
271
|
"""
|
|
269
|
-
cls = self.typedb.get_class(class_hash)
|
|
272
|
+
cls = self.typedb.get_class(class_hash, crc)
|
|
270
273
|
if cls is None:
|
|
271
274
|
raise ParseError(f"class hash 0x{class_hash:08x} not found in schema")
|
|
272
275
|
out: dict[str, Any] = {"_class": cls.name, "fields": {}}
|
|
@@ -11,6 +11,7 @@ from pathlib import Path
|
|
|
11
11
|
from typing import Any
|
|
12
12
|
|
|
13
13
|
from ..core import ParseError, enum_storage_type_from_size
|
|
14
|
+
from ..enum_codec import enum_member_for_value, is_probable_flags_enum
|
|
14
15
|
from ..rich_ui import get_console
|
|
15
16
|
|
|
16
17
|
|
|
@@ -100,8 +101,6 @@ class ExporterMetadataMixin:
|
|
|
100
101
|
# Register enum values through the shared lookup tables so readable
|
|
101
102
|
# labels and numeric packing stay reversible.
|
|
102
103
|
value_map[raw_value] = entry
|
|
103
|
-
value_map[self._to_s32(raw_value)] = entry
|
|
104
|
-
value_map[self._to_u32(raw_value)] = entry
|
|
105
104
|
if value_map:
|
|
106
105
|
lookup[enum_type] = value_map
|
|
107
106
|
return lookup
|
|
@@ -205,11 +204,12 @@ class ExporterMetadataMixin:
|
|
|
205
204
|
value_map = self.enum_lookup.get(enum_type)
|
|
206
205
|
if value_map is None:
|
|
207
206
|
continue
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
207
|
+
if enum_member_for_value(
|
|
208
|
+
self.enum_lookup,
|
|
209
|
+
enum_type,
|
|
210
|
+
value,
|
|
211
|
+
self.enum_underlying_types,
|
|
212
|
+
) is not None:
|
|
213
213
|
matched.append(enum_type)
|
|
214
214
|
if len(matched) == 1:
|
|
215
215
|
return matched[0]
|
|
@@ -232,8 +232,11 @@ class ExporterMetadataMixin:
|
|
|
232
232
|
self.class_field_fixed_types = {}
|
|
233
233
|
self.serializable_to_fixed = {}
|
|
234
234
|
self.generic_container_rules = {}
|
|
235
|
+
self.generic_scalar_rules = {}
|
|
236
|
+
self.bitset_rules = {}
|
|
235
237
|
self.param_type_default_enum = {}
|
|
236
238
|
self.enum_underlying_types = {}
|
|
239
|
+
self.enum_flags = set()
|
|
237
240
|
|
|
238
241
|
class_field_fixed_types = raw.get("class_field_fixed_types")
|
|
239
242
|
if isinstance(class_field_fixed_types, dict):
|
|
@@ -268,7 +271,6 @@ class ExporterMetadataMixin:
|
|
|
268
271
|
if (
|
|
269
272
|
isinstance(serializable_name, str)
|
|
270
273
|
and isinstance(fixed_name, str)
|
|
271
|
-
and fixed_name.endswith("_Fixed")
|
|
272
274
|
):
|
|
273
275
|
self.serializable_to_fixed[serializable_name] = fixed_name
|
|
274
276
|
|
|
@@ -285,7 +287,6 @@ class ExporterMetadataMixin:
|
|
|
285
287
|
if (
|
|
286
288
|
isinstance(param_type, str)
|
|
287
289
|
and isinstance(enum_type, str)
|
|
288
|
-
and enum_type.endswith("_Fixed")
|
|
289
290
|
):
|
|
290
291
|
self.generic_container_rules[container_name] = (
|
|
291
292
|
param_type,
|
|
@@ -299,6 +300,32 @@ class ExporterMetadataMixin:
|
|
|
299
300
|
# labels and numeric packing stay reversible.
|
|
300
301
|
self.param_type_default_enum[param_type] = next(iter(enum_types))
|
|
301
302
|
|
|
303
|
+
generic_scalar_rules = raw.get("generic_scalar_rules")
|
|
304
|
+
if isinstance(generic_scalar_rules, dict):
|
|
305
|
+
self.generic_scalar_rules = {
|
|
306
|
+
class_name: enum_type
|
|
307
|
+
for class_name, enum_type in generic_scalar_rules.items()
|
|
308
|
+
if isinstance(class_name, str)
|
|
309
|
+
and isinstance(enum_type, str)
|
|
310
|
+
and enum_type in self.enum_lookup
|
|
311
|
+
}
|
|
312
|
+
bitset_rules = raw.get("bitset_rules")
|
|
313
|
+
if isinstance(bitset_rules, dict):
|
|
314
|
+
self.bitset_rules = {
|
|
315
|
+
class_name: enum_type
|
|
316
|
+
for class_name, enum_type in bitset_rules.items()
|
|
317
|
+
if isinstance(class_name, str)
|
|
318
|
+
and isinstance(enum_type, str)
|
|
319
|
+
and enum_type in self.enum_lookup
|
|
320
|
+
}
|
|
321
|
+
self.enum_flags = {
|
|
322
|
+
enum_type
|
|
323
|
+
for enum_type, value_map in self.enum_lookup.items()
|
|
324
|
+
if is_probable_flags_enum(
|
|
325
|
+
enum_type, value_map, self.enum_underlying_types.get(enum_type)
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
302
329
|
def _enum_type_candidates_for_lookup(self, type_name: str) -> list[str]:
|
|
303
330
|
"""Yield enum lookup candidates for schema or il2cpp type names."""
|
|
304
331
|
candidates = [type_name]
|
|
@@ -337,11 +364,11 @@ class ExporterMetadataMixin:
|
|
|
337
364
|
if typedb is None or not enum_lookup:
|
|
338
365
|
return
|
|
339
366
|
|
|
340
|
-
|
|
367
|
+
iter_classes = getattr(typedb, "iter_classes", None)
|
|
368
|
+
class_defs = iter_classes() if callable(iter_classes) else typedb.classes.values()
|
|
369
|
+
for class_def in class_defs:
|
|
341
370
|
field_map = self.class_field_fixed_types.setdefault(class_def.name, {})
|
|
342
371
|
for field in class_def.fields:
|
|
343
|
-
if field.field_type != "Enum":
|
|
344
|
-
continue
|
|
345
372
|
enum_type = self._resolve_enum_type_for_lookup(field.original_type)
|
|
346
373
|
if enum_type is not None:
|
|
347
374
|
field_map.setdefault(field.name or "unnamed", enum_type)
|