PyREUser3 0.4.0__tar.gz → 0.5.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.5.0}/PKG-INFO +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/PKG-INFO +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyproject.toml +1 -1
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/fields.py +7 -4
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/metadata.py +3 -1
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/tree.py +11 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/user3.py +14 -6
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/pack/plan.py +23 -13
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/pack/writer.py +2 -2
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/schema.py +94 -2
- pyreuser3-0.5.0/tests/test_enum_binary.py +221 -0
- pyreuser3-0.4.0/tests/test_enum_binary.py +0 -92
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/LICENSE +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/MANIFEST.in +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/SOURCES.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/dependency_links.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/entry_points.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/requires.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/PyREUser3.egg-info/top_level.txt +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/README.md +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/docs/PYPI.md +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/docs/README.zh-CN.md +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/__main__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/api.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/cli.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/core.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/base.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/enums.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/export/postprocess.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/pack/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/pack/base.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/pack/models.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/rich_ui.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/__init__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/__main__.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/handler.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/jobs.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/page.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/picker.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/runners.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/server.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/pyreuser3/web/settings.py +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/setup.cfg +0 -0
- {pyreuser3-0.4.0 → pyreuser3-0.5.0}/tests/test_export_enums.py +0 -0
|
@@ -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": {}}
|
|
@@ -337,7 +337,9 @@ class ExporterMetadataMixin:
|
|
|
337
337
|
if typedb is None or not enum_lookup:
|
|
338
338
|
return
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
iter_classes = getattr(typedb, "iter_classes", None)
|
|
341
|
+
class_defs = iter_classes() if callable(iter_classes) else typedb.classes.values()
|
|
342
|
+
for class_def in class_defs:
|
|
341
343
|
field_map = self.class_field_fixed_types.setdefault(class_def.name, {})
|
|
342
344
|
for field in class_def.fields:
|
|
343
345
|
if field.field_type != "Enum":
|
|
@@ -245,6 +245,17 @@ class ExporterTreeMixin:
|
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
if inst.get("unparsed"):
|
|
249
|
+
class_name = inst.get("class_name", "Unknown Class")
|
|
250
|
+
class_name = self._normalize_to_fixed_enum_type(class_name)
|
|
251
|
+
return {
|
|
252
|
+
class_name: {
|
|
253
|
+
"ref_instance_id": idx,
|
|
254
|
+
"unparsed": True,
|
|
255
|
+
"reason": inst.get("reason", "unparsed"),
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
248
259
|
data = inst.get("data", {})
|
|
249
260
|
class_name = data.get("_class", inst.get("class_name", "Unknown Class"))
|
|
250
261
|
class_name = self._normalize_to_fixed_enum_type(class_name)
|
|
@@ -56,11 +56,12 @@ class ExporterUser3ParserMixin:
|
|
|
56
56
|
reader.seek(usr_header["userdata_info_tbl"])
|
|
57
57
|
for idx in range(usr_header["userdata_count"]):
|
|
58
58
|
class_hash = reader.read_u32()
|
|
59
|
-
|
|
59
|
+
crc = reader.read_u32()
|
|
60
60
|
path_offset = reader.read_u64()
|
|
61
|
+
class_def = self.typedb.get_class(class_hash, crc)
|
|
61
62
|
class_name = (
|
|
62
|
-
|
|
63
|
-
if
|
|
63
|
+
class_def.name
|
|
64
|
+
if class_def
|
|
64
65
|
else "Unknown Class"
|
|
65
66
|
)
|
|
66
67
|
header_userdata_infos.append(
|
|
@@ -112,7 +113,7 @@ class ExporterUser3ParserMixin:
|
|
|
112
113
|
# depend on these indexes remaining stable.
|
|
113
114
|
class_hash = reader.read_u32()
|
|
114
115
|
crc = reader.read_u32()
|
|
115
|
-
class_def = self.typedb.get_class(class_hash)
|
|
116
|
+
class_def = self.typedb.get_class(class_hash, crc)
|
|
116
117
|
instance_infos.append(
|
|
117
118
|
{
|
|
118
119
|
"index": idx,
|
|
@@ -175,7 +176,7 @@ class ExporterUser3ParserMixin:
|
|
|
175
176
|
}
|
|
176
177
|
)
|
|
177
178
|
continue
|
|
178
|
-
cls = self.typedb.get_class(class_hash)
|
|
179
|
+
cls = self.typedb.get_class(class_hash, int(info["crc"]))
|
|
179
180
|
if cls is None:
|
|
180
181
|
# Follow schema field layout exactly so alignment, padding, and unknown
|
|
181
182
|
# data remain binary-compatible.
|
|
@@ -198,7 +199,14 @@ class ExporterUser3ParserMixin:
|
|
|
198
199
|
start_pos = reader.tell()
|
|
199
200
|
try:
|
|
200
201
|
parsed_instances.append(
|
|
201
|
-
{
|
|
202
|
+
{
|
|
203
|
+
"index": idx,
|
|
204
|
+
"data": self._parse_instance(
|
|
205
|
+
reader,
|
|
206
|
+
class_hash,
|
|
207
|
+
crc=int(info["crc"]),
|
|
208
|
+
),
|
|
209
|
+
}
|
|
202
210
|
)
|
|
203
211
|
except Exception as exc:
|
|
204
212
|
# Treat each file independently so one malformed resource is reported
|
|
@@ -95,16 +95,23 @@ class PackerPlanMixin:
|
|
|
95
95
|
class_name = entry.get("_class")
|
|
96
96
|
if not isinstance(class_name, str) or not class_name:
|
|
97
97
|
raise PackError(f"instance {idx} is missing _class")
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
declared_crc = self._parse_optional_u32(entry.get("_crc"))
|
|
99
|
+
fields = entry.get("fields", {})
|
|
100
|
+
field_names = (
|
|
101
|
+
{key for key in fields if isinstance(key, str)}
|
|
102
|
+
if isinstance(fields, dict)
|
|
103
|
+
else None
|
|
104
|
+
)
|
|
105
|
+
resolved = self.typedb.get_class_for_fields(
|
|
106
|
+
class_name,
|
|
107
|
+
field_names=field_names,
|
|
108
|
+
crc=declared_crc,
|
|
109
|
+
)
|
|
110
|
+
if resolved is None:
|
|
100
111
|
raise PackError(
|
|
101
112
|
f"class not found in schema for instance {idx}: {class_name}"
|
|
102
113
|
)
|
|
103
|
-
class_def =
|
|
104
|
-
if class_def is None:
|
|
105
|
-
raise PackError(
|
|
106
|
-
f"class hash not found in schema for instance {idx}: {class_name}"
|
|
107
|
-
)
|
|
114
|
+
class_hash, class_def = resolved
|
|
108
115
|
self._validate_declared_hash(idx, entry, class_hash, class_def.crc)
|
|
109
116
|
self.instances[idx] = InstanceSpec(
|
|
110
117
|
class_hash=class_hash, class_def=class_def
|
|
@@ -363,12 +370,15 @@ class PackerPlanMixin:
|
|
|
363
370
|
PackError: JSON input could not be represented safely as .user.3 binary data.
|
|
364
371
|
"""
|
|
365
372
|
class_name, fields = self._unwrap_node(node, expected_class)
|
|
366
|
-
|
|
367
|
-
|
|
373
|
+
field_names = (
|
|
374
|
+
{key for key in fields if isinstance(key, str)}
|
|
375
|
+
if isinstance(fields, dict)
|
|
376
|
+
else None
|
|
377
|
+
)
|
|
378
|
+
resolved = self.typedb.get_class_for_fields(class_name, field_names=field_names)
|
|
379
|
+
if resolved is None:
|
|
368
380
|
raise PackError(f"class not found in schema: {class_name}")
|
|
369
|
-
class_def =
|
|
370
|
-
if class_def is None:
|
|
371
|
-
raise PackError(f"class hash not found in schema: {class_name}")
|
|
381
|
+
class_hash, class_def = resolved
|
|
372
382
|
|
|
373
383
|
spec = InstanceSpec(class_hash=class_hash, class_def=class_def)
|
|
374
384
|
spec.fields = self._prepare_fields(class_def, fields)
|
|
@@ -602,7 +612,7 @@ class PackerPlanMixin:
|
|
|
602
612
|
return False
|
|
603
613
|
if field_def.field_type in {"F32", "F64"}:
|
|
604
614
|
return 0.0
|
|
605
|
-
if field_def.field_type in {"String", "Resource", "C8"}:
|
|
615
|
+
if field_def.field_type in {"String", "Resource", "C8", "RuntimeType"}:
|
|
606
616
|
return ""
|
|
607
617
|
if field_def.field_type in {"Guid", "GameObjectRef", "Uri"}:
|
|
608
618
|
return "00000000-0000-0000-0000-000000000000"
|
|
@@ -215,8 +215,8 @@ class PackerWriterMixin:
|
|
|
215
215
|
writer.write_struct("<I", len(raw) // 2)
|
|
216
216
|
writer.write(raw)
|
|
217
217
|
return
|
|
218
|
-
if t
|
|
219
|
-
# C8 strings store UTF-8 byte length and keep a trailing null byte in the binary stream.
|
|
218
|
+
if t in {"C8", "RuntimeType"}:
|
|
219
|
+
# C8-style strings store UTF-8 byte length and keep a trailing null byte in the binary stream.
|
|
220
220
|
writer.align(4)
|
|
221
221
|
raw = f"{value or ''}\x00".encode("utf-8")
|
|
222
222
|
writer.write_struct("<I", len(raw))
|
|
@@ -10,6 +10,11 @@ import json
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
+
BTABLE_ORDER_LIST_CLASS_HASH = 0xD081F6C1
|
|
14
|
+
BTABLE_ORDER_LIST_NAME = "ace.btable.user_data.BTableOrderList"
|
|
15
|
+
BTABLE_ORDER_LIST_LEGACY_CRC = 0x6C85037C
|
|
16
|
+
BTABLE_ORDER_LIST_LEGACY_FIELDS = ("_OperatorFactories", "_CommandFactories")
|
|
17
|
+
|
|
13
18
|
|
|
14
19
|
def murmur3_32(data: bytes, seed: int = 0xFFFFFFFF) -> int:
|
|
15
20
|
"""Compute the MurmurHash3 variant used by RE_RSZ type names.
|
|
@@ -101,7 +106,7 @@ class ClassDef:
|
|
|
101
106
|
|
|
102
107
|
|
|
103
108
|
class TypeDB:
|
|
104
|
-
"""Index RE_RSZ class definitions by hash and
|
|
109
|
+
"""Index RE_RSZ class definitions by hash, CRC, and name for parser and packer lookups.
|
|
105
110
|
"""
|
|
106
111
|
|
|
107
112
|
def __init__(self, classes: dict[int, ClassDef]):
|
|
@@ -114,6 +119,11 @@ class TypeDB:
|
|
|
114
119
|
None. The method performs its documented side effect in place and raises on invalid input.
|
|
115
120
|
"""
|
|
116
121
|
self.classes = classes
|
|
122
|
+
self.class_variants: dict[tuple[int, int], ClassDef] = {
|
|
123
|
+
(class_hash, class_def.crc & 0xFFFFFFFF): class_def
|
|
124
|
+
for class_hash, class_def in classes.items()
|
|
125
|
+
}
|
|
126
|
+
self._add_builtin_class_variants()
|
|
117
127
|
# Preserve the exported JSON structure so external scripts and hand-edited files
|
|
118
128
|
# remain compatible across workflows.
|
|
119
129
|
self.name_to_hash = {c.name: h for h, c in classes.items()}
|
|
@@ -164,17 +174,99 @@ class TypeDB:
|
|
|
164
174
|
)
|
|
165
175
|
return cls(classes)
|
|
166
176
|
|
|
167
|
-
def
|
|
177
|
+
def _add_builtin_class_variants(self) -> None:
|
|
178
|
+
"""Register known CRC-specific layouts omitted by some public schema dumps."""
|
|
179
|
+
base_class = self.classes.get(BTABLE_ORDER_LIST_CLASS_HASH)
|
|
180
|
+
if base_class is None or base_class.name != BTABLE_ORDER_LIST_NAME:
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
fields_by_name = {field.name: field for field in base_class.fields}
|
|
184
|
+
legacy_fields = [
|
|
185
|
+
fields_by_name[name]
|
|
186
|
+
for name in BTABLE_ORDER_LIST_LEGACY_FIELDS
|
|
187
|
+
if name in fields_by_name
|
|
188
|
+
]
|
|
189
|
+
if len(legacy_fields) != len(BTABLE_ORDER_LIST_LEGACY_FIELDS):
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
self.class_variants.setdefault(
|
|
193
|
+
(BTABLE_ORDER_LIST_CLASS_HASH, BTABLE_ORDER_LIST_LEGACY_CRC),
|
|
194
|
+
ClassDef(
|
|
195
|
+
name=base_class.name,
|
|
196
|
+
crc=BTABLE_ORDER_LIST_LEGACY_CRC,
|
|
197
|
+
fields=legacy_fields,
|
|
198
|
+
),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def get_class(self, class_hash: int, crc: int | None = None) -> ClassDef | None:
|
|
168
202
|
"""Get class.
|
|
169
203
|
|
|
170
204
|
Args:
|
|
171
205
|
class_hash (int): RE_RSZ type hash for a class.
|
|
206
|
+
crc (int | None): Optional CRC used to choose among same-hash layout variants.
|
|
172
207
|
|
|
173
208
|
Returns:
|
|
174
209
|
ClassDef | None: Matching class definition when the schema contains the requested identifier.
|
|
175
210
|
"""
|
|
211
|
+
if crc is not None:
|
|
212
|
+
class_def = self.class_variants.get((class_hash, crc & 0xFFFFFFFF))
|
|
213
|
+
if class_def is not None:
|
|
214
|
+
return class_def
|
|
176
215
|
return self.classes.get(class_hash)
|
|
177
216
|
|
|
217
|
+
def get_class_for_fields(
|
|
218
|
+
self,
|
|
219
|
+
class_name: str,
|
|
220
|
+
field_names: set[str] | None = None,
|
|
221
|
+
crc: int | None = None,
|
|
222
|
+
) -> tuple[int, ClassDef] | None:
|
|
223
|
+
"""Resolve a class by name, optionally preferring a CRC or exact field layout."""
|
|
224
|
+
class_hash = self.name_to_hash.get(class_name)
|
|
225
|
+
if class_hash is None:
|
|
226
|
+
return None
|
|
227
|
+
if crc is not None:
|
|
228
|
+
class_def = self.class_variants.get((class_hash, crc & 0xFFFFFFFF))
|
|
229
|
+
if class_def is not None:
|
|
230
|
+
return class_hash, class_def
|
|
231
|
+
if field_names is not None:
|
|
232
|
+
exact_matches: list[ClassDef] = []
|
|
233
|
+
for (variant_hash, _variant_crc), class_def in self.class_variants.items():
|
|
234
|
+
if variant_hash != class_hash or class_def.name != class_name:
|
|
235
|
+
continue
|
|
236
|
+
variant_fields = {field.name or "unnamed" for field in class_def.fields}
|
|
237
|
+
if variant_fields == field_names:
|
|
238
|
+
exact_matches.append(class_def)
|
|
239
|
+
if len(exact_matches) == 1:
|
|
240
|
+
class_def = exact_matches[0]
|
|
241
|
+
if crc is not None and (class_def.crc & 0xFFFFFFFF) != (
|
|
242
|
+
crc & 0xFFFFFFFF
|
|
243
|
+
):
|
|
244
|
+
return (
|
|
245
|
+
class_hash,
|
|
246
|
+
ClassDef(
|
|
247
|
+
name=class_def.name,
|
|
248
|
+
crc=crc & 0xFFFFFFFF,
|
|
249
|
+
fields=class_def.fields,
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
return class_hash, class_def
|
|
253
|
+
class_def = self.get_class(class_hash)
|
|
254
|
+
if class_def is None:
|
|
255
|
+
return None
|
|
256
|
+
return class_hash, class_def
|
|
257
|
+
|
|
258
|
+
def iter_classes(self) -> list[ClassDef]:
|
|
259
|
+
"""Return base classes and CRC-specific variants without duplicate objects."""
|
|
260
|
+
out: list[ClassDef] = []
|
|
261
|
+
seen: set[int] = set()
|
|
262
|
+
for class_def in list(self.classes.values()) + list(self.class_variants.values()):
|
|
263
|
+
marker = id(class_def)
|
|
264
|
+
if marker in seen:
|
|
265
|
+
continue
|
|
266
|
+
seen.add(marker)
|
|
267
|
+
out.append(class_def)
|
|
268
|
+
return out
|
|
269
|
+
|
|
178
270
|
def resolve_struct_hash(self, original_type: str) -> int | None:
|
|
179
271
|
"""Resolve struct hash.
|
|
180
272
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pyreuser3.core import BinaryReader, RSZ_MAGIC, USR_MAGIC, enum_storage_type_from_size
|
|
7
|
+
from pyreuser3.export.fields import ExporterFieldParserMixin
|
|
8
|
+
from pyreuser3.pack.models import BinaryWriter
|
|
9
|
+
from pyreuser3.pack.plan import PackerPlanMixin
|
|
10
|
+
from pyreuser3.pack.writer import PackerWriterMixin
|
|
11
|
+
from pyreuser3.schema import FieldDef, TypeDB
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EnumFieldParser(ExporterFieldParserMixin):
|
|
15
|
+
def __init__(self, storage_type: str | None = None):
|
|
16
|
+
self.storage_type = storage_type
|
|
17
|
+
|
|
18
|
+
def _resolve_enum_storage_type(self, field: FieldDef) -> str:
|
|
19
|
+
return self.storage_type or enum_storage_type_from_size(field.size)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EnumFieldWriter(PackerWriterMixin):
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.enum_underlying_types = {"via.motion.AxisDirection": "U8"}
|
|
25
|
+
self.member_lookup = {"via.motion.AxisDirection": {"Right": 2}}
|
|
26
|
+
|
|
27
|
+
@staticmethod
|
|
28
|
+
def _to_s32(value: int) -> int:
|
|
29
|
+
u32 = value & 0xFFFFFFFF
|
|
30
|
+
return u32 if u32 < 0x80000000 else u32 - 0x100000000
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class EmptyBinaryWriter(PackerWriterMixin):
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.instances = [None]
|
|
36
|
+
self.user_magic = USR_MAGIC
|
|
37
|
+
self.rsz_magic = RSZ_MAGIC
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class PlanDefaults(PackerPlanMixin):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class EnumBinaryTests(unittest.TestCase):
|
|
45
|
+
def test_build_binary_uses_shared_alignment_helper(self):
|
|
46
|
+
writer_mixin = EmptyBinaryWriter()
|
|
47
|
+
|
|
48
|
+
data = writer_mixin._build_binary([])
|
|
49
|
+
|
|
50
|
+
self.assertEqual(data[:4], USR_MAGIC.to_bytes(4, "little"))
|
|
51
|
+
self.assertEqual(data[0x30:0x34], RSZ_MAGIC.to_bytes(4, "little"))
|
|
52
|
+
|
|
53
|
+
def test_enum_reader_uses_schema_size_fallback(self):
|
|
54
|
+
parser = EnumFieldParser()
|
|
55
|
+
reader = BinaryReader(b"\x02abcdef")
|
|
56
|
+
field = FieldDef(
|
|
57
|
+
name="v2_LegDirection",
|
|
58
|
+
field_type="Enum",
|
|
59
|
+
original_type="via.motion.AxisDirection",
|
|
60
|
+
size=1,
|
|
61
|
+
align=1,
|
|
62
|
+
is_array=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.assertEqual(parser._parse_field_value(reader, field), 2)
|
|
66
|
+
self.assertEqual(reader.tell(), 1)
|
|
67
|
+
|
|
68
|
+
def test_enum_reader_uses_il2cpp_underlying_type(self):
|
|
69
|
+
parser = EnumFieldParser("U8")
|
|
70
|
+
reader = BinaryReader(b"\x02\x00\x00\x00")
|
|
71
|
+
field = FieldDef(
|
|
72
|
+
name="v2_LegDirection",
|
|
73
|
+
field_type="Enum",
|
|
74
|
+
original_type="via.motion.AxisDirection",
|
|
75
|
+
size=4,
|
|
76
|
+
align=4,
|
|
77
|
+
is_array=False,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
self.assertEqual(parser._parse_field_value(reader, field), 2)
|
|
81
|
+
self.assertEqual(reader.tell(), 1)
|
|
82
|
+
|
|
83
|
+
def test_enum_writer_uses_il2cpp_underlying_type_for_labels(self):
|
|
84
|
+
writer_mixin = EnumFieldWriter()
|
|
85
|
+
writer = BinaryWriter()
|
|
86
|
+
field = FieldDef(
|
|
87
|
+
name="v2_LegDirection",
|
|
88
|
+
field_type="Enum",
|
|
89
|
+
original_type="via.motion.AxisDirection",
|
|
90
|
+
size=1,
|
|
91
|
+
align=1,
|
|
92
|
+
is_array=False,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
writer_mixin._write_scalar(writer, field, "Right")
|
|
96
|
+
self.assertEqual(bytes(writer.data), b"\x02")
|
|
97
|
+
|
|
98
|
+
def test_runtime_type_reader_uses_c8_string_layout(self):
|
|
99
|
+
parser = EnumFieldParser()
|
|
100
|
+
reader = BinaryReader(b"\x04\x00\x00\x00abc\x00tail")
|
|
101
|
+
field = FieldDef(
|
|
102
|
+
name="_OrderType",
|
|
103
|
+
field_type="RuntimeType",
|
|
104
|
+
original_type="System.Type",
|
|
105
|
+
size=4,
|
|
106
|
+
align=4,
|
|
107
|
+
is_array=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.assertEqual(parser._parse_field_value(reader, field), "abc")
|
|
111
|
+
self.assertEqual(reader.tell(), 8)
|
|
112
|
+
|
|
113
|
+
def test_runtime_type_writer_uses_c8_string_layout(self):
|
|
114
|
+
writer_mixin = EnumFieldWriter()
|
|
115
|
+
writer = BinaryWriter()
|
|
116
|
+
field = FieldDef(
|
|
117
|
+
name="_OrderType",
|
|
118
|
+
field_type="RuntimeType",
|
|
119
|
+
original_type="System.Type",
|
|
120
|
+
size=4,
|
|
121
|
+
align=4,
|
|
122
|
+
is_array=False,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
writer_mixin._write_scalar(writer, field, "abc")
|
|
126
|
+
|
|
127
|
+
self.assertEqual(bytes(writer.data), b"\x04\x00\x00\x00abc\x00")
|
|
128
|
+
|
|
129
|
+
def test_runtime_type_default_is_empty_string(self):
|
|
130
|
+
field = FieldDef(
|
|
131
|
+
name="_OrderType",
|
|
132
|
+
field_type="RuntimeType",
|
|
133
|
+
original_type="System.Type",
|
|
134
|
+
size=4,
|
|
135
|
+
align=4,
|
|
136
|
+
is_array=False,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self.assertEqual(PlanDefaults()._default_value(field), "")
|
|
140
|
+
|
|
141
|
+
def test_typedb_selects_btable_order_list_crc_variant(self):
|
|
142
|
+
schema = {
|
|
143
|
+
"d081f6c1": {
|
|
144
|
+
"crc": "15afce2d",
|
|
145
|
+
"name": "ace.btable.user_data.BTableOrderList",
|
|
146
|
+
"fields": [
|
|
147
|
+
{
|
|
148
|
+
"name": "_OperatorFactories",
|
|
149
|
+
"type": "Object",
|
|
150
|
+
"original_type": "ace.btable.cOperatorFactory",
|
|
151
|
+
"size": 4,
|
|
152
|
+
"align": 4,
|
|
153
|
+
"array": True,
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "_CommandFactories",
|
|
157
|
+
"type": "Object",
|
|
158
|
+
"original_type": "ace.btable.cCommandFactory",
|
|
159
|
+
"size": 4,
|
|
160
|
+
"align": 4,
|
|
161
|
+
"array": True,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"name": "_OperatorHashList",
|
|
165
|
+
"type": "U64",
|
|
166
|
+
"original_type": "System.UInt64",
|
|
167
|
+
"size": 8,
|
|
168
|
+
"align": 8,
|
|
169
|
+
"array": True,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "_CommandHashList",
|
|
173
|
+
"type": "U64",
|
|
174
|
+
"original_type": "System.UInt64",
|
|
175
|
+
"size": 8,
|
|
176
|
+
"align": 8,
|
|
177
|
+
"array": True,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
183
|
+
schema_path = Path(tmpdir) / "schema.json"
|
|
184
|
+
schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
|
185
|
+
|
|
186
|
+
typedb = TypeDB.load(schema_path)
|
|
187
|
+
|
|
188
|
+
current = typedb.get_class(0xD081F6C1, 0x15AFCE2D)
|
|
189
|
+
legacy = typedb.get_class(0xD081F6C1, 0x6C85037C)
|
|
190
|
+
resolved = typedb.get_class_for_fields(
|
|
191
|
+
"ace.btable.user_data.BTableOrderList",
|
|
192
|
+
field_names={"_OperatorFactories", "_CommandFactories"},
|
|
193
|
+
)
|
|
194
|
+
crc_alias = typedb.get_class_for_fields(
|
|
195
|
+
"ace.btable.user_data.BTableOrderList",
|
|
196
|
+
field_names={
|
|
197
|
+
"_OperatorFactories",
|
|
198
|
+
"_CommandFactories",
|
|
199
|
+
"_OperatorHashList",
|
|
200
|
+
"_CommandHashList",
|
|
201
|
+
},
|
|
202
|
+
crc=0x12345678,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
self.assertIsNotNone(current)
|
|
206
|
+
self.assertEqual(len(current.fields), 4)
|
|
207
|
+
self.assertIsNotNone(legacy)
|
|
208
|
+
self.assertEqual(legacy.crc, 0x6C85037C)
|
|
209
|
+
self.assertEqual([field.name for field in legacy.fields], [
|
|
210
|
+
"_OperatorFactories",
|
|
211
|
+
"_CommandFactories",
|
|
212
|
+
])
|
|
213
|
+
self.assertIsNotNone(resolved)
|
|
214
|
+
self.assertEqual(resolved[1].crc, 0x6C85037C)
|
|
215
|
+
self.assertIsNotNone(crc_alias)
|
|
216
|
+
self.assertEqual(crc_alias[1].crc, 0x12345678)
|
|
217
|
+
self.assertEqual(len(crc_alias[1].fields), 4)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
unittest.main()
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from pyreuser3.core import BinaryReader, RSZ_MAGIC, USR_MAGIC, enum_storage_type_from_size
|
|
4
|
-
from pyreuser3.export.fields import ExporterFieldParserMixin
|
|
5
|
-
from pyreuser3.pack.models import BinaryWriter
|
|
6
|
-
from pyreuser3.pack.writer import PackerWriterMixin
|
|
7
|
-
from pyreuser3.schema import FieldDef
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class EnumFieldParser(ExporterFieldParserMixin):
|
|
11
|
-
def __init__(self, storage_type: str | None = None):
|
|
12
|
-
self.storage_type = storage_type
|
|
13
|
-
|
|
14
|
-
def _resolve_enum_storage_type(self, field: FieldDef) -> str:
|
|
15
|
-
return self.storage_type or enum_storage_type_from_size(field.size)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class EnumFieldWriter(PackerWriterMixin):
|
|
19
|
-
def __init__(self):
|
|
20
|
-
self.enum_underlying_types = {"via.motion.AxisDirection": "U8"}
|
|
21
|
-
self.member_lookup = {"via.motion.AxisDirection": {"Right": 2}}
|
|
22
|
-
|
|
23
|
-
@staticmethod
|
|
24
|
-
def _to_s32(value: int) -> int:
|
|
25
|
-
u32 = value & 0xFFFFFFFF
|
|
26
|
-
return u32 if u32 < 0x80000000 else u32 - 0x100000000
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class EmptyBinaryWriter(PackerWriterMixin):
|
|
30
|
-
def __init__(self):
|
|
31
|
-
self.instances = [None]
|
|
32
|
-
self.user_magic = USR_MAGIC
|
|
33
|
-
self.rsz_magic = RSZ_MAGIC
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class EnumBinaryTests(unittest.TestCase):
|
|
37
|
-
def test_build_binary_uses_shared_alignment_helper(self):
|
|
38
|
-
writer_mixin = EmptyBinaryWriter()
|
|
39
|
-
|
|
40
|
-
data = writer_mixin._build_binary([])
|
|
41
|
-
|
|
42
|
-
self.assertEqual(data[:4], USR_MAGIC.to_bytes(4, "little"))
|
|
43
|
-
self.assertEqual(data[0x30:0x34], RSZ_MAGIC.to_bytes(4, "little"))
|
|
44
|
-
|
|
45
|
-
def test_enum_reader_uses_schema_size_fallback(self):
|
|
46
|
-
parser = EnumFieldParser()
|
|
47
|
-
reader = BinaryReader(b"\x02abcdef")
|
|
48
|
-
field = FieldDef(
|
|
49
|
-
name="v2_LegDirection",
|
|
50
|
-
field_type="Enum",
|
|
51
|
-
original_type="via.motion.AxisDirection",
|
|
52
|
-
size=1,
|
|
53
|
-
align=1,
|
|
54
|
-
is_array=False,
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
self.assertEqual(parser._parse_field_value(reader, field), 2)
|
|
58
|
-
self.assertEqual(reader.tell(), 1)
|
|
59
|
-
|
|
60
|
-
def test_enum_reader_uses_il2cpp_underlying_type(self):
|
|
61
|
-
parser = EnumFieldParser("U8")
|
|
62
|
-
reader = BinaryReader(b"\x02\x00\x00\x00")
|
|
63
|
-
field = FieldDef(
|
|
64
|
-
name="v2_LegDirection",
|
|
65
|
-
field_type="Enum",
|
|
66
|
-
original_type="via.motion.AxisDirection",
|
|
67
|
-
size=4,
|
|
68
|
-
align=4,
|
|
69
|
-
is_array=False,
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
self.assertEqual(parser._parse_field_value(reader, field), 2)
|
|
73
|
-
self.assertEqual(reader.tell(), 1)
|
|
74
|
-
|
|
75
|
-
def test_enum_writer_uses_il2cpp_underlying_type_for_labels(self):
|
|
76
|
-
writer_mixin = EnumFieldWriter()
|
|
77
|
-
writer = BinaryWriter()
|
|
78
|
-
field = FieldDef(
|
|
79
|
-
name="v2_LegDirection",
|
|
80
|
-
field_type="Enum",
|
|
81
|
-
original_type="via.motion.AxisDirection",
|
|
82
|
-
size=1,
|
|
83
|
-
align=1,
|
|
84
|
-
is_array=False,
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
writer_mixin._write_scalar(writer, field, "Right")
|
|
88
|
-
self.assertEqual(bytes(writer.data), b"\x02")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if __name__ == "__main__":
|
|
92
|
-
unittest.main()
|
|
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
|