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,236 @@
1
+ """
2
+ Property Registry - Type Dispatch for Property Reading.
3
+
4
+ This module provides the central registry for reading properties from binary data.
5
+ It dispatches to the appropriate property class based on the type name in the header.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typing as t
11
+
12
+ from ..common.binary_reader import BinaryReader
13
+ from ..common.exceptions import UnknownPropertyError
14
+ from .base import NameTable, Property, PropertyHeader, read_property_header
15
+ from .byte_property import ByteProperty
16
+ from .compound import ArrayProperty, MapProperty, StructProperty
17
+ from .primitives import (
18
+ BoolProperty,
19
+ DoubleProperty,
20
+ FloatProperty,
21
+ Int8Property,
22
+ Int16Property,
23
+ Int64Property,
24
+ IntProperty,
25
+ NameProperty,
26
+ ObjectProperty,
27
+ SoftObjectProperty,
28
+ StrProperty,
29
+ UInt16Property,
30
+ UInt32Property,
31
+ UInt64Property,
32
+ )
33
+
34
+ # Type alias for property reader functions
35
+ PropertyReader = t.Callable[[BinaryReader, PropertyHeader, bool], Property]
36
+
37
+
38
+ # Registry mapping type names to their reader classes
39
+ PROPERTY_REGISTRY: dict[str, type[Property]] = {
40
+ # Numeric properties
41
+ "Int8Property": Int8Property,
42
+ "Int16Property": Int16Property,
43
+ "IntProperty": IntProperty,
44
+ "Int64Property": Int64Property,
45
+ "UInt16Property": UInt16Property,
46
+ "UInt32Property": UInt32Property,
47
+ "UInt64Property": UInt64Property,
48
+ "FloatProperty": FloatProperty,
49
+ "DoubleProperty": DoubleProperty,
50
+ # Boolean
51
+ "BoolProperty": BoolProperty,
52
+ # String/Name
53
+ "StrProperty": StrProperty,
54
+ "NameProperty": NameProperty,
55
+ # Object reference
56
+ "ObjectProperty": ObjectProperty,
57
+ # Soft object reference (asset paths)
58
+ "SoftObjectProperty": SoftObjectProperty,
59
+ # Byte (can be raw or enum)
60
+ "ByteProperty": ByteProperty,
61
+ # Compound types
62
+ "ArrayProperty": ArrayProperty,
63
+ "StructProperty": StructProperty,
64
+ "MapProperty": MapProperty,
65
+ }
66
+
67
+
68
+ def read_property(
69
+ reader: BinaryReader,
70
+ is_asa: bool = False,
71
+ name_table: NameTable = None,
72
+ worldsave_format: bool = False,
73
+ ) -> Property | None:
74
+ """
75
+ Read a single property from the binary reader.
76
+
77
+ Reads the property header, then dispatches to the appropriate
78
+ property type's read method.
79
+
80
+ Args:
81
+ reader: The binary reader positioned at the property header.
82
+ is_asa: True for ASA format, False for ASE.
83
+ name_table: Optional name table for world saves.
84
+ - list: ASE format (1-based index)
85
+ - dict: ASA format (hash key)
86
+ worldsave_format: True for ASA WorldSave SQLite object format.
87
+
88
+ Returns:
89
+ The parsed Property object, or None if the terminating "None" property
90
+ was encountered.
91
+
92
+ Raises:
93
+ UnknownPropertyError: If the property type is not recognized.
94
+ """
95
+ header = read_property_header(reader, is_asa, name_table=name_table, worldsave_format=worldsave_format)
96
+
97
+ # Check for terminator (read_property_header returns None for "None")
98
+ if header is None:
99
+ return None
100
+
101
+ # Look up the property type in the registry
102
+ property_class = PROPERTY_REGISTRY.get(header.type_name)
103
+
104
+ if property_class is None:
105
+ raise UnknownPropertyError(
106
+ property_type=header.type_name,
107
+ position=reader.position,
108
+ )
109
+
110
+ # Read the property value
111
+ # In WorldSave format, all properties need the flag and name_table
112
+ if worldsave_format:
113
+ return property_class.read(reader, header, is_asa, name_table=name_table, worldsave_format=worldsave_format)
114
+ elif header.type_name in (
115
+ "ArrayProperty",
116
+ "StructProperty",
117
+ "MapProperty",
118
+ "ByteProperty",
119
+ "NameProperty",
120
+ "ObjectProperty",
121
+ ):
122
+ # These types need name_table even in non-worldsave mode
123
+ return property_class.read(reader, header, is_asa, name_table=name_table, worldsave_format=worldsave_format)
124
+ else:
125
+ return property_class.read(reader, header, is_asa)
126
+
127
+
128
+ def read_properties(
129
+ reader: BinaryReader,
130
+ is_asa: bool = False,
131
+ name_table: NameTable = None,
132
+ worldsave_format: bool = False,
133
+ ) -> list[Property]:
134
+ """
135
+ Read all properties until the "None" terminator.
136
+
137
+ This reads properties in a loop until encountering a property
138
+ named "None", which signals the end of the property list.
139
+
140
+ Args:
141
+ reader: The binary reader positioned at the first property.
142
+ is_asa: True for ASA format, False for ASE.
143
+ name_table: Optional name table for world saves.
144
+ - list: ASE format (1-based index)
145
+ - dict: ASA format (hash key)
146
+ worldsave_format: True for ASA WorldSave SQLite object format.
147
+
148
+ Returns:
149
+ List of parsed Property objects.
150
+
151
+ Raises:
152
+ UnknownPropertyError: If any property type is not recognized.
153
+ """
154
+ properties: list[Property] = []
155
+
156
+ while True:
157
+ prop = read_property(reader, is_asa, name_table=name_table, worldsave_format=worldsave_format)
158
+ if prop is None:
159
+ break
160
+ properties.append(prop)
161
+
162
+ return properties
163
+
164
+
165
+ def read_properties_as_dict(
166
+ reader: BinaryReader,
167
+ is_asa: bool = False,
168
+ ) -> dict[str, Property | list[Property]]:
169
+ """
170
+ Read all properties and return as a dictionary.
171
+
172
+ Properties with the same name (different indices) are grouped into lists.
173
+
174
+ Args:
175
+ reader: The binary reader positioned at the first property.
176
+ is_asa: True for ASA format, False for ASE.
177
+
178
+ Returns:
179
+ Dictionary mapping property names to Property objects or lists of them.
180
+ """
181
+ properties = read_properties(reader, is_asa)
182
+ result: dict[str, Property | list[Property]] = {}
183
+
184
+ for prop in properties:
185
+ if prop.name in result:
186
+ existing = result[prop.name]
187
+ if isinstance(existing, list):
188
+ existing.append(prop)
189
+ else:
190
+ result[prop.name] = [existing, prop]
191
+ else:
192
+ result[prop.name] = prop
193
+
194
+ return result
195
+
196
+
197
+ def get_property_value(
198
+ properties: dict[str, Property | list[Property]],
199
+ name: str,
200
+ default: t.Any = None,
201
+ index: int | None = None,
202
+ ) -> t.Any:
203
+ """
204
+ Get a property value from a properties dictionary.
205
+
206
+ Helper function for easily extracting values from parsed properties.
207
+
208
+ Args:
209
+ properties: Dictionary of properties from read_properties_as_dict.
210
+ name: The property name to look up.
211
+ default: Default value if property not found.
212
+ index: Specific index to retrieve if there are multiple properties
213
+ with the same name. None returns the first/only one.
214
+
215
+ Returns:
216
+ The property value, or the default if not found.
217
+ """
218
+ if name not in properties:
219
+ return default
220
+
221
+ prop_or_list = properties[name]
222
+
223
+ if isinstance(prop_or_list, list):
224
+ if index is not None:
225
+ # Find the property with the matching index
226
+ for prop in prop_or_list:
227
+ if prop.index == index:
228
+ return prop.value
229
+ return default
230
+ else:
231
+ # Return the first one
232
+ return prop_or_list[0].value if prop_or_list else default
233
+ else:
234
+ if index is not None and prop_or_list.index != index:
235
+ return default
236
+ return prop_or_list.value
arkparser/py.typed ADDED
File without changes
@@ -0,0 +1,60 @@
1
+ """
2
+ ARK Struct System.
3
+
4
+ This module provides struct types for structured data within properties.
5
+
6
+ Structs come in two forms:
7
+ 1. Native structs: Fixed binary format (Vector, Color, etc.)
8
+ 2. Property-based structs: List of properties terminated by "None"
9
+
10
+ Usage:
11
+ from arkparser.structs import read_struct, Vector, Color
12
+
13
+ # Read a struct by type name
14
+ struct = read_struct(reader, "Vector", is_asa=False)
15
+
16
+ # Access struct data
17
+ if isinstance(struct, Vector):
18
+ print(f"Position: ({struct.x}, {struct.y}, {struct.z})")
19
+ """
20
+
21
+ from .base import NativeStruct, Struct
22
+ from .colors import Color, LinearColor
23
+ from .misc import CustomItemDataRef, Guid, UniqueNetIdRepl
24
+ from .property_list import StructPropertyList
25
+ from .registry import (
26
+ ARRAY_NAME_TO_STRUCT_TYPE,
27
+ STRUCT_REGISTRY,
28
+ get_array_struct_type,
29
+ is_native_struct,
30
+ read_struct,
31
+ )
32
+ from .vectors import IntPoint, IntVector, Quat, Rotator, Vector, Vector2D
33
+
34
+ __all__ = [
35
+ # Base
36
+ "Struct",
37
+ "NativeStruct",
38
+ # Vectors
39
+ "Vector",
40
+ "Vector2D",
41
+ "Rotator",
42
+ "Quat",
43
+ "IntPoint",
44
+ "IntVector",
45
+ # Colors
46
+ "Color",
47
+ "LinearColor",
48
+ # Misc
49
+ "UniqueNetIdRepl",
50
+ "Guid",
51
+ "CustomItemDataRef",
52
+ # Property-based
53
+ "StructPropertyList",
54
+ # Registry
55
+ "STRUCT_REGISTRY",
56
+ "ARRAY_NAME_TO_STRUCT_TYPE",
57
+ "read_struct",
58
+ "is_native_struct",
59
+ "get_array_struct_type",
60
+ ]
@@ -0,0 +1,63 @@
1
+ """
2
+ Struct Base Classes.
3
+
4
+ Structs in ARK save files come in two forms:
5
+ 1. Native structs: Fixed binary format, known by struct type name
6
+ 2. Property-based structs: List of properties terminated by "None"
7
+
8
+ This module provides the abstract base class and common types.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import typing as t
14
+ from abc import ABC, abstractmethod
15
+ from dataclasses import dataclass
16
+
17
+ if t.TYPE_CHECKING:
18
+ from ..common.binary_reader import BinaryReader
19
+
20
+
21
+ class Struct(ABC):
22
+ """
23
+ Abstract base class for all struct types.
24
+
25
+ Structs are typed data containers used within StructProperty values.
26
+ """
27
+
28
+ @property
29
+ @abstractmethod
30
+ def struct_type(self) -> str:
31
+ """The struct type name (e.g., 'Vector', 'Color')."""
32
+ ...
33
+
34
+ @property
35
+ @abstractmethod
36
+ def is_native(self) -> bool:
37
+ """True if this is a native (fixed format) struct."""
38
+ ...
39
+
40
+ @abstractmethod
41
+ def to_dict(self) -> dict[str, t.Any]:
42
+ """Convert the struct to a dictionary for serialization."""
43
+ ...
44
+
45
+ @classmethod
46
+ @abstractmethod
47
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> Struct:
48
+ """Read the struct from binary data."""
49
+ ...
50
+
51
+
52
+ @dataclass
53
+ class NativeStruct(Struct):
54
+ """
55
+ Base class for native structs with fixed binary formats.
56
+
57
+ Native structs have a known, fixed structure that doesn't use the
58
+ property system. Examples: Vector, Rotator, Color, etc.
59
+ """
60
+
61
+ @property
62
+ def is_native(self) -> bool:
63
+ return True
@@ -0,0 +1,108 @@
1
+ """
2
+ Color Structs.
3
+
4
+ Color-related native structs for storing RGBA color values.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typing as t
10
+ from dataclasses import dataclass
11
+
12
+ from .base import NativeStruct
13
+
14
+ if t.TYPE_CHECKING:
15
+ from ..common.binary_reader import BinaryReader
16
+
17
+
18
+ @dataclass
19
+ class Color(NativeStruct):
20
+ """
21
+ BGRA Color (8-bit per channel).
22
+
23
+ Format: b, g, r, a as UInt8 (4 bytes total).
24
+ Note: Byte order is BGRA, not RGBA!
25
+ """
26
+
27
+ b: int = 0
28
+ g: int = 0
29
+ r: int = 0
30
+ a: int = 255
31
+
32
+ @property
33
+ def struct_type(self) -> str:
34
+ return "Color"
35
+
36
+ def to_dict(self) -> dict[str, int]:
37
+ return {"r": self.r, "g": self.g, "b": self.b, "a": self.a}
38
+
39
+ @property
40
+ def rgba(self) -> tuple[int, int, int, int]:
41
+ """Get color as (r, g, b, a) tuple."""
42
+ return (self.r, self.g, self.b, self.a)
43
+
44
+ @property
45
+ def hex(self) -> str:
46
+ """Get color as hex string (#RRGGBB or #RRGGBBAA)."""
47
+ if self.a == 255:
48
+ return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
49
+ return f"#{self.r:02x}{self.g:02x}{self.b:02x}{self.a:02x}"
50
+
51
+ @classmethod
52
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> Color:
53
+ """Read a Color from the archive."""
54
+ return cls(
55
+ b=reader.read_uint8(),
56
+ g=reader.read_uint8(),
57
+ r=reader.read_uint8(),
58
+ a=reader.read_uint8(),
59
+ )
60
+
61
+
62
+ @dataclass
63
+ class LinearColor(NativeStruct):
64
+ """
65
+ Linear RGBA Color (32-bit float per channel).
66
+
67
+ Format: r, g, b, a as Float (16 bytes total).
68
+ Values are typically in range [0.0, 1.0] but can exceed for HDR.
69
+ """
70
+
71
+ r: float = 0.0
72
+ g: float = 0.0
73
+ b: float = 0.0
74
+ a: float = 1.0
75
+
76
+ @property
77
+ def struct_type(self) -> str:
78
+ return "LinearColor"
79
+
80
+ def to_dict(self) -> dict[str, float]:
81
+ return {"r": self.r, "g": self.g, "b": self.b, "a": self.a}
82
+
83
+ def to_color(self) -> Color:
84
+ """Convert to 8-bit Color (clamped to 0-255)."""
85
+ return Color(
86
+ r=max(0, min(255, int(self.r * 255))),
87
+ g=max(0, min(255, int(self.g * 255))),
88
+ b=max(0, min(255, int(self.b * 255))),
89
+ a=max(0, min(255, int(self.a * 255))),
90
+ )
91
+
92
+ @classmethod
93
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> LinearColor:
94
+ """Read a LinearColor from the archive.
95
+
96
+ Args:
97
+ reader: Binary reader positioned at the struct data.
98
+ is_asa: True for ASA format.
99
+
100
+ Note: For ASA indexed struct properties, the array index prefix is
101
+ already handled by the struct registry before this method is called.
102
+ """
103
+ return cls(
104
+ r=reader.read_float(),
105
+ g=reader.read_float(),
106
+ b=reader.read_float(),
107
+ a=reader.read_float(),
108
+ )
@@ -0,0 +1,133 @@
1
+ """
2
+ Miscellaneous Native Structs.
3
+
4
+ Various native structs that don't fit into other categories.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typing as t
10
+ from dataclasses import dataclass
11
+
12
+ from .base import NativeStruct
13
+
14
+ if t.TYPE_CHECKING:
15
+ from ..common.binary_reader import BinaryReader
16
+
17
+
18
+ @dataclass
19
+ class UniqueNetIdRepl(NativeStruct):
20
+ """
21
+ Unique Network ID for player identification.
22
+
23
+ Used for Steam IDs and other platform identifiers.
24
+
25
+ ASE Format:
26
+ - Int32 unknown
27
+ - String net_id (e.g., "2533274977850953" for Xbox)
28
+
29
+ ASA Format:
30
+ - Byte unknown
31
+ - String value_type (platform, e.g., "RedpointEOS")
32
+ - Byte length
33
+ - Bytes value (raw ID bytes, converted to hex string)
34
+ """
35
+
36
+ unknown: int = 0
37
+ net_id: str = ""
38
+ value_type: str = "" # ASA only: platform type like "RedpointEOS"
39
+
40
+ @property
41
+ def struct_type(self) -> str:
42
+ return "UniqueNetIdRepl"
43
+
44
+ def to_dict(self) -> dict[str, t.Any]:
45
+ result = {"unknown": self.unknown, "net_id": self.net_id}
46
+ if self.value_type:
47
+ result["value_type"] = self.value_type
48
+ return result
49
+
50
+ @property
51
+ def steam_id(self) -> str | None:
52
+ """Extract Steam ID if this is a Steam network ID."""
53
+ if self.net_id.startswith("steam:"):
54
+ return self.net_id[6:]
55
+ return self.net_id if self.net_id else None
56
+
57
+ @classmethod
58
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> UniqueNetIdRepl:
59
+ """Read a UniqueNetIdRepl from the archive."""
60
+ if is_asa:
61
+ # ASA format: byte unknown + string value_type + byte length + bytes value
62
+ unknown = reader.read_uint8()
63
+ value_type = reader.read_string()
64
+ length = reader.read_uint8()
65
+ value_bytes = reader.read_bytes(length)
66
+ net_id = value_bytes.hex()
67
+ return cls(unknown=unknown, net_id=net_id, value_type=value_type)
68
+ else:
69
+ # ASE format: int32 unknown + string net_id
70
+ unknown = reader.read_int32()
71
+ net_id = reader.read_string()
72
+ return cls(unknown=unknown, net_id=net_id)
73
+
74
+
75
+ @dataclass
76
+ class Guid(NativeStruct):
77
+ """
78
+ GUID/UUID structure.
79
+
80
+ 16-byte globally unique identifier.
81
+ """
82
+
83
+ value: str = "" # Stored as hex string
84
+
85
+ @property
86
+ def struct_type(self) -> str:
87
+ return "Guid"
88
+
89
+ def to_dict(self) -> dict[str, str]:
90
+ return {"guid": self.value}
91
+
92
+ @classmethod
93
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> Guid:
94
+ """Read a Guid from the archive."""
95
+ guid_value = reader.read_guid()
96
+ return cls(value=str(guid_value))
97
+
98
+
99
+ @dataclass
100
+ class CustomItemDataRef(NativeStruct):
101
+ """
102
+ Reference to custom item data.
103
+
104
+ Used for storing references to item-specific custom data.
105
+ Format: 4 x Int32 values (16 bytes)
106
+ """
107
+
108
+ value1: int = 0
109
+ value2: int = 0
110
+ value3: int = 0
111
+ value4: int = 0
112
+
113
+ @property
114
+ def struct_type(self) -> str:
115
+ return "CustomItemDataRef"
116
+
117
+ def to_dict(self) -> dict[str, int]:
118
+ return {
119
+ "value1": self.value1,
120
+ "value2": self.value2,
121
+ "value3": self.value3,
122
+ "value4": self.value4,
123
+ }
124
+
125
+ @classmethod
126
+ def read(cls, reader: BinaryReader, is_asa: bool = False, **kwargs: t.Any) -> CustomItemDataRef:
127
+ """Read a CustomItemDataRef from the archive."""
128
+ return cls(
129
+ value1=reader.read_int32(),
130
+ value2=reader.read_int32(),
131
+ value3=reader.read_int32(),
132
+ value4=reader.read_int32(),
133
+ )
@@ -0,0 +1,101 @@
1
+ """
2
+ Property-Based Struct.
3
+
4
+ This struct type contains a list of properties rather than a fixed binary format.
5
+ It's used for complex game objects and custom data structures.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typing as t
11
+ from dataclasses import dataclass, field
12
+
13
+ from .base import Struct
14
+
15
+ if t.TYPE_CHECKING:
16
+ from ..common.binary_reader import BinaryReader
17
+ from ..properties.base import Property
18
+
19
+
20
+ @dataclass
21
+ class StructPropertyList(Struct):
22
+ """
23
+ A struct that contains a list of properties.
24
+
25
+ This is the fallback for any struct type that isn't a known native type.
26
+ The properties are read until a "None" terminator is encountered.
27
+ """
28
+
29
+ _struct_type: str = "PropertyList"
30
+ properties: list[Property] = field(default_factory=list)
31
+
32
+ @property
33
+ def struct_type(self) -> str:
34
+ return self._struct_type
35
+
36
+ @property
37
+ def is_native(self) -> bool:
38
+ return False
39
+
40
+ def to_dict(self) -> dict[str, t.Any]:
41
+ """Convert properties to a dictionary."""
42
+ result: dict[str, t.Any] = {}
43
+ for prop in self.properties:
44
+ if prop.name in result:
45
+ # Handle duplicate property names (array-like)
46
+ existing = result[prop.name]
47
+ if isinstance(existing, list):
48
+ existing.append(prop.value)
49
+ else:
50
+ result[prop.name] = [existing, prop.value]
51
+ else:
52
+ result[prop.name] = prop.value
53
+ return result
54
+
55
+ def get_property(self, name: str, index: int = 0) -> Property | None:
56
+ """Get a property by name and optional index."""
57
+ for prop in self.properties:
58
+ if prop.name == name and prop.index == index:
59
+ return prop
60
+ return None
61
+
62
+ def get_value(self, name: str, default: t.Any = None, index: int = 0) -> t.Any:
63
+ """Get a property value by name."""
64
+ prop = self.get_property(name, index)
65
+ return prop.value if prop else default
66
+
67
+ @classmethod
68
+ def read(
69
+ cls,
70
+ reader: BinaryReader,
71
+ is_asa: bool = False,
72
+ struct_type: str = "PropertyList",
73
+ name_table: list[str] | None = None,
74
+ worldsave_format: bool = False,
75
+ ) -> StructPropertyList:
76
+ """
77
+ Read a property-based struct from the archive.
78
+
79
+ Note: This imports the registry at runtime to avoid circular imports.
80
+
81
+ Args:
82
+ reader: The binary reader.
83
+ is_asa: True for ASA format.
84
+ struct_type: The struct type name.
85
+ name_table: Optional name table for world saves (version 6+).
86
+ worldsave_format: True for ASA WorldSave SQLite object format.
87
+ """
88
+ # Import here to avoid circular dependency
89
+ from ..properties.registry import read_properties
90
+
91
+ # Note: For ASA, the extra_byte that precedes struct data is read by the
92
+ # calling code (StructProperty.read or ArrayProperty.read), not here.
93
+ # We just read the properties directly.
94
+
95
+ properties = read_properties(
96
+ reader,
97
+ is_asa,
98
+ name_table=name_table,
99
+ worldsave_format=worldsave_format,
100
+ )
101
+ return cls(_struct_type=struct_type, properties=properties)