arkparser 0.1.0__py3-none-any.whl
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.
- arkparser/__init__.py +117 -0
- arkparser/common/__init__.py +72 -0
- arkparser/common/binary_reader.py +402 -0
- arkparser/common/exceptions.py +99 -0
- arkparser/common/map_config.py +166 -0
- arkparser/common/types.py +249 -0
- arkparser/common/version_detection.py +195 -0
- arkparser/data_models.py +801 -0
- arkparser/export.py +485 -0
- arkparser/files/__init__.py +25 -0
- arkparser/files/base.py +309 -0
- arkparser/files/cloud_inventory.py +259 -0
- arkparser/files/profile.py +205 -0
- arkparser/files/tribe.py +155 -0
- arkparser/files/world_save.py +699 -0
- arkparser/game_objects/__init__.py +32 -0
- arkparser/game_objects/container.py +180 -0
- arkparser/game_objects/game_object.py +273 -0
- arkparser/game_objects/location.py +87 -0
- arkparser/models/__init__.py +29 -0
- arkparser/models/character.py +227 -0
- arkparser/models/creature.py +642 -0
- arkparser/models/item.py +207 -0
- arkparser/models/player.py +263 -0
- arkparser/models/stats.py +226 -0
- arkparser/models/structure.py +176 -0
- arkparser/models/tribe.py +291 -0
- arkparser/properties/__init__.py +77 -0
- arkparser/properties/base.py +329 -0
- arkparser/properties/byte_property.py +230 -0
- arkparser/properties/compound.py +1125 -0
- arkparser/properties/primitives.py +803 -0
- arkparser/properties/registry.py +236 -0
- arkparser/py.typed +0 -0
- arkparser/structs/__init__.py +60 -0
- arkparser/structs/base.py +63 -0
- arkparser/structs/colors.py +108 -0
- arkparser/structs/misc.py +133 -0
- arkparser/structs/property_list.py +101 -0
- arkparser/structs/registry.py +140 -0
- arkparser/structs/vectors.py +221 -0
- arkparser-0.1.0.dist-info/METADATA +833 -0
- arkparser-0.1.0.dist-info/RECORD +46 -0
- arkparser-0.1.0.dist-info/WHEEL +5 -0
- arkparser-0.1.0.dist-info/licenses/LICENSE +21 -0
- arkparser-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Compound Property Types.
|
|
3
|
+
|
|
4
|
+
These are complex property types that contain other values or properties:
|
|
5
|
+
- ArrayProperty: Contains a list of values of the same type
|
|
6
|
+
- StructProperty: Contains structured data (either native format or property list)
|
|
7
|
+
- MapProperty: Contains key-value pairs (less common)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import typing as t
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
|
|
15
|
+
from .base import Property, PropertyHeader, read_name
|
|
16
|
+
|
|
17
|
+
if t.TYPE_CHECKING:
|
|
18
|
+
from ..common.binary_reader import BinaryReader
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# =============================================================================
|
|
22
|
+
# Array Property
|
|
23
|
+
# =============================================================================
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ArrayProperty(Property):
|
|
28
|
+
"""
|
|
29
|
+
Array property - contains a list of values of the same type.
|
|
30
|
+
|
|
31
|
+
Format:
|
|
32
|
+
Header + Name arrayType + Int32 count + [elements...]
|
|
33
|
+
|
|
34
|
+
For ASA, there's an extra unknown byte after the index.
|
|
35
|
+
|
|
36
|
+
Element types are determined by arrayType:
|
|
37
|
+
- IntProperty, FloatProperty, etc. -> simple values
|
|
38
|
+
- ObjectProperty -> object references
|
|
39
|
+
- StructProperty -> struct arrays (have additional header data)
|
|
40
|
+
- ByteProperty -> raw bytes (if enum_name is "None") or enum values
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
index: int = 0
|
|
45
|
+
array_type: str = ""
|
|
46
|
+
_values: list[t.Any] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def type_name(self) -> str:
|
|
50
|
+
return "ArrayProperty"
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def value(self) -> list[t.Any]:
|
|
54
|
+
return self._values
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def count(self) -> int:
|
|
58
|
+
return len(self._values)
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def read(
|
|
62
|
+
cls,
|
|
63
|
+
reader: BinaryReader,
|
|
64
|
+
header: PropertyHeader,
|
|
65
|
+
is_asa: bool = False,
|
|
66
|
+
name_table: list[str] | None = None,
|
|
67
|
+
worldsave_format: bool = False,
|
|
68
|
+
) -> ArrayProperty:
|
|
69
|
+
"""
|
|
70
|
+
Read an ArrayProperty from the archive.
|
|
71
|
+
|
|
72
|
+
ASE format after header:
|
|
73
|
+
- ElementType (name)
|
|
74
|
+
- Count (int32)
|
|
75
|
+
- Elements...
|
|
76
|
+
|
|
77
|
+
ASA format after header (CloudInventory/Profile/Tribe):
|
|
78
|
+
For primitive arrays:
|
|
79
|
+
- Index (int32)
|
|
80
|
+
- ElementType (string)
|
|
81
|
+
- Zeros (int32)
|
|
82
|
+
- DataSize (int32)
|
|
83
|
+
- Extra (byte)
|
|
84
|
+
- Count (int32)
|
|
85
|
+
- Elements...
|
|
86
|
+
|
|
87
|
+
For struct arrays:
|
|
88
|
+
- Index (int32)
|
|
89
|
+
- ElementType (string) = "StructProperty"
|
|
90
|
+
- Extra1 (int32, usually 1)
|
|
91
|
+
- StructType (string)
|
|
92
|
+
- Extra2 (int32, usually 1)
|
|
93
|
+
- ScriptPath (string)
|
|
94
|
+
- Zeros (int32)
|
|
95
|
+
- DataSize (int32)
|
|
96
|
+
- Extra3 (byte)
|
|
97
|
+
- Count (int32)
|
|
98
|
+
- Elements...
|
|
99
|
+
|
|
100
|
+
ASA WorldSave format (SQLite objects):
|
|
101
|
+
- ArrayHeader (int32) = 1
|
|
102
|
+
- ElementTypeID (int32) + Instance (int32)
|
|
103
|
+
- Zeros (int32)
|
|
104
|
+
- ArrayByteLength (int32)
|
|
105
|
+
- ArrayIndex (byte) = property array index
|
|
106
|
+
- Count (int32)
|
|
107
|
+
- Elements...
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
reader: The binary reader.
|
|
111
|
+
header: The property header.
|
|
112
|
+
is_asa: True for ASA format.
|
|
113
|
+
name_table: Optional name table for world saves (version 6+).
|
|
114
|
+
worldsave_format: True for ASA WorldSave SQLite object format.
|
|
115
|
+
"""
|
|
116
|
+
if worldsave_format:
|
|
117
|
+
return cls._read_worldsave_array(reader, header, name_table)
|
|
118
|
+
|
|
119
|
+
# ASA cloud inventory (version 7+) uses a special format where:
|
|
120
|
+
# - header.data_size is a flag (1) instead of actual size
|
|
121
|
+
# - header.index contains the element type string length
|
|
122
|
+
# ASA profiles/tribes (version 6) use ASE-style format with actual sizes
|
|
123
|
+
use_asa_cloud_format = is_asa and header.data_size == 1 and header.index > 0
|
|
124
|
+
|
|
125
|
+
if use_asa_cloud_format:
|
|
126
|
+
# ASA CloudInventory format
|
|
127
|
+
# The property header already read data_size and index, where:
|
|
128
|
+
# - data_size is a flag (usually 1)
|
|
129
|
+
# - index is the length of the element type string
|
|
130
|
+
# So we read the element type string directly (its length is header.index)
|
|
131
|
+
array_type_len = header.index
|
|
132
|
+
array_type_bytes = reader.read_bytes(array_type_len)
|
|
133
|
+
array_type = array_type_bytes[:-1].decode("latin-1") # Remove null terminator
|
|
134
|
+
index = header.data_size
|
|
135
|
+
|
|
136
|
+
if array_type == "StructProperty":
|
|
137
|
+
# Struct arrays have completely different header - pass count as -1 to signal
|
|
138
|
+
# that the struct array handler should read its own header
|
|
139
|
+
count = -1 # Sentinel value
|
|
140
|
+
else:
|
|
141
|
+
# Primitive arrays have zeros, dataSize, extra byte before count
|
|
142
|
+
_zeros = reader.read_int32() # Usually 0
|
|
143
|
+
_data_size = reader.read_int32() # Size of array data
|
|
144
|
+
_extra = reader.read_uint8() # Extra byte
|
|
145
|
+
count = reader.read_int32()
|
|
146
|
+
else:
|
|
147
|
+
# ASE format / ASA profile/tribe format - element type first, then count
|
|
148
|
+
# Use name table if available
|
|
149
|
+
index = header.index
|
|
150
|
+
array_type = read_name(reader, name_table)
|
|
151
|
+
if is_asa:
|
|
152
|
+
# ASA v6 profiles/tribes have an extra byte between array_type and count
|
|
153
|
+
# (same as primitive properties). If bit 0 is set, an index override follows.
|
|
154
|
+
extra_byte = reader.read_uint8()
|
|
155
|
+
if extra_byte & 0x01:
|
|
156
|
+
index = reader.read_int32()
|
|
157
|
+
count = reader.read_int32()
|
|
158
|
+
|
|
159
|
+
# For now, read raw values based on simple types
|
|
160
|
+
# Complex types (structs, objects) will need special handling
|
|
161
|
+
values: list[t.Any] = []
|
|
162
|
+
|
|
163
|
+
if count != 0: # -1 for ASA struct arrays, > 0 for regular arrays
|
|
164
|
+
values = _read_array_elements(reader, array_type, count, header.data_size, header.name, is_asa, name_table)
|
|
165
|
+
|
|
166
|
+
return cls(
|
|
167
|
+
name=header.name,
|
|
168
|
+
index=index,
|
|
169
|
+
array_type=array_type,
|
|
170
|
+
_values=values,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _read_worldsave_array(
|
|
175
|
+
cls,
|
|
176
|
+
reader: BinaryReader,
|
|
177
|
+
header: PropertyHeader,
|
|
178
|
+
name_table: dict[int, str] | None,
|
|
179
|
+
) -> ArrayProperty:
|
|
180
|
+
"""
|
|
181
|
+
Read an ArrayProperty in ASA WorldSave format.
|
|
182
|
+
|
|
183
|
+
WorldSave array format (after property header: Name ID + Instance + Type ID):
|
|
184
|
+
|
|
185
|
+
For primitive arrays (non-struct):
|
|
186
|
+
- ArrayHeader (int32) = 1
|
|
187
|
+
- ElementTypeID (int32)
|
|
188
|
+
- ElementTypeInstance (int32)
|
|
189
|
+
- 8 zero bytes
|
|
190
|
+
- ArrayByteLength (int32)
|
|
191
|
+
- ArrayIndex (byte)
|
|
192
|
+
- Count (int32)
|
|
193
|
+
- Elements...
|
|
194
|
+
|
|
195
|
+
For struct arrays (element type = StructProperty):
|
|
196
|
+
- ArrayHeader (int32) = 1
|
|
197
|
+
- ElementTypeID (int32) = StructProperty
|
|
198
|
+
- ElementTypeInstance (int32) = 0
|
|
199
|
+
- StructHeader (int32) = 1
|
|
200
|
+
- StructTypeID (int32)
|
|
201
|
+
- StructTypeInstance (int32)
|
|
202
|
+
- StructHeader2 (int32) = 1
|
|
203
|
+
- ScriptPathID (int32)
|
|
204
|
+
- ScriptPathInstance (int32)
|
|
205
|
+
- 4 zero bytes
|
|
206
|
+
- ArrayByteLength (int32)
|
|
207
|
+
- Flag (byte) - NOT array_index for struct arrays
|
|
208
|
+
- Count (int32)
|
|
209
|
+
- Elements... (each is a property list ending with None)
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
reader: The binary reader.
|
|
213
|
+
header: The property header (already parsed).
|
|
214
|
+
name_table: The name table dictionary.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
ArrayProperty with parsed values.
|
|
218
|
+
"""
|
|
219
|
+
if name_table is None:
|
|
220
|
+
raise ValueError("WorldSave array format requires a name table")
|
|
221
|
+
|
|
222
|
+
# Read array header (should be 1)
|
|
223
|
+
_array_header = reader.read_int32()
|
|
224
|
+
|
|
225
|
+
# Read element type from name table
|
|
226
|
+
element_type_id = reader.read_int32()
|
|
227
|
+
element_type = name_table.get(element_type_id, f"__UNKNOWN_{element_type_id}__")
|
|
228
|
+
|
|
229
|
+
struct_type: str | None = None
|
|
230
|
+
array_index = 0
|
|
231
|
+
|
|
232
|
+
if element_type == "StructProperty":
|
|
233
|
+
# Struct arrays have additional header fields after element type ID:
|
|
234
|
+
# - 4 zeros (padding = element_type_instance)
|
|
235
|
+
# - StructHeader (4): Usually 1. If > 1, extra name references follow.
|
|
236
|
+
# - StructTypeID (4) + StructTypeInstance (4)
|
|
237
|
+
# - StructHeader2 (4) = 1
|
|
238
|
+
# - ScriptPathID (4) + ScriptPathInstance (4)
|
|
239
|
+
# - 4 zeros (padding)
|
|
240
|
+
# - [If StructHeader > 1: extra (NameID(4) + NameInstance(4) + Zeros(4)) groups]
|
|
241
|
+
# - ByteLength (4)
|
|
242
|
+
# - Flag (1)
|
|
243
|
+
# - Count (4)
|
|
244
|
+
_zeros1 = reader.read_int32() # Padding after element type ID
|
|
245
|
+
_struct_header = reader.read_int32() # Usually 1, sometimes 2+
|
|
246
|
+
struct_type_id = reader.read_int32()
|
|
247
|
+
struct_type = name_table.get(struct_type_id, f"__UNKNOWN_{struct_type_id}__")
|
|
248
|
+
_zeros2 = reader.read_int32() # Padding after struct type ID
|
|
249
|
+
|
|
250
|
+
_struct_header2 = reader.read_int32() # Usually 1
|
|
251
|
+
_script_path_id = reader.read_int32()
|
|
252
|
+
_zeros3 = reader.read_int32() # Padding after script path ID
|
|
253
|
+
_zeros4 = reader.read_int32() # Additional padding
|
|
254
|
+
|
|
255
|
+
# If _struct_header > 1, read extra name reference groups
|
|
256
|
+
# Each extra group is: name_id(4) + name_inst(4) + zeros(4) = 12 bytes
|
|
257
|
+
for _ in range(_struct_header - 1):
|
|
258
|
+
_extra_name_id = reader.read_int32()
|
|
259
|
+
_extra_name_inst = reader.read_int32()
|
|
260
|
+
_extra_zeros = reader.read_int32()
|
|
261
|
+
|
|
262
|
+
_array_byte_length = reader.read_int32()
|
|
263
|
+
_flag_byte = reader.read_uint8() # Not array_index for struct arrays
|
|
264
|
+
count = reader.read_int32()
|
|
265
|
+
else:
|
|
266
|
+
# Primitive arrays: 8 zeros + byte_length + array_index + count
|
|
267
|
+
_zeros1 = reader.read_int32()
|
|
268
|
+
_zeros2 = reader.read_int32()
|
|
269
|
+
_array_byte_length = reader.read_int32()
|
|
270
|
+
array_index = reader.read_uint8()
|
|
271
|
+
count = reader.read_int32()
|
|
272
|
+
|
|
273
|
+
# Read elements
|
|
274
|
+
values: list[t.Any] = []
|
|
275
|
+
|
|
276
|
+
if count > 0:
|
|
277
|
+
if element_type == "StructProperty" and struct_type:
|
|
278
|
+
values = _read_worldsave_struct_array_elements(reader, struct_type, count, name_table)
|
|
279
|
+
else:
|
|
280
|
+
values = _read_worldsave_array_elements(reader, element_type, count, name_table)
|
|
281
|
+
|
|
282
|
+
return cls(
|
|
283
|
+
name=header.name,
|
|
284
|
+
index=array_index,
|
|
285
|
+
array_type=element_type,
|
|
286
|
+
_values=values,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _read_array_elements(
|
|
291
|
+
reader: BinaryReader,
|
|
292
|
+
array_type: str,
|
|
293
|
+
count: int,
|
|
294
|
+
data_size: int,
|
|
295
|
+
array_name: str,
|
|
296
|
+
is_asa: bool,
|
|
297
|
+
name_table: list[str] | None = None,
|
|
298
|
+
) -> list[t.Any]:
|
|
299
|
+
"""
|
|
300
|
+
Read array elements based on the array type.
|
|
301
|
+
|
|
302
|
+
This is a simplified implementation for primitive array types.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
reader: The binary reader.
|
|
306
|
+
array_type: The element type name.
|
|
307
|
+
count: Number of elements to read.
|
|
308
|
+
data_size: Total data size from property header.
|
|
309
|
+
array_name: The array property name (for debugging).
|
|
310
|
+
is_asa: True for ASA format.
|
|
311
|
+
name_table: Optional name table for world saves (version 6+).
|
|
312
|
+
"""
|
|
313
|
+
values: list[t.Any] = []
|
|
314
|
+
|
|
315
|
+
# Simple numeric types
|
|
316
|
+
if array_type == "IntProperty":
|
|
317
|
+
for _ in range(count):
|
|
318
|
+
values.append(reader.read_int32())
|
|
319
|
+
elif array_type == "UInt32Property":
|
|
320
|
+
for _ in range(count):
|
|
321
|
+
values.append(reader.read_uint32())
|
|
322
|
+
elif array_type == "Int64Property":
|
|
323
|
+
for _ in range(count):
|
|
324
|
+
values.append(reader.read_int64())
|
|
325
|
+
elif array_type == "UInt64Property":
|
|
326
|
+
for _ in range(count):
|
|
327
|
+
values.append(reader.read_uint64())
|
|
328
|
+
elif array_type == "Int16Property":
|
|
329
|
+
for _ in range(count):
|
|
330
|
+
values.append(reader.read_int16())
|
|
331
|
+
elif array_type == "UInt16Property":
|
|
332
|
+
for _ in range(count):
|
|
333
|
+
values.append(reader.read_uint16())
|
|
334
|
+
elif array_type == "Int8Property":
|
|
335
|
+
for _ in range(count):
|
|
336
|
+
values.append(reader.read_int8())
|
|
337
|
+
elif array_type == "ByteProperty":
|
|
338
|
+
for _ in range(count):
|
|
339
|
+
values.append(reader.read_uint8())
|
|
340
|
+
elif array_type == "FloatProperty":
|
|
341
|
+
for _ in range(count):
|
|
342
|
+
values.append(reader.read_float())
|
|
343
|
+
elif array_type == "DoubleProperty":
|
|
344
|
+
for _ in range(count):
|
|
345
|
+
values.append(reader.read_double())
|
|
346
|
+
elif array_type == "BoolProperty":
|
|
347
|
+
for _ in range(count):
|
|
348
|
+
values.append(reader.read_uint8() != 0)
|
|
349
|
+
elif array_type == "StrProperty":
|
|
350
|
+
for _ in range(count):
|
|
351
|
+
values.append(reader.read_string())
|
|
352
|
+
elif array_type == "NameProperty":
|
|
353
|
+
for _ in range(count):
|
|
354
|
+
# Names in arrays use name table if available
|
|
355
|
+
values.append(read_name(reader, name_table))
|
|
356
|
+
elif array_type == "ObjectProperty":
|
|
357
|
+
# Object references in arrays
|
|
358
|
+
for _ in range(count):
|
|
359
|
+
if is_asa:
|
|
360
|
+
# ASA: Int32 type (-1=null, 0=id, 1=path)
|
|
361
|
+
ref_type = reader.read_int32()
|
|
362
|
+
if ref_type == -1:
|
|
363
|
+
# Null reference
|
|
364
|
+
values.append(None)
|
|
365
|
+
elif ref_type == 0:
|
|
366
|
+
# ID reference
|
|
367
|
+
ref_id = reader.read_int32()
|
|
368
|
+
if ref_id == -1:
|
|
369
|
+
values.append(None)
|
|
370
|
+
else:
|
|
371
|
+
values.append(("id", ref_id))
|
|
372
|
+
elif ref_type == 1:
|
|
373
|
+
# Path reference
|
|
374
|
+
path = reader.read_string()
|
|
375
|
+
values.append(("path", path))
|
|
376
|
+
else:
|
|
377
|
+
# Unknown type - might be path without marker
|
|
378
|
+
reader.skip(-4)
|
|
379
|
+
path = reader.read_string()
|
|
380
|
+
values.append(("path", path))
|
|
381
|
+
else:
|
|
382
|
+
# ASE: Int32 type + value
|
|
383
|
+
ref_type = reader.read_int32()
|
|
384
|
+
if ref_type == 0:
|
|
385
|
+
values.append(("id", reader.read_int32()))
|
|
386
|
+
elif ref_type == 1:
|
|
387
|
+
# Name reference (uses name table if available)
|
|
388
|
+
values.append(("name", read_name(reader, name_table)))
|
|
389
|
+
else:
|
|
390
|
+
values.append(("unknown", reader.read_int32()))
|
|
391
|
+
elif array_type == "SoftObjectProperty":
|
|
392
|
+
# Soft object references in arrays (asset paths)
|
|
393
|
+
for _ in range(count):
|
|
394
|
+
path = reader.read_string()
|
|
395
|
+
sub_path = reader.read_string()
|
|
396
|
+
# In arrays, SoftObjectProperty also has trailing padding (always 0)
|
|
397
|
+
_padding = reader.read_int32()
|
|
398
|
+
values.append({"path": path, "name": sub_path})
|
|
399
|
+
elif array_type == "StructProperty":
|
|
400
|
+
# Struct arrays have special handling
|
|
401
|
+
if is_asa and count < 0:
|
|
402
|
+
# ASA v7 cloud format struct array header (count == -1 sentinel):
|
|
403
|
+
# - Extra1 (int32, usually 1)
|
|
404
|
+
# - StructType (string)
|
|
405
|
+
# - Extra2 (int32, usually 1)
|
|
406
|
+
# - ScriptPath (string)
|
|
407
|
+
# - Zeros (int32, usually 0)
|
|
408
|
+
# - DataSize (int32)
|
|
409
|
+
# - Extra3 (byte)
|
|
410
|
+
# - Count (int32)
|
|
411
|
+
|
|
412
|
+
_extra1 = reader.read_int32()
|
|
413
|
+
struct_type = reader.read_string()
|
|
414
|
+
_extra2 = reader.read_int32()
|
|
415
|
+
_script_path = reader.read_string()
|
|
416
|
+
_zeros = reader.read_int32()
|
|
417
|
+
array_data_size = reader.read_int32()
|
|
418
|
+
extra3 = reader.read_uint8()
|
|
419
|
+
|
|
420
|
+
# Record position after extra3 - this is where data_size counts from
|
|
421
|
+
# The data_size INCLUDES the count field (4 bytes), so:
|
|
422
|
+
# array_data_end = position_after_extra3 + array_data_size
|
|
423
|
+
data_region_start = reader.position
|
|
424
|
+
array_data_end = data_region_start + array_data_size
|
|
425
|
+
|
|
426
|
+
actual_count = reader.read_int32()
|
|
427
|
+
|
|
428
|
+
# extra3 appears to be a flag byte. When it's 8 (bit 3 set), the
|
|
429
|
+
# struct array has 4 padding bytes after each element's None terminator.
|
|
430
|
+
# When it's 0, there's no padding between elements.
|
|
431
|
+
has_element_padding = (extra3 & 0x08) != 0
|
|
432
|
+
|
|
433
|
+
# Read struct elements
|
|
434
|
+
# For ASA string-based files (no name table), struct array elements
|
|
435
|
+
# do NOT have per-element headers. They directly start with properties.
|
|
436
|
+
# The struct_type from the array header applies to all elements.
|
|
437
|
+
from ..structs.registry import read_struct
|
|
438
|
+
|
|
439
|
+
# All elements are read as property-list structs with the same type
|
|
440
|
+
for struct_idx in range(actual_count):
|
|
441
|
+
struct_value = read_struct(reader, struct_type, is_asa, name_table=name_table)
|
|
442
|
+
if hasattr(struct_value, "to_dict"):
|
|
443
|
+
values.append(struct_value.to_dict())
|
|
444
|
+
else:
|
|
445
|
+
values.append(struct_value)
|
|
446
|
+
|
|
447
|
+
# If this array type has padding between elements, skip it
|
|
448
|
+
# (except for the last element where we position to array end)
|
|
449
|
+
if has_element_padding and struct_idx < actual_count - 1:
|
|
450
|
+
reader.skip(4)
|
|
451
|
+
|
|
452
|
+
# After reading all elements, ensure we're at the expected end.
|
|
453
|
+
# This handles any remaining padding after the last element.
|
|
454
|
+
if reader.position < array_data_end:
|
|
455
|
+
reader.skip(array_data_end - reader.position)
|
|
456
|
+
else:
|
|
457
|
+
# ASE struct arrays - read as property-based structs
|
|
458
|
+
from ..structs.registry import read_struct_for_array
|
|
459
|
+
|
|
460
|
+
for _ in range(count):
|
|
461
|
+
struct = read_struct_for_array(reader, array_name, is_asa, name_table=name_table)
|
|
462
|
+
if hasattr(struct, "to_dict"):
|
|
463
|
+
values.append(struct.to_dict())
|
|
464
|
+
else:
|
|
465
|
+
values.append(struct)
|
|
466
|
+
else:
|
|
467
|
+
# Unknown array type - read as raw bytes
|
|
468
|
+
# We can't determine element size, so just note it
|
|
469
|
+
values.append(f"<UnknownArray({array_type}): {count} elements>")
|
|
470
|
+
|
|
471
|
+
return values
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _read_worldsave_array_elements(
|
|
475
|
+
reader: BinaryReader,
|
|
476
|
+
element_type: str,
|
|
477
|
+
count: int,
|
|
478
|
+
name_table: dict[int, str],
|
|
479
|
+
) -> list[t.Any]:
|
|
480
|
+
"""
|
|
481
|
+
Read array elements for ASA WorldSave format.
|
|
482
|
+
|
|
483
|
+
WorldSave arrays have different element formats:
|
|
484
|
+
- ObjectProperty: 2 bytes + 16 byte GUID
|
|
485
|
+
- Primitive types: Same as other formats
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
reader: The binary reader.
|
|
489
|
+
element_type: The element type name from name table.
|
|
490
|
+
count: Number of elements.
|
|
491
|
+
name_table: The name table dictionary.
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
List of element values.
|
|
495
|
+
"""
|
|
496
|
+
values: list[t.Any] = []
|
|
497
|
+
|
|
498
|
+
# Simple numeric types - same as other formats
|
|
499
|
+
if element_type == "IntProperty":
|
|
500
|
+
for _ in range(count):
|
|
501
|
+
values.append(reader.read_int32())
|
|
502
|
+
elif element_type == "UInt32Property":
|
|
503
|
+
for _ in range(count):
|
|
504
|
+
values.append(reader.read_uint32())
|
|
505
|
+
elif element_type == "Int64Property":
|
|
506
|
+
for _ in range(count):
|
|
507
|
+
values.append(reader.read_int64())
|
|
508
|
+
elif element_type == "UInt64Property":
|
|
509
|
+
for _ in range(count):
|
|
510
|
+
values.append(reader.read_uint64())
|
|
511
|
+
elif element_type == "Int16Property":
|
|
512
|
+
for _ in range(count):
|
|
513
|
+
values.append(reader.read_int16())
|
|
514
|
+
elif element_type == "UInt16Property":
|
|
515
|
+
for _ in range(count):
|
|
516
|
+
values.append(reader.read_uint16())
|
|
517
|
+
elif element_type == "Int8Property":
|
|
518
|
+
for _ in range(count):
|
|
519
|
+
values.append(reader.read_int8())
|
|
520
|
+
elif element_type == "ByteProperty":
|
|
521
|
+
for _ in range(count):
|
|
522
|
+
values.append(reader.read_uint8())
|
|
523
|
+
elif element_type == "FloatProperty":
|
|
524
|
+
for _ in range(count):
|
|
525
|
+
values.append(reader.read_float())
|
|
526
|
+
elif element_type == "DoubleProperty":
|
|
527
|
+
for _ in range(count):
|
|
528
|
+
values.append(reader.read_double())
|
|
529
|
+
elif element_type == "BoolProperty":
|
|
530
|
+
for _ in range(count):
|
|
531
|
+
values.append(reader.read_uint8() != 0)
|
|
532
|
+
elif element_type == "StrProperty":
|
|
533
|
+
for _ in range(count):
|
|
534
|
+
values.append(reader.read_string())
|
|
535
|
+
elif element_type == "NameProperty":
|
|
536
|
+
# Names use the name table
|
|
537
|
+
for _ in range(count):
|
|
538
|
+
name_id = reader.read_int32()
|
|
539
|
+
name_instance = reader.read_int32()
|
|
540
|
+
name = name_table.get(name_id, f"__UNKNOWN_{name_id}__")
|
|
541
|
+
if name_instance > 0:
|
|
542
|
+
name = f"{name}_{name_instance - 1}"
|
|
543
|
+
values.append(name)
|
|
544
|
+
elif element_type == "ObjectProperty":
|
|
545
|
+
# WorldSave object references: 2 bytes marker + name(8) or GUID(16)
|
|
546
|
+
from uuid import UUID
|
|
547
|
+
|
|
548
|
+
for _ in range(count):
|
|
549
|
+
marker = reader.read_uint16()
|
|
550
|
+
if marker == 1:
|
|
551
|
+
# Name reference: name_id(4) + name_instance(4)
|
|
552
|
+
ref_name_id = reader.read_int32()
|
|
553
|
+
ref_name_inst = reader.read_int32()
|
|
554
|
+
ref_name = name_table.get(ref_name_id, f"__UNKNOWN_{ref_name_id}__")
|
|
555
|
+
if ref_name_inst > 0:
|
|
556
|
+
ref_name = f"{ref_name}_{ref_name_inst - 1}"
|
|
557
|
+
values.append(ref_name)
|
|
558
|
+
else:
|
|
559
|
+
# GUID reference: 16 bytes
|
|
560
|
+
guid_bytes = reader.read_bytes(16)
|
|
561
|
+
if all(b == 0 for b in guid_bytes):
|
|
562
|
+
values.append(None)
|
|
563
|
+
else:
|
|
564
|
+
guid = UUID(bytes_le=guid_bytes)
|
|
565
|
+
values.append(str(guid))
|
|
566
|
+
elif element_type == "StructProperty":
|
|
567
|
+
# This shouldn't be called anymore - struct arrays use
|
|
568
|
+
# _read_worldsave_struct_array_elements instead
|
|
569
|
+
values.append(f"<WorldSaveStructArray: {count} elements>")
|
|
570
|
+
elif element_type == "SoftObjectProperty":
|
|
571
|
+
# WorldSave SoftObjectProperty array elements:
|
|
572
|
+
# Each element is: name_ref(8) + padding(4) = 12 bytes
|
|
573
|
+
for _ in range(count):
|
|
574
|
+
name_id = reader.read_int32()
|
|
575
|
+
name_instance = reader.read_int32()
|
|
576
|
+
ref_name = name_table.get(name_id, f"__UNKNOWN_{name_id}__")
|
|
577
|
+
if name_instance > 0:
|
|
578
|
+
ref_name = f"{ref_name}_{name_instance - 1}"
|
|
579
|
+
_padding = reader.read_int32()
|
|
580
|
+
values.append(ref_name)
|
|
581
|
+
else:
|
|
582
|
+
# Unknown type
|
|
583
|
+
values.append(f"<UnknownWorldSaveArray({element_type}): {count} elements>")
|
|
584
|
+
|
|
585
|
+
return values
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _read_worldsave_struct_array_elements(
|
|
589
|
+
reader: BinaryReader,
|
|
590
|
+
struct_type: str,
|
|
591
|
+
count: int,
|
|
592
|
+
name_table: dict[int, str],
|
|
593
|
+
) -> list[t.Any]:
|
|
594
|
+
"""
|
|
595
|
+
Read struct array elements for ASA WorldSave format.
|
|
596
|
+
|
|
597
|
+
There are two categories of struct types:
|
|
598
|
+
1. **Native structs** (Color, LinearColor, Vector, etc.): Fixed binary size,
|
|
599
|
+
no property headers or None terminators. Each element is read using the
|
|
600
|
+
native struct reader from the struct registry.
|
|
601
|
+
2. **Property-list structs** (CustomItemData, etc.): Each element is a list
|
|
602
|
+
of properties terminated by a None marker (8 bytes: name_id + inst).
|
|
603
|
+
|
|
604
|
+
Args:
|
|
605
|
+
reader: The binary reader.
|
|
606
|
+
struct_type: The struct type name from array header.
|
|
607
|
+
count: Number of struct elements.
|
|
608
|
+
name_table: The name table dictionary.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
List of struct element values (dicts for native, dicts for prop-lists).
|
|
612
|
+
"""
|
|
613
|
+
from ..structs.registry import STRUCT_REGISTRY, read_struct
|
|
614
|
+
from .registry import read_property
|
|
615
|
+
|
|
616
|
+
values: list[t.Any] = []
|
|
617
|
+
|
|
618
|
+
# Check if this is a native struct type (fixed binary format)
|
|
619
|
+
if struct_type in STRUCT_REGISTRY:
|
|
620
|
+
# Native struct: each element is a fixed-size binary blob
|
|
621
|
+
for _ in range(count):
|
|
622
|
+
struct_obj = read_struct(
|
|
623
|
+
reader,
|
|
624
|
+
struct_type,
|
|
625
|
+
is_asa=True,
|
|
626
|
+
name_table=name_table,
|
|
627
|
+
worldsave_format=True,
|
|
628
|
+
)
|
|
629
|
+
if hasattr(struct_obj, "to_dict"):
|
|
630
|
+
values.append(struct_obj.to_dict())
|
|
631
|
+
else:
|
|
632
|
+
values.append(struct_obj)
|
|
633
|
+
return values
|
|
634
|
+
|
|
635
|
+
# Property-list struct: each element is properties until None terminator
|
|
636
|
+
for _ in range(count):
|
|
637
|
+
struct_props: dict[str, t.Any] = {"_struct_type": struct_type}
|
|
638
|
+
|
|
639
|
+
while True:
|
|
640
|
+
prop = read_property(
|
|
641
|
+
reader,
|
|
642
|
+
is_asa=True,
|
|
643
|
+
name_table=name_table,
|
|
644
|
+
worldsave_format=True,
|
|
645
|
+
)
|
|
646
|
+
if prop is None:
|
|
647
|
+
# None terminator marks end of struct element
|
|
648
|
+
break
|
|
649
|
+
|
|
650
|
+
# Add property to struct
|
|
651
|
+
# Handle array indices for same-named properties
|
|
652
|
+
key = prop.name
|
|
653
|
+
if prop.index > 0:
|
|
654
|
+
key = f"{prop.name}[{prop.index}]"
|
|
655
|
+
|
|
656
|
+
if hasattr(prop, "value"):
|
|
657
|
+
struct_props[key] = prop.value
|
|
658
|
+
else:
|
|
659
|
+
struct_props[key] = prop
|
|
660
|
+
|
|
661
|
+
values.append(struct_props)
|
|
662
|
+
|
|
663
|
+
return values
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
# =============================================================================
|
|
667
|
+
# Struct Property
|
|
668
|
+
# =============================================================================
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
@dataclass
|
|
672
|
+
class StructProperty(Property):
|
|
673
|
+
"""
|
|
674
|
+
Struct property - contains structured data.
|
|
675
|
+
|
|
676
|
+
Format:
|
|
677
|
+
Header + Name structType + [type-specific fields] + value
|
|
678
|
+
|
|
679
|
+
Structs come in two forms:
|
|
680
|
+
1. Native structs: Fixed format known by name (Vector, Rotator, Color, etc.)
|
|
681
|
+
2. Property-based structs: List of properties terminated by "None"
|
|
682
|
+
"""
|
|
683
|
+
|
|
684
|
+
name: str
|
|
685
|
+
index: int = 0
|
|
686
|
+
struct_type: str = ""
|
|
687
|
+
_value: t.Any = None # Struct object from structs module
|
|
688
|
+
|
|
689
|
+
@property
|
|
690
|
+
def type_name(self) -> str:
|
|
691
|
+
return "StructProperty"
|
|
692
|
+
|
|
693
|
+
@property
|
|
694
|
+
def value(self) -> t.Any:
|
|
695
|
+
"""Returns the struct value (Struct object or dict for serialization)."""
|
|
696
|
+
if hasattr(self._value, "to_dict"):
|
|
697
|
+
return self._value.to_dict()
|
|
698
|
+
return self._value
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def struct(self) -> t.Any:
|
|
702
|
+
"""Returns the raw Struct object."""
|
|
703
|
+
return self._value
|
|
704
|
+
|
|
705
|
+
@classmethod
|
|
706
|
+
def read(
|
|
707
|
+
cls,
|
|
708
|
+
reader: BinaryReader,
|
|
709
|
+
header: PropertyHeader,
|
|
710
|
+
is_asa: bool = False,
|
|
711
|
+
name_table: list[str] | None = None,
|
|
712
|
+
worldsave_format: bool = False,
|
|
713
|
+
) -> StructProperty:
|
|
714
|
+
"""
|
|
715
|
+
Read a StructProperty from the archive.
|
|
716
|
+
|
|
717
|
+
Uses the struct registry to dispatch to the appropriate struct type.
|
|
718
|
+
|
|
719
|
+
ASE format after header (also used by ASA profiles/tribes):
|
|
720
|
+
- StructType (name)
|
|
721
|
+
- [struct data]
|
|
722
|
+
|
|
723
|
+
ASA CloudInventory format after header (version 7+):
|
|
724
|
+
- StructType (string, length from header.index)
|
|
725
|
+
- Extra1 (int32, usually 1)
|
|
726
|
+
- ScriptPath (string, e.g. "/Script/ShooterGame")
|
|
727
|
+
- Zeros (int32, usually 0)
|
|
728
|
+
- DataSize (int32)
|
|
729
|
+
- ExtraByte (byte) - bit 0 indicates struct data has index prefix
|
|
730
|
+
- [struct data]
|
|
731
|
+
|
|
732
|
+
ASA WorldSave format after header:
|
|
733
|
+
- Index (int32) = 1
|
|
734
|
+
- StructTypeID (int32, name table ref) + Instance (int32)
|
|
735
|
+
- 8 zero bytes
|
|
736
|
+
- DataSize (int32)
|
|
737
|
+
- ArrayIndex (byte) = property array index
|
|
738
|
+
- [struct data]
|
|
739
|
+
|
|
740
|
+
Detection: ASA cloud inventory uses header.data_size=1 as a flag, while
|
|
741
|
+
profiles/tribes have actual data sizes > 1.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
reader: The binary reader.
|
|
745
|
+
header: The property header.
|
|
746
|
+
is_asa: True for ASA format.
|
|
747
|
+
name_table: Optional name table for world saves (version 6+).
|
|
748
|
+
worldsave_format: True for ASA WorldSave SQLite object format.
|
|
749
|
+
"""
|
|
750
|
+
has_index_prefix = False
|
|
751
|
+
|
|
752
|
+
if worldsave_format:
|
|
753
|
+
return cls._read_worldsave_struct(reader, header, name_table)
|
|
754
|
+
|
|
755
|
+
# ASA cloud inventory (version 7+) uses a special format where:
|
|
756
|
+
# - header.data_size is a flag (1) instead of actual size
|
|
757
|
+
# - header.index contains the struct_type string length
|
|
758
|
+
# ASA profiles/tribes (version 6) use ASE-style format with actual sizes
|
|
759
|
+
use_asa_cloud_format = is_asa and header.data_size == 1 and header.index > 0
|
|
760
|
+
|
|
761
|
+
if use_asa_cloud_format:
|
|
762
|
+
# ASA CloudInventory format
|
|
763
|
+
# The property header already read data_size and index, where:
|
|
764
|
+
# - data_size is a flag (usually 1)
|
|
765
|
+
# - index is the length of the struct_type string
|
|
766
|
+
# So we read the struct_type string directly (its length is header.index)
|
|
767
|
+
struct_type_len = header.index
|
|
768
|
+
struct_type_bytes = reader.read_bytes(struct_type_len)
|
|
769
|
+
struct_type = struct_type_bytes[:-1].decode("latin-1") # Remove null terminator
|
|
770
|
+
|
|
771
|
+
_extra1 = reader.read_int32() # Usually 1
|
|
772
|
+
_script_path = reader.read_string() # e.g. "/Script/ShooterGame"
|
|
773
|
+
_zeros = reader.read_int32() # Usually 0
|
|
774
|
+
_data_size = reader.read_int32() # Size of struct data
|
|
775
|
+
|
|
776
|
+
# Use header.data_size as the index for ASA (usually 1, but could vary)
|
|
777
|
+
index = header.data_size
|
|
778
|
+
|
|
779
|
+
# Read the extra byte that appears before struct data
|
|
780
|
+
extra_byte = reader.read_uint8()
|
|
781
|
+
|
|
782
|
+
# If extra_byte bit 0 is set, struct data has an index prefix
|
|
783
|
+
has_index_prefix = bool(extra_byte & 0x01)
|
|
784
|
+
else:
|
|
785
|
+
# ASE format / ASA profile/tribe format - uses name table if available
|
|
786
|
+
index = header.index
|
|
787
|
+
struct_type = read_name(reader, name_table)
|
|
788
|
+
|
|
789
|
+
# ASA profiles/tribes (version 6) have a 17-byte header after the struct type
|
|
790
|
+
# for property-list structs (structs NOT in the native struct registry).
|
|
791
|
+
# This 17-byte block is: extra1(4) + script_path_len(4, value 0) + zeros(4) +
|
|
792
|
+
# data_size(4) + extra_byte(1) — all zeros in v6 files.
|
|
793
|
+
# Native structs (UniqueNetIdRepl, LinearColor, Vector, etc.) have their raw data
|
|
794
|
+
# immediately after the struct_type string with no padding.
|
|
795
|
+
if is_asa:
|
|
796
|
+
from ..structs.registry import STRUCT_REGISTRY
|
|
797
|
+
|
|
798
|
+
if struct_type not in STRUCT_REGISTRY:
|
|
799
|
+
reader.skip(17)
|
|
800
|
+
|
|
801
|
+
# Use the struct registry to read the value
|
|
802
|
+
from ..structs.registry import read_struct
|
|
803
|
+
|
|
804
|
+
struct_value = read_struct(reader, struct_type, is_asa, has_index_prefix, name_table=name_table)
|
|
805
|
+
|
|
806
|
+
return cls(
|
|
807
|
+
name=header.name,
|
|
808
|
+
index=index,
|
|
809
|
+
struct_type=struct_type,
|
|
810
|
+
_value=struct_value,
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
@classmethod
|
|
814
|
+
def _read_worldsave_struct(
|
|
815
|
+
cls,
|
|
816
|
+
reader: BinaryReader,
|
|
817
|
+
header: PropertyHeader,
|
|
818
|
+
name_table: dict[int, str] | None,
|
|
819
|
+
) -> StructProperty:
|
|
820
|
+
"""
|
|
821
|
+
Read a StructProperty in ASA WorldSave format.
|
|
822
|
+
|
|
823
|
+
WorldSave struct format (after property header NameID + NameInstance + TypeID):
|
|
824
|
+
- Struct Header (int32) = 1
|
|
825
|
+
- StructTypeID (int32) = name table reference (e.g., ItemNetID)
|
|
826
|
+
- 4 zero bytes
|
|
827
|
+
- Second Struct Header (int32) = 1
|
|
828
|
+
- BlueprintTypeID (int32) = name table reference (e.g., /Script/ShooterGame)
|
|
829
|
+
- 8 zero bytes
|
|
830
|
+
- DataSize (int32) = size of struct data
|
|
831
|
+
- Flag/Terminator byte (1 byte)
|
|
832
|
+
- [struct data - property list or native struct]
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
reader: The binary reader.
|
|
836
|
+
header: The property header (already parsed).
|
|
837
|
+
name_table: The name table dictionary.
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
StructProperty with parsed value.
|
|
841
|
+
"""
|
|
842
|
+
if name_table is None:
|
|
843
|
+
raise ValueError("WorldSave struct format requires a name table")
|
|
844
|
+
|
|
845
|
+
# Read first struct header: usually 1, sometimes 2+ for structs with extra type refs
|
|
846
|
+
_struct_header1 = reader.read_int32()
|
|
847
|
+
|
|
848
|
+
# Read struct type from name table
|
|
849
|
+
struct_type_id = reader.read_int32()
|
|
850
|
+
struct_type = name_table.get(struct_type_id, f"__UNKNOWN_{struct_type_id}__")
|
|
851
|
+
|
|
852
|
+
# 4 zeros after struct type (struct_type_instance)
|
|
853
|
+
_zeros1 = reader.read_int32()
|
|
854
|
+
|
|
855
|
+
# Read second struct header (should be 1)
|
|
856
|
+
_struct_header2 = reader.read_int32()
|
|
857
|
+
|
|
858
|
+
# Read blueprint type from name table
|
|
859
|
+
_blueprint_type_id = reader.read_int32()
|
|
860
|
+
|
|
861
|
+
# Padding after blueprint (blueprint_instance + additional zeros)
|
|
862
|
+
_zeros2 = reader.read_int32()
|
|
863
|
+
_zeros3 = reader.read_int32()
|
|
864
|
+
|
|
865
|
+
# If _struct_header1 > 1, read extra name reference groups
|
|
866
|
+
# Each extra group is: name_id(4) + name_inst(4) + zeros(4) = 12 bytes
|
|
867
|
+
for _ in range(_struct_header1 - 1):
|
|
868
|
+
_extra_name_id = reader.read_int32()
|
|
869
|
+
_extra_name_inst = reader.read_int32()
|
|
870
|
+
_extra_zeros = reader.read_int32()
|
|
871
|
+
|
|
872
|
+
# Read data size
|
|
873
|
+
_data_size = reader.read_int32()
|
|
874
|
+
|
|
875
|
+
# Read flag/terminator byte
|
|
876
|
+
flag_byte = reader.read_uint8()
|
|
877
|
+
|
|
878
|
+
# Array index is encoded in flag if bit 0 is set
|
|
879
|
+
array_index = 0
|
|
880
|
+
if flag_byte & 0x01:
|
|
881
|
+
array_index = reader.read_int32()
|
|
882
|
+
|
|
883
|
+
# Use the struct registry to read the value
|
|
884
|
+
# For WorldSave, we pass worldsave_format=True to the struct reader
|
|
885
|
+
from ..structs.registry import read_struct
|
|
886
|
+
|
|
887
|
+
struct_value = read_struct(
|
|
888
|
+
reader,
|
|
889
|
+
struct_type,
|
|
890
|
+
is_asa=True,
|
|
891
|
+
has_index_prefix=False,
|
|
892
|
+
name_table=name_table,
|
|
893
|
+
worldsave_format=True,
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
return cls(
|
|
897
|
+
name=header.name,
|
|
898
|
+
index=array_index,
|
|
899
|
+
struct_type=struct_type,
|
|
900
|
+
_value=struct_value,
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
# =============================================================================
|
|
905
|
+
# Map Property
|
|
906
|
+
# =============================================================================
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
@dataclass
|
|
910
|
+
class MapProperty(Property):
|
|
911
|
+
"""
|
|
912
|
+
Map property - contains key-value pairs.
|
|
913
|
+
|
|
914
|
+
Format:
|
|
915
|
+
Header + Name keyType + Name valueType + UInt8 unknown + Int32 count + [entries...]
|
|
916
|
+
|
|
917
|
+
Less common than arrays and structs.
|
|
918
|
+
"""
|
|
919
|
+
|
|
920
|
+
name: str
|
|
921
|
+
index: int = 0
|
|
922
|
+
key_type: str = ""
|
|
923
|
+
value_type: str = ""
|
|
924
|
+
_entries: dict[t.Any, t.Any] = field(default_factory=dict)
|
|
925
|
+
|
|
926
|
+
@property
|
|
927
|
+
def type_name(self) -> str:
|
|
928
|
+
return "MapProperty"
|
|
929
|
+
|
|
930
|
+
@property
|
|
931
|
+
def value(self) -> dict[t.Any, t.Any]:
|
|
932
|
+
return self._entries
|
|
933
|
+
|
|
934
|
+
@property
|
|
935
|
+
def count(self) -> int:
|
|
936
|
+
return len(self._entries)
|
|
937
|
+
|
|
938
|
+
@classmethod
|
|
939
|
+
def read(
|
|
940
|
+
cls,
|
|
941
|
+
reader: BinaryReader,
|
|
942
|
+
header: PropertyHeader,
|
|
943
|
+
is_asa: bool = False,
|
|
944
|
+
name_table: list[str] | None = None,
|
|
945
|
+
worldsave_format: bool = False,
|
|
946
|
+
) -> MapProperty:
|
|
947
|
+
"""
|
|
948
|
+
Read a MapProperty from the archive.
|
|
949
|
+
|
|
950
|
+
Args:
|
|
951
|
+
reader: The binary reader.
|
|
952
|
+
header: The property header.
|
|
953
|
+
is_asa: True for ASA format.
|
|
954
|
+
name_table: Optional name table for world saves (version 6+).
|
|
955
|
+
worldsave_format: True for ASA WorldSave SQLite object format.
|
|
956
|
+
"""
|
|
957
|
+
if worldsave_format:
|
|
958
|
+
return cls._read_worldsave_map(reader, header, name_table)
|
|
959
|
+
|
|
960
|
+
# Read key and value types - uses name table if available
|
|
961
|
+
key_type = read_name(reader, name_table)
|
|
962
|
+
value_type = read_name(reader, name_table)
|
|
963
|
+
|
|
964
|
+
if is_asa:
|
|
965
|
+
reader.skip(1) # ASA has extra byte
|
|
966
|
+
|
|
967
|
+
# Unknown byte
|
|
968
|
+
_unknown = reader.read_uint8()
|
|
969
|
+
|
|
970
|
+
# Read count
|
|
971
|
+
count = reader.read_int32()
|
|
972
|
+
|
|
973
|
+
# For now, store raw bytes for the entries
|
|
974
|
+
# Full implementation needs type-specific parsing
|
|
975
|
+
entries: dict[t.Any, t.Any] = {}
|
|
976
|
+
|
|
977
|
+
if count > 0:
|
|
978
|
+
# Read remaining data as raw bytes
|
|
979
|
+
# This is a placeholder - full implementation needs type registry
|
|
980
|
+
remaining = header.data_size - 4 - len(key_type) - 5 - len(value_type) - 5 - 1 - 4
|
|
981
|
+
if remaining > 0:
|
|
982
|
+
raw_data = reader.read_bytes(remaining)
|
|
983
|
+
entries["_raw"] = raw_data
|
|
984
|
+
entries["_note"] = f"Map with {count} entries, needs type-specific parsing"
|
|
985
|
+
|
|
986
|
+
return cls(
|
|
987
|
+
name=header.name,
|
|
988
|
+
index=header.index,
|
|
989
|
+
key_type=key_type,
|
|
990
|
+
value_type=value_type,
|
|
991
|
+
_entries=entries,
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
@classmethod
|
|
995
|
+
def _read_worldsave_map(
|
|
996
|
+
cls,
|
|
997
|
+
reader: BinaryReader,
|
|
998
|
+
header: PropertyHeader,
|
|
999
|
+
name_table: dict[int, str] | None,
|
|
1000
|
+
) -> MapProperty:
|
|
1001
|
+
"""
|
|
1002
|
+
Read a MapProperty in ASA WorldSave format.
|
|
1003
|
+
|
|
1004
|
+
WorldSave MapProperty format after 16-byte header:
|
|
1005
|
+
- Marker (4) = 2 (two type references: key + value)
|
|
1006
|
+
- Key type name: ID(4) + Instance(4)
|
|
1007
|
+
- If key type is NOT StructProperty: padding(4)
|
|
1008
|
+
- If key type IS StructProperty: struct sub-header
|
|
1009
|
+
- Value type name: ID(4) + Instance(4)
|
|
1010
|
+
- If value type is NOT StructProperty: padding(4)
|
|
1011
|
+
- If value type IS StructProperty: struct sub-header
|
|
1012
|
+
(marker(4)=1 + struct_type(8) + marker2(4)=1 + script_path(8) + zeros(4))
|
|
1013
|
+
- DataSize(4) + Flag(1) + SkipCount(4) + MapCount(4)
|
|
1014
|
+
- [Entries...]
|
|
1015
|
+
"""
|
|
1016
|
+
if name_table is None:
|
|
1017
|
+
raise ValueError("WorldSave MapProperty requires a name table")
|
|
1018
|
+
|
|
1019
|
+
# Read marker (should be 2 for Map: key + value types)
|
|
1020
|
+
_marker = reader.read_int32()
|
|
1021
|
+
|
|
1022
|
+
# Read key type
|
|
1023
|
+
key_type_id = reader.read_int32()
|
|
1024
|
+
_key_type_inst = reader.read_int32()
|
|
1025
|
+
key_type = name_table.get(key_type_id, f"__UNKNOWN_{key_type_id}__")
|
|
1026
|
+
|
|
1027
|
+
# Handle key type sub-header
|
|
1028
|
+
if key_type == "StructProperty":
|
|
1029
|
+
# Key is a struct - read struct sub-header
|
|
1030
|
+
_struct_marker = reader.read_int32()
|
|
1031
|
+
_key_struct_type_id = reader.read_int32()
|
|
1032
|
+
_key_struct_type_inst = reader.read_int32()
|
|
1033
|
+
_script_marker = reader.read_int32()
|
|
1034
|
+
_key_script_path_id = reader.read_int32()
|
|
1035
|
+
_key_script_path_inst = reader.read_int32()
|
|
1036
|
+
_key_zeros = reader.read_int32()
|
|
1037
|
+
else:
|
|
1038
|
+
# Simple key type - just padding
|
|
1039
|
+
_pad_after_key = reader.read_int32()
|
|
1040
|
+
|
|
1041
|
+
# Read value type
|
|
1042
|
+
value_type_id = reader.read_int32()
|
|
1043
|
+
_value_type_inst = reader.read_int32()
|
|
1044
|
+
value_type = name_table.get(value_type_id, f"__UNKNOWN_{value_type_id}__")
|
|
1045
|
+
|
|
1046
|
+
# Handle value type sub-header
|
|
1047
|
+
if value_type == "StructProperty":
|
|
1048
|
+
_struct_marker = reader.read_int32() # Usually 1, sometimes 2+
|
|
1049
|
+
struct_type_id = reader.read_int32()
|
|
1050
|
+
_struct_type_inst = reader.read_int32()
|
|
1051
|
+
_script_marker = reader.read_int32()
|
|
1052
|
+
_script_path_id = reader.read_int32()
|
|
1053
|
+
_script_path_inst = reader.read_int32()
|
|
1054
|
+
_zeros = reader.read_int32()
|
|
1055
|
+
# If _struct_marker > 1, read extra name reference groups
|
|
1056
|
+
for _ in range(_struct_marker - 1):
|
|
1057
|
+
_extra_name_id = reader.read_int32()
|
|
1058
|
+
_extra_name_inst = reader.read_int32()
|
|
1059
|
+
_extra_zeros = reader.read_int32()
|
|
1060
|
+
else:
|
|
1061
|
+
_pad_after_value = reader.read_int32()
|
|
1062
|
+
|
|
1063
|
+
# Read data_size, flag, skipCount, mapCount
|
|
1064
|
+
data_size = reader.read_int32()
|
|
1065
|
+
_flag = reader.read_uint8()
|
|
1066
|
+
_skip_count = reader.read_int32()
|
|
1067
|
+
map_count = reader.read_int32()
|
|
1068
|
+
|
|
1069
|
+
# Calculate the end position for the map data
|
|
1070
|
+
# data_size appears to be total bytes from after flag to end of entries
|
|
1071
|
+
# which includes: skipCount(4) + mapCount(4) + entry_data
|
|
1072
|
+
entries_end = reader.position + (data_size - 8) if data_size > 8 else reader.position
|
|
1073
|
+
|
|
1074
|
+
entries: dict[t.Any, t.Any] = {}
|
|
1075
|
+
|
|
1076
|
+
if map_count > 0:
|
|
1077
|
+
try:
|
|
1078
|
+
for _ in range(map_count):
|
|
1079
|
+
# Read key based on key type
|
|
1080
|
+
if key_type == "NameProperty":
|
|
1081
|
+
key_name_id = reader.read_int32()
|
|
1082
|
+
_key_name_inst = reader.read_int32()
|
|
1083
|
+
key_val = name_table.get(key_name_id, f"__UNKNOWN_{key_name_id}__")
|
|
1084
|
+
elif key_type == "ObjectProperty":
|
|
1085
|
+
# Object reference as key
|
|
1086
|
+
key_val = reader.read_bytes(16).hex()
|
|
1087
|
+
reader.skip(1)
|
|
1088
|
+
else:
|
|
1089
|
+
# Generic: read name reference
|
|
1090
|
+
key_id = reader.read_int32()
|
|
1091
|
+
_key_inst = reader.read_int32()
|
|
1092
|
+
key_val = name_table.get(key_id, f"__UNKNOWN_{key_id}__")
|
|
1093
|
+
|
|
1094
|
+
# Read value based on value type
|
|
1095
|
+
if value_type == "StructProperty":
|
|
1096
|
+
# Value is a struct property list (ends with None)
|
|
1097
|
+
from .registry import read_properties
|
|
1098
|
+
|
|
1099
|
+
struct_props = read_properties(
|
|
1100
|
+
reader,
|
|
1101
|
+
is_asa=True,
|
|
1102
|
+
name_table=name_table,
|
|
1103
|
+
worldsave_format=True,
|
|
1104
|
+
)
|
|
1105
|
+
# Convert to dict of name → value
|
|
1106
|
+
val_dict: dict[str, t.Any] = {}
|
|
1107
|
+
for p in struct_props:
|
|
1108
|
+
val_dict[p.name] = p.value
|
|
1109
|
+
entries[key_val] = val_dict
|
|
1110
|
+
else:
|
|
1111
|
+
# For other value types, try reading a simple value
|
|
1112
|
+
entries[key_val] = f"<{value_type} value>"
|
|
1113
|
+
|
|
1114
|
+
except Exception:
|
|
1115
|
+
# If entry parsing fails, skip to calculated end
|
|
1116
|
+
if reader.position < entries_end:
|
|
1117
|
+
reader.position = entries_end
|
|
1118
|
+
|
|
1119
|
+
return cls(
|
|
1120
|
+
name=header.name,
|
|
1121
|
+
index=header.index,
|
|
1122
|
+
key_type=key_type,
|
|
1123
|
+
value_type=value_type,
|
|
1124
|
+
_entries=entries,
|
|
1125
|
+
)
|