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,32 @@
1
+ """
2
+ ARK Game Objects.
3
+
4
+ This module provides classes for representing game objects from ARK save files.
5
+
6
+ Game objects are the fundamental entities - creatures, items, structures, players,
7
+ and other game entities.
8
+
9
+ Usage:
10
+ from arkparser.game_objects import GameObject, GameObjectContainer, LocationData
11
+
12
+ # Read objects from binary
13
+ objects = read_object_list(reader, is_asa=False)
14
+
15
+ # Create a container for lookup
16
+ container = GameObjectContainer(objects=objects)
17
+ container.build_relationships()
18
+
19
+ # Find creatures
20
+ creatures = container.get_creatures()
21
+ """
22
+
23
+ from .container import GameObjectContainer
24
+ from .game_object import GameObject, read_object_list
25
+ from .location import LocationData
26
+
27
+ __all__ = [
28
+ "LocationData",
29
+ "GameObject",
30
+ "GameObjectContainer",
31
+ "read_object_list",
32
+ ]
@@ -0,0 +1,180 @@
1
+ """
2
+ Game Object Container.
3
+
4
+ A container for game objects that provides lookup and relationship management.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import typing as t
10
+ from dataclasses import dataclass, field
11
+
12
+ from .game_object import GameObject
13
+
14
+ if t.TYPE_CHECKING:
15
+ from ..common.binary_reader import BinaryReader
16
+
17
+
18
+ @dataclass
19
+ class GameObjectContainer:
20
+ """
21
+ Container for a collection of game objects.
22
+
23
+ Provides methods for looking up objects by ID, class name, or name,
24
+ and manages parent/component relationships.
25
+ """
26
+
27
+ objects: list[GameObject] = field(default_factory=list)
28
+
29
+ # Lookup caches (built on demand)
30
+ _by_guid: dict[str, GameObject] = field(default_factory=dict, repr=False)
31
+ _by_class: dict[str, list[GameObject]] = field(default_factory=dict, repr=False)
32
+ _by_name: dict[str, GameObject] = field(default_factory=dict, repr=False)
33
+
34
+ def __len__(self) -> int:
35
+ return len(self.objects)
36
+
37
+ def __iter__(self) -> t.Iterator[GameObject]:
38
+ return iter(self.objects)
39
+
40
+ def __getitem__(self, index: int) -> GameObject:
41
+ return self.objects[index]
42
+
43
+ def add(self, obj: GameObject) -> None:
44
+ """Add an object to the container."""
45
+ self.objects.append(obj)
46
+ self._invalidate_caches()
47
+
48
+ def _invalidate_caches(self) -> None:
49
+ """Clear lookup caches."""
50
+ self._by_guid.clear()
51
+ self._by_class.clear()
52
+ self._by_name.clear()
53
+
54
+ def _build_caches(self) -> None:
55
+ """Build lookup caches if empty."""
56
+ if not self._by_guid and self.objects:
57
+ for obj in self.objects:
58
+ if obj.guid:
59
+ self._by_guid[obj.guid] = obj
60
+ if obj.class_name:
61
+ if obj.class_name not in self._by_class:
62
+ self._by_class[obj.class_name] = []
63
+ self._by_class[obj.class_name].append(obj)
64
+ if obj.primary_name:
65
+ self._by_name[obj.primary_name] = obj
66
+
67
+ def get_by_id(self, obj_id: int) -> GameObject | None:
68
+ """Get object by ID."""
69
+ if 0 <= obj_id < len(self.objects):
70
+ return self.objects[obj_id]
71
+ return None
72
+
73
+ def get_by_guid(self, guid: str) -> GameObject | None:
74
+ """Get object by GUID."""
75
+ self._build_caches()
76
+ return self._by_guid.get(guid)
77
+
78
+ def get_by_name(self, name: str) -> GameObject | None:
79
+ """Get object by primary name."""
80
+ self._build_caches()
81
+ return self._by_name.get(name)
82
+
83
+ def get_by_class(self, class_name: str) -> list[GameObject]:
84
+ """Get all objects with the given class name."""
85
+ self._build_caches()
86
+ return self._by_class.get(class_name, [])
87
+
88
+ def find_by_class_pattern(self, pattern: str) -> list[GameObject]:
89
+ """Find objects whose class name contains the pattern."""
90
+ return [obj for obj in self.objects if pattern in obj.class_name]
91
+
92
+ def build_relationships(self) -> None:
93
+ """
94
+ Build parent/component relationships between objects.
95
+
96
+ Components have multiple names - the last name references the parent.
97
+ """
98
+ self._build_caches()
99
+
100
+ for obj in self.objects:
101
+ if obj.has_parent_names:
102
+ # This is a component - find its parent
103
+ parent_name = obj.names[-1] # Last name is parent reference
104
+ parent = self.get_by_name(parent_name)
105
+ if parent:
106
+ parent.add_component(obj)
107
+
108
+ # Class name patterns that are never structures
109
+ _NON_STRUCTURE_PATTERNS: t.ClassVar[tuple[str, ...]] = (
110
+ "_Character_BP",
111
+ "DinoCharacter",
112
+ "PlayerPawn",
113
+ "Buff_",
114
+ "PrimalBuff",
115
+ "Weap",
116
+ "StatusComponent",
117
+ "Inventory",
118
+ "DroppedItem",
119
+ "DeathItemCache",
120
+ "NPCZone",
121
+ "DinoDropInventory",
122
+ )
123
+
124
+ def get_creatures(self) -> list[GameObject]:
125
+ """Get all creature objects (tamed and wild)."""
126
+ return [
127
+ obj for obj in self.objects
128
+ if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name
129
+ ]
130
+
131
+ def get_items(self) -> list[GameObject]:
132
+ """Get all item objects."""
133
+ return [obj for obj in self.objects if obj.is_item]
134
+
135
+ def get_structures(self) -> list[GameObject]:
136
+ """Get all tribe-owned placed structures."""
137
+ results: list[GameObject] = []
138
+ for obj in self.objects:
139
+ cn = obj.class_name
140
+ if obj.get_property_value("TargetingTeam") is None:
141
+ continue
142
+ if obj.get_property_value("DinoID1") is not None:
143
+ continue
144
+ if any(pat in cn for pat in self._NON_STRUCTURE_PATTERNS):
145
+ continue
146
+ results.append(obj)
147
+ return results
148
+
149
+ def get_player_pawns(self) -> list[GameObject]:
150
+ """Get player character objects on the map."""
151
+ return [obj for obj in self.objects if "PlayerPawn" in obj.class_name]
152
+
153
+ def get_players(self) -> list[GameObject]:
154
+ """Get all player data objects."""
155
+ return self.get_by_class("PrimalPlayerData") + self.find_by_class_pattern("PlayerPawnTest")
156
+
157
+ def load_all_properties(
158
+ self,
159
+ reader: BinaryReader,
160
+ properties_block_offset: int,
161
+ is_asa: bool = False,
162
+ ) -> None:
163
+ """
164
+ Load properties for all objects.
165
+
166
+ Args:
167
+ reader: Binary reader.
168
+ properties_block_offset: Base offset of properties block.
169
+ is_asa: True for ASA format.
170
+ """
171
+ for i, obj in enumerate(self.objects):
172
+ next_obj = self.objects[i + 1] if i + 1 < len(self.objects) else None
173
+ obj.load_properties(reader, properties_block_offset, is_asa, next_obj)
174
+
175
+ def to_dict(self) -> dict[str, t.Any]:
176
+ """Convert to dictionary for serialization."""
177
+ return {
178
+ "count": len(self.objects),
179
+ "objects": [obj.to_dict() for obj in self.objects],
180
+ }
@@ -0,0 +1,273 @@
1
+ """
2
+ Game Object.
3
+
4
+ The core entity class for all objects in ARK save files.
5
+ Game objects represent creatures, items, structures, players, and other entities.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typing as t
11
+ from dataclasses import dataclass, field
12
+
13
+ from .location import LocationData
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 GameObject:
22
+ """
23
+ A game object from an ARK save file.
24
+
25
+ Game objects are the fundamental entities in ARK saves. They represent
26
+ creatures, items, structures, players, and other game entities.
27
+
28
+ Attributes:
29
+ id: Object index/ID within the save file.
30
+ guid: 16-byte GUID (ASA only, empty string for ASE).
31
+ class_name: The UE4 class name (e.g., "Dodo_Character_BP_C").
32
+ is_item: True if this is an item/blueprint/engram.
33
+ names: List of ArkNames (usually 1 for actors, 2 for components).
34
+ from_data_file: True if loaded from external data file.
35
+ data_file_index: Index of the external data file.
36
+ location: Position and rotation data (if has_location is True).
37
+ properties_offset: Offset to property data in the save file.
38
+ properties: List of parsed properties.
39
+ extra_data: Additional data after properties (type depends on structGuid).
40
+ """
41
+
42
+ id: int = 0
43
+ guid: str = ""
44
+ class_name: str = ""
45
+ is_item: bool = False
46
+ names: list[str] = field(default_factory=list)
47
+ from_data_file: bool = False
48
+ data_file_index: int = 0
49
+ location: LocationData | None = None
50
+ properties_offset: int = 0
51
+ properties: list[Property] = field(default_factory=list)
52
+ extra_data: bytes | None = None
53
+
54
+ # Parent/component relationships (set after loading)
55
+ parent: GameObject | None = field(default=None, repr=False)
56
+ components: dict[str, GameObject] = field(default_factory=dict, repr=False)
57
+
58
+ @property
59
+ def has_location(self) -> bool:
60
+ """True if this object has location data."""
61
+ return self.location is not None
62
+
63
+ @property
64
+ def primary_name(self) -> str | None:
65
+ """The primary name of this object (first in names list)."""
66
+ return self.names[0] if self.names else None
67
+
68
+ @property
69
+ def parent_names(self) -> list[str]:
70
+ """Parent names (all names after the first)."""
71
+ return self.names[1:] if len(self.names) > 1 else []
72
+
73
+ @property
74
+ def has_parent_names(self) -> bool:
75
+ """True if this object has parent names (component)."""
76
+ return len(self.names) > 1
77
+
78
+ def get_property(self, name: str, index: int | None = None) -> Property | None:
79
+ """
80
+ Get a property by name and optional index.
81
+
82
+ Args:
83
+ name: Property name to find.
84
+ index: Property index (for arrays with same name). If None, returns first match.
85
+
86
+ Returns:
87
+ The Property object, or None if not found.
88
+ """
89
+ for prop in self.properties:
90
+ if prop.name == name:
91
+ if index is None or prop.index == index:
92
+ return prop
93
+ return None
94
+
95
+ def get_property_value(self, name: str, default: t.Any = None, index: int | None = None) -> t.Any:
96
+ """
97
+ Get a property value by name.
98
+
99
+ Args:
100
+ name: Property name to find.
101
+ default: Default value if property not found.
102
+ index: Property index (for arrays with same name). If None, returns first match.
103
+
104
+ Returns:
105
+ The property value, or the default if not found.
106
+ """
107
+ prop = self.get_property(name, index)
108
+ return prop.value if prop is not None else default
109
+
110
+ def get_properties_by_name(self, name: str) -> list[Property]:
111
+ """Get all properties with the given name (any index)."""
112
+ return [p for p in self.properties if p.name == name]
113
+
114
+ def has_property(self, name: str) -> bool:
115
+ """Check if this object has a property with the given name."""
116
+ return any(p.name == name for p in self.properties)
117
+
118
+ def to_dict(self) -> dict[str, t.Any]:
119
+ """Convert to dictionary for serialization."""
120
+ result: dict[str, t.Any] = {
121
+ "id": self.id,
122
+ "class_name": self.class_name,
123
+ }
124
+
125
+ if self.guid:
126
+ result["guid"] = self.guid
127
+ if self.is_item:
128
+ result["is_item"] = True
129
+ if self.names:
130
+ result["names"] = self.names
131
+ if self.from_data_file:
132
+ result["from_data_file"] = True
133
+ result["data_file_index"] = self.data_file_index
134
+ if self.location:
135
+ result["location"] = self.location.to_dict()
136
+
137
+ # Convert properties to dict
138
+ props: dict[str, t.Any] = {}
139
+ for prop in self.properties:
140
+ if prop.name in props:
141
+ existing = props[prop.name]
142
+ if isinstance(existing, list):
143
+ existing.append(prop.value)
144
+ else:
145
+ props[prop.name] = [existing, prop.value]
146
+ else:
147
+ props[prop.name] = prop.value
148
+ if props:
149
+ result["properties"] = props
150
+
151
+ return result
152
+
153
+ @classmethod
154
+ def read_header(
155
+ cls,
156
+ reader: BinaryReader,
157
+ obj_id: int,
158
+ is_asa: bool = False,
159
+ ) -> GameObject:
160
+ """
161
+ Read the object header (not properties).
162
+
163
+ Properties are loaded separately via load_properties().
164
+
165
+ Args:
166
+ reader: Binary reader positioned at object header.
167
+ obj_id: Object ID/index.
168
+ is_asa: True for ASA format.
169
+
170
+ Returns:
171
+ GameObject with header data populated.
172
+ """
173
+ obj = cls(id=obj_id)
174
+
175
+ # Read GUID (16 bytes) - always present, but all zeros in ASE
176
+ guid = reader.read_guid()
177
+ obj.guid = str(guid) if any(b != 0 for b in guid.bytes) else ""
178
+
179
+ # Read class name
180
+ obj.class_name = reader.read_string()
181
+
182
+ # Is item flag (UInt32 bool)
183
+ obj.is_item = reader.read_uint32() != 0
184
+
185
+ # Read names
186
+ name_count = reader.read_int32()
187
+ obj.names = [reader.read_string() for _ in range(name_count)]
188
+
189
+ # From data file flag
190
+ obj.from_data_file = reader.read_uint32() != 0
191
+ obj.data_file_index = reader.read_int32()
192
+
193
+ # Location data
194
+ has_location = reader.read_uint32() != 0
195
+ if has_location:
196
+ obj.location = LocationData.read(reader, is_asa)
197
+
198
+ # Properties offset
199
+ obj.properties_offset = reader.read_int32()
200
+
201
+ # Unknown int (should be 0)
202
+ _unknown = reader.read_int32()
203
+
204
+ return obj
205
+
206
+ def load_properties(
207
+ self,
208
+ reader: BinaryReader,
209
+ properties_block_offset: int,
210
+ is_asa: bool = False,
211
+ next_object: GameObject | None = None,
212
+ name_table: list[str] | None = None,
213
+ ) -> None:
214
+ """
215
+ Load properties for this object.
216
+
217
+ Args:
218
+ reader: Binary reader.
219
+ properties_block_offset: Base offset of the properties block.
220
+ is_asa: True for ASA format.
221
+ next_object: Next object (to determine property block end).
222
+ name_table: Optional name table for world saves (version 6+).
223
+ """
224
+ from ..properties.registry import read_properties
225
+
226
+ # Calculate absolute offset
227
+ offset = properties_block_offset + self.properties_offset
228
+
229
+ # Seek to properties
230
+ reader.position = offset
231
+
232
+ # Read properties
233
+ self.properties = read_properties(reader, is_asa, name_table=name_table)
234
+
235
+ # Any remaining data before next object is extra data
236
+ if next_object is not None:
237
+ next_offset = properties_block_offset + next_object.properties_offset
238
+ remaining = next_offset - reader.position
239
+ if remaining > 0:
240
+ self.extra_data = reader.read_bytes(remaining)
241
+
242
+ def add_component(self, component: GameObject) -> None:
243
+ """Add a component to this object."""
244
+ if component.primary_name:
245
+ self.components[component.primary_name] = component
246
+ component.parent = self
247
+
248
+
249
+ def read_object_list(
250
+ reader: BinaryReader,
251
+ is_asa: bool = False,
252
+ ) -> list[GameObject]:
253
+ """
254
+ Read a list of game objects from the archive.
255
+
256
+ This reads only the object headers. Properties must be loaded
257
+ separately using load_properties().
258
+
259
+ Args:
260
+ reader: Binary reader positioned at object count.
261
+ is_asa: True for ASA format.
262
+
263
+ Returns:
264
+ List of GameObject instances with headers populated.
265
+ """
266
+ count = reader.read_int32()
267
+ objects: list[GameObject] = []
268
+
269
+ for i in range(count):
270
+ obj = GameObject.read_header(reader, i, is_asa)
271
+ objects.append(obj)
272
+
273
+ return objects
@@ -0,0 +1,87 @@
1
+ """
2
+ Location Data.
3
+
4
+ Position and rotation data for game objects.
5
+ ASE uses floats (4 bytes each), ASA uses doubles (8 bytes each).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import typing as t
11
+ from dataclasses import dataclass
12
+
13
+ if t.TYPE_CHECKING:
14
+ from ..common.binary_reader import BinaryReader
15
+
16
+
17
+ @dataclass
18
+ class LocationData:
19
+ """
20
+ Position and rotation data for a game object.
21
+
22
+ Contains 6 values: x, y, z position and pitch, yaw, roll rotation.
23
+ """
24
+
25
+ x: float = 0.0
26
+ y: float = 0.0
27
+ z: float = 0.0
28
+ pitch: float = 0.0
29
+ yaw: float = 0.0
30
+ roll: float = 0.0
31
+
32
+ @property
33
+ def position(self) -> tuple[float, float, float]:
34
+ """Get position as (x, y, z) tuple."""
35
+ return (self.x, self.y, self.z)
36
+
37
+ @property
38
+ def rotation(self) -> tuple[float, float, float]:
39
+ """Get rotation as (pitch, yaw, roll) tuple."""
40
+ return (self.pitch, self.yaw, self.roll)
41
+
42
+ def to_dict(self) -> dict[str, float]:
43
+ """Convert to dictionary."""
44
+ return {
45
+ "x": self.x,
46
+ "y": self.y,
47
+ "z": self.z,
48
+ "pitch": self.pitch,
49
+ "yaw": self.yaw,
50
+ "roll": self.roll,
51
+ }
52
+
53
+ @classmethod
54
+ def read(cls, reader: BinaryReader, is_asa: bool = False) -> LocationData:
55
+ """
56
+ Read location data from the archive.
57
+
58
+ Args:
59
+ reader: Binary reader positioned at location data.
60
+ is_asa: True for ASA format (doubles), False for ASE (floats).
61
+
62
+ Returns:
63
+ LocationData instance.
64
+ """
65
+ if is_asa:
66
+ return cls(
67
+ x=reader.read_double(),
68
+ y=reader.read_double(),
69
+ z=reader.read_double(),
70
+ pitch=reader.read_double(),
71
+ yaw=reader.read_double(),
72
+ roll=reader.read_double(),
73
+ )
74
+ else:
75
+ return cls(
76
+ x=reader.read_float(),
77
+ y=reader.read_float(),
78
+ z=reader.read_float(),
79
+ pitch=reader.read_float(),
80
+ yaw=reader.read_float(),
81
+ roll=reader.read_float(),
82
+ )
83
+
84
+ @classmethod
85
+ def size(cls, is_asa: bool = False) -> int:
86
+ """Get the byte size of location data."""
87
+ return 48 if is_asa else 24 # 6 doubles vs 6 floats
@@ -0,0 +1,29 @@
1
+ """
2
+ ARK save parser game object models.
3
+
4
+ Provides higher-level model classes wrapping raw GameObjects
5
+ with intuitive property access for common ARK save data.
6
+ """
7
+
8
+ from arkparser.models.character import Character
9
+ from arkparser.models.creature import Creature, TamedCreature, WildCreature
10
+ from arkparser.models.item import Item
11
+ from arkparser.models.player import Player
12
+ from arkparser.models.stats import CreatureStats, Location
13
+ from arkparser.models.structure import Structure
14
+ from arkparser.models.tribe import Tribe, TribeLogEntry, TribeMember
15
+
16
+ __all__ = [
17
+ "CreatureStats",
18
+ "Location",
19
+ "Creature",
20
+ "TamedCreature",
21
+ "WildCreature",
22
+ "Item",
23
+ "Player",
24
+ "Tribe",
25
+ "TribeMember",
26
+ "TribeLogEntry",
27
+ "Structure",
28
+ "Character",
29
+ ]