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.
Files changed (46) hide show
  1. arkparser/__init__.py +117 -0
  2. arkparser/common/__init__.py +72 -0
  3. arkparser/common/binary_reader.py +402 -0
  4. arkparser/common/exceptions.py +99 -0
  5. arkparser/common/map_config.py +166 -0
  6. arkparser/common/types.py +249 -0
  7. arkparser/common/version_detection.py +195 -0
  8. arkparser/data_models.py +801 -0
  9. arkparser/export.py +485 -0
  10. arkparser/files/__init__.py +25 -0
  11. arkparser/files/base.py +309 -0
  12. arkparser/files/cloud_inventory.py +259 -0
  13. arkparser/files/profile.py +205 -0
  14. arkparser/files/tribe.py +155 -0
  15. arkparser/files/world_save.py +699 -0
  16. arkparser/game_objects/__init__.py +32 -0
  17. arkparser/game_objects/container.py +180 -0
  18. arkparser/game_objects/game_object.py +273 -0
  19. arkparser/game_objects/location.py +87 -0
  20. arkparser/models/__init__.py +29 -0
  21. arkparser/models/character.py +227 -0
  22. arkparser/models/creature.py +642 -0
  23. arkparser/models/item.py +207 -0
  24. arkparser/models/player.py +263 -0
  25. arkparser/models/stats.py +226 -0
  26. arkparser/models/structure.py +176 -0
  27. arkparser/models/tribe.py +291 -0
  28. arkparser/properties/__init__.py +77 -0
  29. arkparser/properties/base.py +329 -0
  30. arkparser/properties/byte_property.py +230 -0
  31. arkparser/properties/compound.py +1125 -0
  32. arkparser/properties/primitives.py +803 -0
  33. arkparser/properties/registry.py +236 -0
  34. arkparser/py.typed +0 -0
  35. arkparser/structs/__init__.py +60 -0
  36. arkparser/structs/base.py +63 -0
  37. arkparser/structs/colors.py +108 -0
  38. arkparser/structs/misc.py +133 -0
  39. arkparser/structs/property_list.py +101 -0
  40. arkparser/structs/registry.py +140 -0
  41. arkparser/structs/vectors.py +221 -0
  42. arkparser-0.1.0.dist-info/METADATA +833 -0
  43. arkparser-0.1.0.dist-info/RECORD +46 -0
  44. arkparser-0.1.0.dist-info/WHEEL +5 -0
  45. arkparser-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. 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
+ )