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,329 @@
1
+ """
2
+ Property Base Classes.
3
+
4
+ Properties are the core data storage mechanism in ARK save files.
5
+ Each game object has a list of properties that store its state.
6
+
7
+ Property Format:
8
+ +--------+--------+--------+--------+
9
+ | Name | Name | Int32 | Int32 |
10
+ | name | type | size | index |
11
+ +--------+--------+--------+--------+
12
+
13
+ The property list is terminated by a property with name "None".
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import typing as t
19
+ from abc import ABC, abstractmethod
20
+ from dataclasses import dataclass
21
+
22
+ if t.TYPE_CHECKING:
23
+ from ..common.binary_reader import BinaryReader
24
+
25
+
26
+ @dataclass
27
+ class Property(ABC):
28
+ """
29
+ Base class for all ARK properties.
30
+
31
+ Properties store named, typed values on game objects.
32
+ Each property has:
33
+ - name: The property name (e.g., "Health", "TamedName")
34
+ - type_name: The property type (e.g., "FloatProperty", "StrProperty")
35
+ - index: Array index for properties with the same name
36
+ - value: The property value (type depends on property type)
37
+
38
+ Subclasses implement reading the value based on the type.
39
+ """
40
+
41
+ name: str
42
+ index: int = 0
43
+
44
+ @property
45
+ @abstractmethod
46
+ def type_name(self) -> str:
47
+ """The property type name (e.g., 'FloatProperty')."""
48
+ ...
49
+
50
+ @property
51
+ @abstractmethod
52
+ def value(self) -> t.Any:
53
+ """The property value."""
54
+ ...
55
+
56
+ def __repr__(self) -> str:
57
+ idx_str = f", index={self.index}" if self.index != 0 else ""
58
+ return f"{self.__class__.__name__}(name={self.name!r}{idx_str}, value={self.value!r})"
59
+
60
+ @classmethod
61
+ def read(
62
+ cls,
63
+ reader: BinaryReader,
64
+ header: PropertyHeader,
65
+ is_asa: bool = False,
66
+ **kwargs: t.Any,
67
+ ) -> Property:
68
+ """
69
+ Read a property value from binary data.
70
+
71
+ Subclasses must implement this to parse their specific value format.
72
+
73
+ Args:
74
+ reader: The binary reader positioned at the property value.
75
+ header: The property header (name, type, data_size, index).
76
+ is_asa: True for ASA format, False for ASE.
77
+ **kwargs: Additional keyword arguments (name_table, worldsave_format, etc.).
78
+
79
+ Returns:
80
+ The parsed property instance.
81
+ """
82
+ raise NotImplementedError(f"{cls.__name__} must implement read()")
83
+
84
+
85
+ @dataclass
86
+ class PropertyHeader:
87
+ """
88
+ Property header data read before the value.
89
+
90
+ This is used internally during parsing to pass header
91
+ information to property constructors.
92
+ """
93
+
94
+ name: str
95
+ type_name: str
96
+ data_size: int
97
+ index: int
98
+
99
+ def __repr__(self) -> str:
100
+ return f"PropertyHeader(name={self.name!r}, type={self.type_name!r}, size={self.data_size}, index={self.index})"
101
+
102
+
103
+ # Type alias for name table - can be either list (ASE) or dict (ASA)
104
+ NameTable = list[str] | dict[int, str] | None
105
+
106
+
107
+ def _read_name_from_list_table(reader: BinaryReader, name_table: list[str]) -> str:
108
+ """
109
+ Read a name using a list-based name table (ASE world saves).
110
+
111
+ Name table format:
112
+ - Int32 index: Index into name table (1-based)
113
+ - Int32 instance: Instance number (0 = no suffix, otherwise append _{instance-1})
114
+
115
+ Args:
116
+ reader: The binary reader.
117
+ name_table: The name table list (1-based indexing).
118
+
119
+ Returns:
120
+ The full name string with instance suffix if applicable.
121
+ """
122
+ index = reader.read_int32()
123
+
124
+ # Convert from 1-based to 0-based index
125
+ internal_index = index - 1
126
+
127
+ if internal_index < 0 or internal_index >= len(name_table):
128
+ # Invalid index - return placeholder
129
+ return f"__INVALID_NAME_INDEX_{index}__"
130
+
131
+ name = name_table[internal_index]
132
+
133
+ # Read instance number
134
+ instance = reader.read_int32()
135
+
136
+ # Instance 0 means no suffix, otherwise append _{instance-1}
137
+ if instance > 0:
138
+ return f"{name}_{instance - 1}"
139
+ return name
140
+
141
+
142
+ def _read_name_from_dict_table(reader: BinaryReader, name_table: dict[int, str]) -> str:
143
+ """
144
+ Read a name using a dict-based name table (ASA world saves).
145
+
146
+ ASA name table format:
147
+ - Int32 id: Hash key into name table dictionary
148
+ - Int32 instance: Instance number (0 = no suffix, otherwise append _{instance-1})
149
+
150
+ Args:
151
+ reader: The binary reader.
152
+ name_table: The name table dictionary (hash keys).
153
+
154
+ Returns:
155
+ The full name string with instance suffix if applicable.
156
+ """
157
+ name_id = reader.read_int32()
158
+
159
+ if name_id not in name_table:
160
+ # Unknown name - return placeholder
161
+ return f"__UNKNOWN_NAME_{name_id}__"
162
+
163
+ name = name_table[name_id]
164
+
165
+ # Read instance number
166
+ instance = reader.read_int32()
167
+
168
+ # Instance 0 means no suffix, otherwise append _{instance-1}
169
+ if instance > 0:
170
+ return f"{name}_{instance - 1}"
171
+ return name
172
+
173
+
174
+ def read_name(reader: BinaryReader, name_table: NameTable = None) -> str:
175
+ """
176
+ Read a name from the archive.
177
+
178
+ If a name table is provided, reads from the table.
179
+ Otherwise, reads a raw string.
180
+
181
+ Args:
182
+ reader: The binary reader.
183
+ name_table: Optional name table for world saves.
184
+ - list: ASE format (1-based index)
185
+ - dict: ASA format (hash key)
186
+ - None: Read raw string
187
+
188
+ Returns:
189
+ The name string.
190
+ """
191
+ if name_table is None:
192
+ return reader.read_string()
193
+ elif isinstance(name_table, dict):
194
+ return _read_name_from_dict_table(reader, name_table)
195
+ else:
196
+ return _read_name_from_list_table(reader, name_table)
197
+
198
+
199
+ def read_property_header(
200
+ reader: BinaryReader,
201
+ is_asa: bool = False,
202
+ name_table: NameTable = None,
203
+ worldsave_format: bool = False,
204
+ ) -> PropertyHeader | None:
205
+ """
206
+ Read a property header from the archive.
207
+
208
+ Returns None if the property name is "None" (end of property list).
209
+
210
+ ASE Property header format:
211
+ - Name: property name (string or name table index)
212
+ - Name: property type (string or name table index)
213
+ - Int32: data size (size of value in bytes)
214
+ - Int32: index (for array elements with same name)
215
+
216
+ ASA Property header format (CloudInventory, Profile, Tribe):
217
+ - Name: property name
218
+ - Name: property type
219
+ - Int32: data_size
220
+ - Int32: index
221
+
222
+ ASA WorldSave Property header format (SQLite objects):
223
+ - Name ID (Int32) + Name Instance (Int32)
224
+ - Type ID (Int32) + 8 zero bytes
225
+ - Int32: data_size
226
+ - Byte: terminator (usually 0)
227
+
228
+ Args:
229
+ reader: The binary reader positioned at the property header.
230
+ is_asa: True for ASA format, False for ASE.
231
+ name_table: Optional name table for world saves.
232
+ - list: ASE format (1-based index)
233
+ - dict: ASA format (hash key)
234
+ worldsave_format: True for ASA WorldSave SQLite object format.
235
+
236
+ Returns:
237
+ PropertyHeader or None if this is the terminator.
238
+ """
239
+ if worldsave_format:
240
+ return _read_worldsave_property_header(reader, name_table)
241
+
242
+ # Read property name
243
+ name = read_name(reader, name_table)
244
+
245
+ # Check for terminator
246
+ if name == "None" or name == "" or name is None:
247
+ return None
248
+
249
+ # Read property type
250
+ type_name = read_name(reader, name_table)
251
+
252
+ # Both ASE and ASA have data_size and index in the header
253
+ data_size = reader.read_int32()
254
+ index = reader.read_int32()
255
+
256
+ return PropertyHeader(
257
+ name=name,
258
+ type_name=type_name,
259
+ data_size=data_size,
260
+ index=index,
261
+ )
262
+
263
+
264
+ def _read_worldsave_property_header(
265
+ reader: BinaryReader,
266
+ name_table: dict[int, str] | None,
267
+ ) -> PropertyHeader | None:
268
+ """
269
+ Read a property header in ASA WorldSave format.
270
+
271
+ WorldSave property header format (from TypeScript docs):
272
+ Common header:
273
+ - Name ID (Int32): Hash key into name table
274
+ - Name Instance (Int32): Usually 0, for array indices use name_instance - 1
275
+ - Type ID (Int32): Hash key for type name
276
+ - Type Instance (Int32): Usually 0
277
+
278
+ After the common header, the format depends on the property type:
279
+ - Simple properties (Int, Float, etc.): 4 zeros + Length(4) + Terminator(1) + Value
280
+ - ArrayProperty: ArrayHeader(4) + ElementTypeID(4) + ... (complex structure)
281
+ - StructProperty: similar complex structure
282
+
283
+ Args:
284
+ reader: The binary reader.
285
+ name_table: The name table dictionary (hash keys to names).
286
+
287
+ Returns:
288
+ PropertyHeader or None if this is the terminator.
289
+ """
290
+ if name_table is None:
291
+ raise ValueError("WorldSave format requires a name table")
292
+
293
+ # Read name: ID (4) + Instance (4)
294
+ name_id = reader.read_int32()
295
+ name_instance = reader.read_int32()
296
+
297
+ # Lookup name
298
+ name = name_table.get(name_id, f"__UNKNOWN_NAME_{name_id}__")
299
+
300
+ # Apply instance suffix (for array-like properties with same name)
301
+ index = 0
302
+ if name_instance > 0:
303
+ index = name_instance - 1
304
+ # Don't append to name - use index field instead
305
+
306
+ # Check for terminator
307
+ if name == "None":
308
+ return None
309
+
310
+ # Read type: ID (4) + Instance (4)
311
+ # Type instance is almost always 0 (padding), but must be read
312
+ type_id = reader.read_int32()
313
+ _type_instance = reader.read_int32() # Usually 0
314
+ type_name = name_table.get(type_id, f"__UNKNOWN_TYPE_{type_id}__")
315
+
316
+ # NOTE: The property-type-specific data (including data_size and terminator)
317
+ # is read by individual property readers. For the header, we return data_size=0
318
+ # and the actual readers will determine the size from their own format.
319
+ # This is because:
320
+ # - Simple properties: 4 zeros + Length(4) + Term(1) + Value
321
+ # - ArrayProperty: ArrayHeader + ElementTypeID + 8zeros + ByteLength + Term + Count
322
+ # - StructProperty: similar complex format
323
+
324
+ return PropertyHeader(
325
+ name=name,
326
+ type_name=type_name,
327
+ data_size=0, # Will be determined by property-specific reader
328
+ index=index,
329
+ )
@@ -0,0 +1,230 @@
1
+ """
2
+ Byte Property Type.
3
+
4
+ ByteProperty is special because it can represent either:
5
+ 1. A raw byte value (UInt8)
6
+ 2. An enum value (string name from an enum type)
7
+
8
+ The enum name is stored after the index, before the value.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import typing as t
14
+ from dataclasses import dataclass
15
+
16
+ from .base import Property, PropertyHeader, read_name
17
+
18
+ if t.TYPE_CHECKING:
19
+ from ..common.binary_reader import BinaryReader
20
+
21
+
22
+ @dataclass
23
+ class ByteProperty(Property):
24
+ """
25
+ Byte property - can be either a raw byte or an enum value.
26
+
27
+ Format after header:
28
+ - String enum_name (e.g., "None" for raw byte, or enum type name)
29
+ - ASA: 1 unknown byte
30
+ - If enum_name == "None": UInt8 value
31
+ - Else: String enum_value (the actual enum constant name)
32
+ """
33
+
34
+ name: str
35
+ index: int = 0
36
+ enum_name: str = "None" # "None" means raw byte, otherwise enum type name
37
+ _byte_value: int | None = None # Raw byte value (if enum_name == "None")
38
+ _enum_value: str | None = None # Enum constant name (if enum_name != "None")
39
+
40
+ @property
41
+ def type_name(self) -> str:
42
+ return "ByteProperty"
43
+
44
+ @property
45
+ def value(self) -> int | str:
46
+ """Returns byte value (int) or enum value (str)."""
47
+ if self._byte_value is not None:
48
+ return self._byte_value
49
+ return self._enum_value or ""
50
+
51
+ @property
52
+ def is_enum(self) -> bool:
53
+ """True if this is an enum value, False if raw byte."""
54
+ return self.enum_name != "None"
55
+
56
+ @property
57
+ def byte_value(self) -> int | None:
58
+ """The raw byte value, or None if this is an enum."""
59
+ return self._byte_value
60
+
61
+ @property
62
+ def enum_value(self) -> str | None:
63
+ """The enum constant name, or None if this is a raw byte."""
64
+ return self._enum_value
65
+
66
+ @classmethod
67
+ def read(
68
+ cls,
69
+ reader: BinaryReader,
70
+ header: PropertyHeader,
71
+ is_asa: bool = False,
72
+ name_table: list[str] | None = None,
73
+ worldsave_format: bool = False,
74
+ ) -> ByteProperty:
75
+ """
76
+ Read a ByteProperty from the archive.
77
+
78
+ WorldSave format: 8 zeros + length + array_index + byte_value
79
+ (Note: In WorldSave format, ByteProperty is always a raw byte)
80
+
81
+ ASE format:
82
+ - enum_name (name) - "None" for raw byte, or enum type name
83
+ - If enum_name == "None": UInt8 value
84
+ - Else: enum_value (name) - the enum constant
85
+
86
+ ASA format (string-based files like cloud inventory):
87
+ Header: name, type, data_size, index
88
+
89
+ For raw byte (data_size=0, index=1):
90
+ - enum_name (1 byte null = empty string)
91
+ - extra_byte (1)
92
+ - byte_value (1)
93
+
94
+ For raw byte (index=1 in header):
95
+ - extra_byte (1)
96
+ - If extra_byte & 0x01: array_index (int32)
97
+ - byte_value (1)
98
+
99
+ For enum (index > 1 = enum_name_length):
100
+ - enum_name string (using header.index as length, null-terminated)
101
+ - extra1 (int32)
102
+ - blueprint_path (string)
103
+ - zeros (int32)
104
+ - data_size (int32)
105
+ - extra_byte (1)
106
+ - If extra_byte & 0x01: array_index (int32)
107
+ - enum_value (string)
108
+ """
109
+ if worldsave_format:
110
+ # WorldSave ByteProperty has two formats, determined by the first int32
111
+ # after the 16-byte common header:
112
+ #
113
+ # Raw byte (marker == 0):
114
+ # marker(4) + data_size(4) + flag(1) + value(1) = 10 bytes
115
+ #
116
+ # Enum (marker == 1):
117
+ # marker(4) + enum_type_name(8) + marker2(4) + blueprint_name(8)
118
+ # + zeros(4) + data_size(4) + flag(1) + enum_value_name(8) = 41 bytes
119
+ marker = reader.read_int32()
120
+
121
+ if marker == 0:
122
+ # Raw byte format (same as simple prefix)
123
+ _data_size = reader.read_int32()
124
+ flag = reader.read_uint8()
125
+ # Simple prefix: if flag bit 0 is set, read array_index
126
+ if flag & 0x01:
127
+ _array_index = reader.read_int32()
128
+ byte_value = reader.read_uint8()
129
+ return cls(
130
+ name=header.name,
131
+ index=header.index,
132
+ enum_name="None",
133
+ _byte_value=byte_value,
134
+ )
135
+ else:
136
+ # Enum format (marker == 1): sub-header + enum value
137
+ if not (name_table and isinstance(name_table, dict)):
138
+ raise ValueError("ByteProperty enum format requires a name table")
139
+
140
+ enum_type_id = reader.read_int32()
141
+ _enum_type_inst = reader.read_int32()
142
+ enum_type_name = name_table.get(enum_type_id, f"__UNKNOWN_{enum_type_id}__")
143
+
144
+ _marker2 = reader.read_int32() # Usually 1
145
+ _blueprint_id = reader.read_int32()
146
+ _blueprint_inst = reader.read_int32()
147
+ _zeros = reader.read_int32()
148
+ _data_size = reader.read_int32()
149
+ _flag = reader.read_uint8()
150
+
151
+ enum_value_id = reader.read_int32()
152
+ enum_value_inst = reader.read_int32()
153
+ enum_value = name_table.get(enum_value_id, f"__UNKNOWN_{enum_value_id}__")
154
+ if enum_value_inst > 0:
155
+ enum_value = f"{enum_value}_{enum_value_inst - 1}"
156
+
157
+ return cls(
158
+ name=header.name,
159
+ index=header.index,
160
+ enum_name=enum_type_name,
161
+ _enum_value=enum_value,
162
+ )
163
+ elif is_asa:
164
+ # For ASA, header.index indicates the type:
165
+ # - index == 1: raw byte (no enum_name)
166
+ # - index > 1: enum type name length (null-terminated string)
167
+ enum_name_len = header.index
168
+
169
+ if enum_name_len == 1:
170
+ # Raw byte value - no enum_name at all
171
+ # Format: extra_byte + optional array_index + byte_value
172
+ extra_byte = reader.read_uint8()
173
+ array_index = 0
174
+ if extra_byte & 0x01:
175
+ array_index = reader.read_int32()
176
+ byte_value = reader.read_uint8()
177
+ return cls(
178
+ name=header.name,
179
+ index=array_index,
180
+ enum_name="None",
181
+ _byte_value=byte_value,
182
+ )
183
+ else:
184
+ # Enum value - has enum_name string followed by more data
185
+ enum_name_bytes = reader.read_bytes(enum_name_len)
186
+ enum_name = enum_name_bytes[:-1].decode("latin-1")
187
+
188
+ # extra1 (int32)
189
+ _extra1 = reader.read_int32()
190
+ # blueprint_path (string)
191
+ _blueprint_path = reader.read_string()
192
+ # zeros (int32)
193
+ _zeros = reader.read_int32()
194
+ # data_size (int32)
195
+ _data_size = reader.read_int32()
196
+ # extra_byte (1)
197
+ extra_byte = reader.read_uint8()
198
+ array_index = 0
199
+ if extra_byte & 0x01:
200
+ array_index = reader.read_int32()
201
+ # enum_value (string)
202
+ enum_value = reader.read_string()
203
+ return cls(
204
+ name=header.name,
205
+ index=array_index,
206
+ enum_name=enum_name,
207
+ _enum_value=enum_value,
208
+ )
209
+ else:
210
+ # ASE format with enum_name (uses name table if present)
211
+ enum_name = read_name(reader, name_table)
212
+
213
+ if enum_name == "None":
214
+ # Raw byte value
215
+ byte_value = reader.read_uint8()
216
+ return cls(
217
+ name=header.name,
218
+ index=header.index,
219
+ enum_name=enum_name,
220
+ _byte_value=byte_value,
221
+ )
222
+ else:
223
+ # Enum value - read the enum constant name (also uses name table)
224
+ enum_value = read_name(reader, name_table)
225
+ return cls(
226
+ name=header.name,
227
+ index=header.index,
228
+ enum_name=enum_name,
229
+ _enum_value=enum_value,
230
+ )