arkparser 0.1.0__tar.gz → 0.1.2__tar.gz

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 (57) hide show
  1. {arkparser-0.1.0 → arkparser-0.1.2}/PKG-INFO +9 -1
  2. {arkparser-0.1.0 → arkparser-0.1.2}/README.md +8 -0
  3. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/cloud_inventory.py +29 -13
  4. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/world_save.py +92 -24
  5. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/container.py +59 -4
  6. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/PKG-INFO +9 -1
  7. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/SOURCES.txt +2 -1
  8. {arkparser-0.1.0 → arkparser-0.1.2}/pyproject.toml +1 -1
  9. {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_cloud_inventory.py +88 -1
  10. arkparser-0.1.2/tests/test_world_save.py +252 -0
  11. {arkparser-0.1.0 → arkparser-0.1.2}/LICENSE +0 -0
  12. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/__init__.py +0 -0
  13. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/__init__.py +0 -0
  14. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/binary_reader.py +0 -0
  15. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/exceptions.py +0 -0
  16. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/map_config.py +0 -0
  17. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/types.py +0 -0
  18. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/version_detection.py +0 -0
  19. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/data_models.py +0 -0
  20. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/export.py +0 -0
  21. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/__init__.py +0 -0
  22. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/base.py +0 -0
  23. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/profile.py +0 -0
  24. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/tribe.py +0 -0
  25. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/__init__.py +0 -0
  26. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/game_object.py +0 -0
  27. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/location.py +0 -0
  28. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/__init__.py +0 -0
  29. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/character.py +0 -0
  30. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/creature.py +0 -0
  31. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/item.py +0 -0
  32. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/player.py +0 -0
  33. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/stats.py +0 -0
  34. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/structure.py +0 -0
  35. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/tribe.py +0 -0
  36. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/__init__.py +0 -0
  37. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/base.py +0 -0
  38. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/byte_property.py +0 -0
  39. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/compound.py +0 -0
  40. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/primitives.py +0 -0
  41. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/registry.py +0 -0
  42. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/py.typed +0 -0
  43. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/__init__.py +0 -0
  44. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/base.py +0 -0
  45. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/colors.py +0 -0
  46. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/misc.py +0 -0
  47. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/property_list.py +0 -0
  48. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/registry.py +0 -0
  49. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/vectors.py +0 -0
  50. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/dependency_links.txt +0 -0
  51. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/requires.txt +0 -0
  52. {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/top_level.txt +0 -0
  53. {arkparser-0.1.0 → arkparser-0.1.2}/setup.cfg +0 -0
  54. {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_models.py +0 -0
  55. {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_profile.py +0 -0
  56. {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_tribe.py +0 -0
  57. {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_version_detection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Pure Python parser for ARK: Survival Evolved and ARK: Survival Ascended save files
5
5
  Author: Vertyco
6
6
  License-Expression: MIT
@@ -338,6 +338,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
338
338
  | `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
339
339
  | `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
340
340
  | `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
341
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
342
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
343
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
344
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
341
345
  | `get_items()` | `list[GameObject]` | Item objects |
342
346
  | `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
343
347
  | `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
@@ -390,6 +394,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
390
394
  | `get_creatures()` | `list[GameObject]` | All creatures |
391
395
  | `get_structures()` | `list[GameObject]` | Tribe-owned structures |
392
396
  | `get_player_pawns()` | `list[GameObject]` | Player characters on map |
397
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
398
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates |
399
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
400
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
393
401
  | `get_players()` | `list[GameObject]` | Player data objects |
394
402
  | `get_items()` | `list[GameObject]` | Item objects |
395
403
 
@@ -304,6 +304,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
304
304
  | `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
305
305
  | `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
306
306
  | `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
307
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
308
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
309
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
310
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
307
311
  | `get_items()` | `list[GameObject]` | Item objects |
308
312
  | `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
309
313
  | `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
@@ -356,6 +360,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
356
360
  | `get_creatures()` | `list[GameObject]` | All creatures |
357
361
  | `get_structures()` | `list[GameObject]` | Tribe-owned structures |
358
362
  | `get_player_pawns()` | `list[GameObject]` | Player characters on map |
363
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
364
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates |
365
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
366
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
359
367
  | `get_players()` | `list[GameObject]` | Player data objects |
360
368
  | `get_items()` | `list[GameObject]` | Item objects |
361
369
 
@@ -56,13 +56,19 @@ class CloudInventory(ArkFile):
56
56
  - Object headers (ASE format)
57
57
  - Properties
58
58
 
59
- ASA format:
60
- - Int32 version
61
- - Int32 unknown1 (extra field)
62
- - Int32 unknown2 (extra field)
59
+ ASA v7 format:
60
+ - Int32 version (7)
61
+ - Int32 unknown1 (extra field, v7+ only)
62
+ - Int32 unknown2 (extra field, v7+ only)
63
63
  - Int32 object_count
64
64
  - Object headers (ASA format with GUIDs)
65
65
  - Properties
66
+
67
+ ASA v6 format (solo-cluster / cross-ARK transfer files):
68
+ - Int32 version (6)
69
+ - Int32 object_count
70
+ - Object headers (ASA format with GUIDs — no extra header fields)
71
+ - Properties
66
72
  """
67
73
  # Read version
68
74
  version = reader.read_int32()
@@ -73,8 +79,8 @@ class CloudInventory(ArkFile):
73
79
  # Detect ASE vs ASA
74
80
  is_asa = cls._detect_asa(reader, version)
75
81
 
76
- if is_asa:
77
- # ASA has extra header fields
82
+ if is_asa and version >= 7:
83
+ # v7+ ASA has two extra header fields before object_count
78
84
  _unknown1 = reader.read_int32()
79
85
  _unknown2 = reader.read_int32()
80
86
 
@@ -88,17 +94,23 @@ class CloudInventory(ArkFile):
88
94
  objects: list[GameObject] = []
89
95
  for i in range(object_count):
90
96
  if is_asa:
91
- obj = cls._read_asa_object_header(reader, obj_id=i)
97
+ obj = cls._read_asa_object_header(reader, obj_id=i, version=version)
92
98
  else:
93
99
  obj = GameObject.read_header(reader, obj_id=i, is_asa=False)
94
100
  objects.append(obj)
95
101
 
96
- # Load properties for each object
102
+ # Load properties for each object.
103
+ # Version 6 ASA (cross-ARK / solecluster) uses ASA-style object headers
104
+ # but ASE-style (is_asa=False) properties. Only v7+ uses ASA properties.
105
+ properties_is_asa = version >= 7
97
106
  properties_block_offset = 0
98
107
  for i, obj in enumerate(objects):
99
108
  next_obj = objects[i + 1] if i + 1 < len(objects) else None
100
109
  obj.load_properties(
101
- reader, properties_block_offset=properties_block_offset, is_asa=is_asa, next_object=next_obj
110
+ reader,
111
+ properties_block_offset=properties_block_offset,
112
+ is_asa=properties_is_asa,
113
+ next_object=next_obj,
102
114
  )
103
115
 
104
116
  # Build container with lookups
@@ -113,7 +125,7 @@ class CloudInventory(ArkFile):
113
125
  )
114
126
 
115
127
  @classmethod
116
- def _read_asa_object_header(cls, reader: BinaryReader, obj_id: int) -> GameObject:
128
+ def _read_asa_object_header(cls, reader: BinaryReader, obj_id: int, version: int = 7) -> GameObject:
117
129
  """
118
130
  Read an ASA object header.
119
131
 
@@ -123,7 +135,10 @@ class CloudInventory(ArkFile):
123
135
  - Field1 (int32)
124
136
  - Field2 (int32)
125
137
  - Instance name (string)
126
- - Padding (21 bytes)
138
+ - Padding (21 bytes for v7+, 20 bytes for v6)
139
+
140
+ Version 6 (cross-ARK / solecluster files) uses a slightly older format
141
+ with one fewer padding byte before the properties block.
127
142
  """
128
143
  obj = GameObject(id=obj_id)
129
144
 
@@ -142,8 +157,9 @@ class CloudInventory(ArkFile):
142
157
  instance_name = reader.read_string()
143
158
  obj.names = [instance_name] if instance_name else []
144
159
 
145
- # Skip 21 bytes of padding
146
- reader.skip(21)
160
+ # v7+ uses 21 bytes of padding; v6 (cross-ARK / solecluster) uses 20
161
+ padding_size = 21 if version >= 7 else 20
162
+ reader.skip(padding_size)
147
163
 
148
164
  # Properties offset will be set by sequential reading
149
165
  obj.properties_offset = reader.position
@@ -221,43 +221,31 @@ class WorldSave:
221
221
  # Class-name patterns that are never structures even though they may
222
222
  # carry ``TargetingTeam``. Checked via ``any(pat in cn for pat ...)``.
223
223
  _NON_STRUCTURE_PATTERNS: t.ClassVar[tuple[str, ...]] = (
224
- "_Character_BP", # Creatures / tamed dinos
225
- "DinoCharacter", # Creature variants
226
- "PlayerPawn", # Player avatars on the map
227
- "Buff_", # Active buffs
228
- "PrimalBuff", # Persistent buff data
229
- "Weap", # Held weapons
224
+ "_Character_BP", # Creatures / tamed dinos
225
+ "DinoCharacter", # Creature variants
226
+ "PlayerPawn", # Player avatars on the map
227
+ "Buff_", # Active buffs
228
+ "PrimalBuff", # Persistent buff data
229
+ "Weap", # Held weapons
230
230
  "StatusComponent", # Character/dino status components
231
- "Inventory", # Inventory components
232
- "DroppedItem", # Dropped items
231
+ "Inventory", # Inventory components
232
+ "DroppedItem", # Dropped items
233
233
  "DeathItemCache", # Death caches
234
- "NPCZone", # NPC spawn zones
234
+ "NPCZone", # NPC spawn zones
235
235
  "DinoDropInventory", # Dino death drops
236
236
  )
237
237
 
238
238
  def get_creatures(self) -> list[GameObject]:
239
239
  """Return all creature objects (tamed **and** wild)."""
240
- return [
241
- obj
242
- for obj in self.objects
243
- if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name
244
- ]
240
+ return [obj for obj in self.objects if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name]
245
241
 
246
242
  def get_tamed_creatures(self) -> list[GameObject]:
247
243
  """Return tamed creatures (have ``TamingTeamID`` property)."""
248
- return [
249
- obj
250
- for obj in self.get_creatures()
251
- if obj.get_property_value("TamingTeamID") is not None
252
- ]
244
+ return [obj for obj in self.get_creatures() if obj.get_property_value("TamingTeamID") is not None]
253
245
 
254
246
  def get_wild_creatures(self) -> list[GameObject]:
255
247
  """Return wild creatures (no ``TamingTeamID`` property)."""
256
- return [
257
- obj
258
- for obj in self.get_creatures()
259
- if obj.get_property_value("TamingTeamID") is None
260
- ]
248
+ return [obj for obj in self.get_creatures() if obj.get_property_value("TamingTeamID") is None]
261
249
 
262
250
  def get_structures(self) -> list[GameObject]:
263
251
  """Return tribe-owned placed structures.
@@ -294,6 +282,86 @@ class WorldSave:
294
282
  """Return objects with ``is_item`` set."""
295
283
  return [obj for obj in self.objects if obj.is_item]
296
284
 
285
+ # ---- Map / engine-placed entities --------------------------------
286
+
287
+ def get_terminals(self) -> list[GameObject]:
288
+ """Return map-placed terminal objects.
289
+
290
+ Covers tribute terminals (obelisks on The Island, Ragnarok, etc.),
291
+ Extinction city terminals, and any variant using ``TributeTerminal``
292
+ or ``CityTerminal`` in the class name. These are engine-placed and
293
+ have **no** ``TargetingTeam``.
294
+
295
+ Inventory components and item sub-objects attached to terminals are
296
+ excluded — only the top-level structure actors are returned.
297
+ """
298
+ return [
299
+ obj
300
+ for obj in self.objects
301
+ if ("TributeTerminal" in obj.class_name or "CityTerminal" in obj.class_name)
302
+ and not obj.is_item
303
+ and "Inventory" not in obj.class_name
304
+ and "PrimalItem" not in obj.class_name
305
+ ]
306
+
307
+ def get_supply_drops(self) -> list[GameObject]:
308
+ """Return active supply-drop / loot-crate objects on the map.
309
+
310
+ Matches ``SupplyCrate``, ``OrbitalSupply``, and ``SupplyDrop``
311
+ class-name substrings. Inventory components are excluded.
312
+ """
313
+ _SUPPLY_PATTERNS = ("SupplyCrate", "OrbitalSupply", "SupplyDrop")
314
+ return [
315
+ obj
316
+ for obj in self.objects
317
+ if any(p in obj.class_name for p in _SUPPLY_PATTERNS)
318
+ and "Inventory" not in obj.class_name
319
+ and not obj.is_item
320
+ ]
321
+
322
+ def get_artifact_crates(self) -> list[GameObject]:
323
+ """Return artifact-crate spawn objects.
324
+
325
+ These are the map-placed crates that contain artifacts for boss
326
+ fights (e.g. ``ArtifactCrate_Desert_Kaiju_EX_C``).
327
+ Inventory components are excluded.
328
+ """
329
+ return [
330
+ obj
331
+ for obj in self.objects
332
+ if "ArtifactCrate" in obj.class_name and "Inventory" not in obj.class_name and not obj.is_item
333
+ ]
334
+
335
+ def get_map_resources(self) -> list[GameObject]:
336
+ """Return engine-placed resource / vein / node objects.
337
+
338
+ Covers map-specific harvestable resource objects:
339
+
340
+ * Oil veins (The Island, Ragnarok, …) — ``OilVein``
341
+ * Water veins (Scorched Earth, …) — ``WaterVein``
342
+ * Gas veins (Scorched Earth, …) — ``GasVein``
343
+ * Charge nodes (Aberration) — ``ChargeNode``
344
+ * Element veins (Extinction) — ``ElementVein``
345
+ * Beaver dams — ``BeaverDam``
346
+
347
+ Inventory components attached to these are excluded.
348
+ """
349
+ _RESOURCE_PATTERNS = (
350
+ "OilVein",
351
+ "WaterVein",
352
+ "GasVein",
353
+ "ChargeNode",
354
+ "ElementVein",
355
+ "BeaverDam",
356
+ )
357
+ return [
358
+ obj
359
+ for obj in self.objects
360
+ if any(p in obj.class_name for p in _RESOURCE_PATTERNS)
361
+ and "Inventory" not in obj.class_name
362
+ and not obj.is_item
363
+ ]
364
+
297
365
  # ------------------------------------------------------------------
298
366
  # Properties
299
367
  # ------------------------------------------------------------------
@@ -123,10 +123,7 @@ class GameObjectContainer:
123
123
 
124
124
  def get_creatures(self) -> list[GameObject]:
125
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
- ]
126
+ return [obj for obj in self.objects if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name]
130
127
 
131
128
  def get_items(self) -> list[GameObject]:
132
129
  """Get all item objects."""
@@ -150,6 +147,64 @@ class GameObjectContainer:
150
147
  """Get player character objects on the map."""
151
148
  return [obj for obj in self.objects if "PlayerPawn" in obj.class_name]
152
149
 
150
+ def get_terminals(self) -> list[GameObject]:
151
+ """Get map-placed terminal objects (tribute terminals, city terminals).
152
+
153
+ Inventory components and item sub-objects are excluded.
154
+ """
155
+ return [
156
+ obj
157
+ for obj in self.objects
158
+ if ("TributeTerminal" in obj.class_name or "CityTerminal" in obj.class_name)
159
+ and not obj.is_item
160
+ and "Inventory" not in obj.class_name
161
+ and "PrimalItem" not in obj.class_name
162
+ ]
163
+
164
+ def get_supply_drops(self) -> list[GameObject]:
165
+ """Get active supply-drop / loot-crate objects on the map.
166
+
167
+ Inventory components are excluded.
168
+ """
169
+ _SUPPLY_PATTERNS = ("SupplyCrate", "OrbitalSupply", "SupplyDrop")
170
+ return [
171
+ obj
172
+ for obj in self.objects
173
+ if any(p in obj.class_name for p in _SUPPLY_PATTERNS)
174
+ and "Inventory" not in obj.class_name
175
+ and not obj.is_item
176
+ ]
177
+
178
+ def get_artifact_crates(self) -> list[GameObject]:
179
+ """Get artifact-crate spawn objects. Inventory components are excluded."""
180
+ return [
181
+ obj
182
+ for obj in self.objects
183
+ if "ArtifactCrate" in obj.class_name and "Inventory" not in obj.class_name and not obj.is_item
184
+ ]
185
+
186
+ def get_map_resources(self) -> list[GameObject]:
187
+ """Get engine-placed resource / vein / node objects.
188
+
189
+ Covers oil veins, water veins, gas veins, charge nodes, element
190
+ veins, and beaver dams. Inventory components are excluded.
191
+ """
192
+ _RESOURCE_PATTERNS = (
193
+ "OilVein",
194
+ "WaterVein",
195
+ "GasVein",
196
+ "ChargeNode",
197
+ "ElementVein",
198
+ "BeaverDam",
199
+ )
200
+ return [
201
+ obj
202
+ for obj in self.objects
203
+ if any(p in obj.class_name for p in _RESOURCE_PATTERNS)
204
+ and "Inventory" not in obj.class_name
205
+ and not obj.is_item
206
+ ]
207
+
153
208
  def get_players(self) -> list[GameObject]:
154
209
  """Get all player data objects."""
155
210
  return self.get_by_class("PrimalPlayerData") + self.find_by_class_pattern("PlayerPawnTest")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Pure Python parser for ARK: Survival Evolved and ARK: Survival Ascended save files
5
5
  Author: Vertyco
6
6
  License-Expression: MIT
@@ -338,6 +338,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
338
338
  | `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
339
339
  | `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
340
340
  | `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
341
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
342
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
343
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
344
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
341
345
  | `get_items()` | `list[GameObject]` | Item objects |
342
346
  | `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
343
347
  | `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
@@ -390,6 +394,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
390
394
  | `get_creatures()` | `list[GameObject]` | All creatures |
391
395
  | `get_structures()` | `list[GameObject]` | Tribe-owned structures |
392
396
  | `get_player_pawns()` | `list[GameObject]` | Player characters on map |
397
+ | `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
398
+ | `get_supply_drops()` | `list[GameObject]` | Active supply crates |
399
+ | `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
400
+ | `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
393
401
  | `get_players()` | `list[GameObject]` | Player data objects |
394
402
  | `get_items()` | `list[GameObject]` | Item objects |
395
403
 
@@ -51,4 +51,5 @@ tests/test_cloud_inventory.py
51
51
  tests/test_models.py
52
52
  tests/test_profile.py
53
53
  tests/test_tribe.py
54
- tests/test_version_detection.py
54
+ tests/test_version_detection.py
55
+ tests/test_world_save.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "Pure Python parser for ARK: Survival Evolved and ARK: Survival Ascended save files"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,9 +2,10 @@
2
2
  Tests for cloud inventory (obelisk) parsing - both ASE and ASA formats.
3
3
  """
4
4
 
5
- import pytest
6
5
  from pathlib import Path
7
6
 
7
+ import pytest
8
+
8
9
  from arkparser import CloudInventory
9
10
 
10
11
 
@@ -126,6 +127,7 @@ class TestCloudInventoryStats:
126
127
  def test_ase_base_stats_object(self, ase_obelisk_path: Path) -> None:
127
128
  """Stats should return a DinoStats object."""
128
129
  from arkparser import DinoStats
130
+
129
131
  inv = CloudInventory.load(ase_obelisk_path)
130
132
  creature = inv.uploaded_creatures[0]
131
133
  assert isinstance(creature.stats, DinoStats)
@@ -133,6 +135,7 @@ class TestCloudInventoryStats:
133
135
  def test_asa_base_stats_object(self, asa_obelisk_path: Path) -> None:
134
136
  """Stats should return a DinoStats object."""
135
137
  from arkparser import DinoStats
138
+
136
139
  inv = CloudInventory.load(asa_obelisk_path)
137
140
  creature = inv.uploaded_creatures[0]
138
141
  assert isinstance(creature.stats, DinoStats)
@@ -146,3 +149,87 @@ class TestCloudInventoryStats:
146
149
  assert "level" in d
147
150
  assert "stats" in d
148
151
 
152
+
153
+ class TestASESolecluster:
154
+ """
155
+ Tests for ASE solecluster (cross-ARK transfer) files.
156
+
157
+ The solecluster directory contains 128 files; 15 are 0-byte empties.
158
+ All non-empty files must parse without errors.
159
+ """
160
+
161
+ def test_all_parse_without_errors(self, ase_solecluster_dir: Path) -> None:
162
+ """Every non-empty ASE solecluster file should load without raising."""
163
+ failures: list[str] = []
164
+ for f in sorted(ase_solecluster_dir.iterdir()):
165
+ if f.stat().st_size == 0:
166
+ continue
167
+ try:
168
+ CloudInventory.load(f)
169
+ except Exception as e:
170
+ failures.append(f"{f.name}: {e}")
171
+ assert failures == [], "Parse failures:\n" + "\n".join(failures)
172
+
173
+ def test_format_is_ase(self, ase_solecluster_dir: Path) -> None:
174
+ """All non-empty ASE solecluster files should be identified as ASE."""
175
+ for f in sorted(ase_solecluster_dir.iterdir())[:20]:
176
+ if f.stat().st_size == 0:
177
+ continue
178
+ inv = CloudInventory.load(f)
179
+ assert not inv.is_asa, f"{f.name} was wrongly detected as ASA"
180
+
181
+ def test_have_objects(self, ase_solecluster_dir: Path) -> None:
182
+ """Every non-empty ASE solecluster file should contain at least 1 object."""
183
+ for f in sorted(ase_solecluster_dir.iterdir())[:20]:
184
+ if f.stat().st_size == 0:
185
+ continue
186
+ inv = CloudInventory.load(f)
187
+ assert len(inv.objects) >= 1, f"{f.name} has no objects"
188
+
189
+ def test_nonempty_count(self, ase_solecluster_dir: Path) -> None:
190
+ """Sanity check: at least 100 non-empty files in the ASE solecluster dir."""
191
+ nonempty = [f for f in ase_solecluster_dir.iterdir() if f.stat().st_size > 0]
192
+ assert len(nonempty) >= 100
193
+
194
+
195
+ class TestASASolecluster:
196
+ """
197
+ Tests for ASA solecluster (cross-ARK transfer) files.
198
+
199
+ These are version-6 ASA files: GUID-based object headers but ASE-style
200
+ properties. The solecluster directory contains 173 files; 35 are 0-byte empties.
201
+ All non-empty files must parse without errors.
202
+ """
203
+
204
+ def test_all_parse_without_errors(self, asa_solecluster_dir: Path) -> None:
205
+ """Every non-empty ASA solecluster file should load without raising."""
206
+ failures: list[str] = []
207
+ for f in sorted(asa_solecluster_dir.iterdir()):
208
+ if f.stat().st_size == 0:
209
+ continue
210
+ try:
211
+ CloudInventory.load(f)
212
+ except Exception as e:
213
+ failures.append(f"{f.name}: {e}")
214
+ assert failures == [], "Parse failures:\n" + "\n".join(failures)
215
+
216
+ def test_format_is_asa(self, asa_solecluster_dir: Path) -> None:
217
+ """All non-empty ASA solecluster files should be identified as ASA."""
218
+ for f in sorted(asa_solecluster_dir.iterdir())[:20]:
219
+ if f.stat().st_size == 0:
220
+ continue
221
+ inv = CloudInventory.load(f)
222
+ assert inv.is_asa, f"{f.name} was wrongly detected as ASE"
223
+
224
+ def test_have_objects(self, asa_solecluster_dir: Path) -> None:
225
+ """Every non-empty ASA solecluster file should contain at least 1 object."""
226
+ for f in sorted(asa_solecluster_dir.iterdir())[:20]:
227
+ if f.stat().st_size == 0:
228
+ continue
229
+ inv = CloudInventory.load(f)
230
+ assert len(inv.objects) >= 1, f"{f.name} has no objects"
231
+
232
+ def test_nonempty_count(self, asa_solecluster_dir: Path) -> None:
233
+ """Sanity check: at least 130 non-empty files in the ASA solecluster dir."""
234
+ nonempty = [f for f in asa_solecluster_dir.iterdir() if f.stat().st_size > 0]
235
+ assert len(nonempty) >= 130
@@ -0,0 +1,252 @@
1
+ """
2
+ Comprehensive tests for WorldSave parsing across multiple maps and formats.
3
+
4
+ Baselines (verified against example save files):
5
+ ASE Extinction – objects=77392, tamed=37, wild=30074, pawns=65
6
+ terminals=32, artifacts=3, resources=37
7
+ ASE Ragnarok – objects=93194, tamed=1, wild=1, pawns=1245
8
+ terminals=3, artifacts=11, resources=118
9
+ ASA Extinction – objects=128751, tamed=181, wild=32438, pawns=67
10
+ terminals=31, artifacts=3, resources=18
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from arkparser import WorldSave
18
+ from arkparser.game_objects.game_object import GameObject
19
+ from arkparser.game_objects.location import LocationData
20
+
21
+ _EXAMPLES = Path(__file__).parent.parent / "references" / "examples"
22
+ _ASE_EXTINCTION = _EXAMPLES / "ase" / "maps" / "extinction" / "Extinction.ark"
23
+ _ASE_RAGNAROK = _EXAMPLES / "ase" / "maps" / "ragnarok" / "Ragnarok.ark"
24
+ _ASA_EXTINCTION = _EXAMPLES / "asa" / "maps" / "extinction" / "Extinction_WP.ark"
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Shared fixtures – the world-save objects are expensive to load, so we
29
+ # cache them at module scope via session-scoped fixtures.
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ @pytest.fixture(scope="session")
34
+ def ase_extinction() -> WorldSave:
35
+ return WorldSave.load(_ASE_EXTINCTION)
36
+
37
+
38
+ @pytest.fixture(scope="session")
39
+ def ase_ragnarok() -> WorldSave:
40
+ return WorldSave.load(_ASE_RAGNAROK)
41
+
42
+
43
+ @pytest.fixture(scope="session")
44
+ def asa_extinction() -> WorldSave:
45
+ return WorldSave.load(_ASA_EXTINCTION)
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # ASE Extinction
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ class TestASEWorldSaveExtinction:
54
+ """Tests against the ASE Extinction world save (Extinction.ark)."""
55
+
56
+ def test_loads(self, ase_extinction: WorldSave) -> None:
57
+ assert ase_extinction is not None
58
+
59
+ def test_is_not_asa(self, ase_extinction: WorldSave) -> None:
60
+ assert not ase_extinction.is_asa
61
+
62
+ def test_object_count(self, ase_extinction: WorldSave) -> None:
63
+ assert len(ase_extinction.objects) == 77392
64
+
65
+ def test_zero_parse_errors(self, ase_extinction: WorldSave) -> None:
66
+ assert ase_extinction.parse_error_count == 0
67
+
68
+ def test_tamed_creature_count(self, ase_extinction: WorldSave) -> None:
69
+ assert len(ase_extinction.get_tamed_creatures()) == 37
70
+
71
+ def test_wild_creature_count(self, ase_extinction: WorldSave) -> None:
72
+ assert len(ase_extinction.get_wild_creatures()) == 30074
73
+
74
+ def test_player_pawn_count(self, ase_extinction: WorldSave) -> None:
75
+ assert len(ase_extinction.get_player_pawns()) == 65
76
+
77
+ def test_terminal_count(self, ase_extinction: WorldSave) -> None:
78
+ assert len(ase_extinction.get_terminals()) == 32
79
+
80
+ def test_artifact_crate_count(self, ase_extinction: WorldSave) -> None:
81
+ assert len(ase_extinction.get_artifact_crates()) == 3
82
+
83
+ def test_map_resource_count(self, ase_extinction: WorldSave) -> None:
84
+ assert len(ase_extinction.get_map_resources()) == 37
85
+
86
+ def test_supply_drops_empty(self, ase_extinction: WorldSave) -> None:
87
+ """No active supply drops in this save."""
88
+ assert len(ase_extinction.get_supply_drops()) == 0
89
+
90
+ def test_creatures_have_class_name(self, ase_extinction: WorldSave) -> None:
91
+ for creature in ase_extinction.get_tamed_creatures():
92
+ assert isinstance(creature.class_name, str)
93
+ assert len(creature.class_name) > 0
94
+
95
+ def test_tamed_creatures_have_location(self, ase_extinction: WorldSave) -> None:
96
+ for creature in ase_extinction.get_tamed_creatures():
97
+ assert isinstance(creature.location, LocationData)
98
+
99
+ def test_wild_creature_is_game_object(self, ase_extinction: WorldSave) -> None:
100
+ for c in ase_extinction.get_wild_creatures()[:10]:
101
+ assert isinstance(c, GameObject)
102
+
103
+ def test_terminals_have_class_name(self, ase_extinction: WorldSave) -> None:
104
+ for terminal in ase_extinction.get_terminals():
105
+ cn = terminal.class_name.lower()
106
+ assert "terminal" in cn or "tribute" in cn or "city" in cn
107
+
108
+ def test_artifacts_have_class_name(self, ase_extinction: WorldSave) -> None:
109
+ for art in ase_extinction.get_artifact_crates():
110
+ assert "artifact" in art.class_name.lower()
111
+
112
+ def test_resources_have_class_name(self, ase_extinction: WorldSave) -> None:
113
+ valid_patterns = ("oilvein", "watervein", "gasvein", "chargenode", "elementvein", "beaverdam")
114
+ for r in ase_extinction.get_map_resources():
115
+ cn = r.class_name.lower()
116
+ assert any(p in cn for p in valid_patterns), f"Unexpected resource class: {r.class_name}"
117
+
118
+ def test_version_is_valid_ase(self, ase_extinction: WorldSave) -> None:
119
+ assert ase_extinction.version in (5, 6, 7, 8, 9, 10, 11, 12)
120
+ assert not ase_extinction.is_asa
121
+
122
+ def test_to_dict(self, ase_extinction: WorldSave) -> None:
123
+ d = ase_extinction.to_dict()
124
+ assert isinstance(d, dict)
125
+ assert "version" in d
126
+ assert "is_asa" in d
127
+ assert d["is_asa"] is False
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # ASE Ragnarok
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ class TestASEWorldSaveRagnarok:
136
+ """Tests against the ASE Ragnarok world save (Ragnarok.ark)."""
137
+
138
+ def test_loads(self, ase_ragnarok: WorldSave) -> None:
139
+ assert ase_ragnarok is not None
140
+
141
+ def test_is_not_asa(self, ase_ragnarok: WorldSave) -> None:
142
+ assert not ase_ragnarok.is_asa
143
+
144
+ def test_object_count(self, ase_ragnarok: WorldSave) -> None:
145
+ assert len(ase_ragnarok.objects) == 93194
146
+
147
+ def test_zero_parse_errors(self, ase_ragnarok: WorldSave) -> None:
148
+ assert ase_ragnarok.parse_error_count == 0
149
+
150
+ def test_tamed_creature_count(self, ase_ragnarok: WorldSave) -> None:
151
+ assert len(ase_ragnarok.get_tamed_creatures()) == 1
152
+
153
+ def test_wild_creature_count(self, ase_ragnarok: WorldSave) -> None:
154
+ assert len(ase_ragnarok.get_wild_creatures()) == 1
155
+
156
+ def test_player_pawn_count(self, ase_ragnarok: WorldSave) -> None:
157
+ assert len(ase_ragnarok.get_player_pawns()) == 1245
158
+
159
+ def test_terminal_count(self, ase_ragnarok: WorldSave) -> None:
160
+ assert len(ase_ragnarok.get_terminals()) == 3
161
+
162
+ def test_artifact_crate_count(self, ase_ragnarok: WorldSave) -> None:
163
+ assert len(ase_ragnarok.get_artifact_crates()) == 11
164
+
165
+ def test_map_resource_count(self, ase_ragnarok: WorldSave) -> None:
166
+ assert len(ase_ragnarok.get_map_resources()) == 118
167
+
168
+ def test_supply_drops_empty(self, ase_ragnarok: WorldSave) -> None:
169
+ assert len(ase_ragnarok.get_supply_drops()) == 0
170
+
171
+ def test_version_is_valid_ase(self, ase_ragnarok: WorldSave) -> None:
172
+ assert ase_ragnarok.version in (5, 6, 7, 8, 9, 10, 11, 12)
173
+ assert not ase_ragnarok.is_asa
174
+
175
+ def test_to_dict(self, ase_ragnarok: WorldSave) -> None:
176
+ d = ase_ragnarok.to_dict()
177
+ assert isinstance(d, dict)
178
+ assert d["is_asa"] is False
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # ASA Extinction
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ class TestASAWorldSaveExtinction:
187
+ """Tests against the ASA Extinction world save (Extinction_WP.ark)."""
188
+
189
+ def test_loads(self, asa_extinction: WorldSave) -> None:
190
+ assert asa_extinction is not None
191
+
192
+ def test_is_asa(self, asa_extinction: WorldSave) -> None:
193
+ assert asa_extinction.is_asa
194
+
195
+ def test_object_count(self, asa_extinction: WorldSave) -> None:
196
+ assert len(asa_extinction.objects) == 128751
197
+
198
+ def test_zero_parse_errors(self, asa_extinction: WorldSave) -> None:
199
+ assert asa_extinction.parse_error_count == 0
200
+
201
+ def test_tamed_creature_count(self, asa_extinction: WorldSave) -> None:
202
+ assert len(asa_extinction.get_tamed_creatures()) == 181
203
+
204
+ def test_wild_creature_count(self, asa_extinction: WorldSave) -> None:
205
+ assert len(asa_extinction.get_wild_creatures()) == 32438
206
+
207
+ def test_player_pawn_count(self, asa_extinction: WorldSave) -> None:
208
+ assert len(asa_extinction.get_player_pawns()) == 67
209
+
210
+ def test_terminal_count(self, asa_extinction: WorldSave) -> None:
211
+ assert len(asa_extinction.get_terminals()) == 31
212
+
213
+ def test_artifact_crate_count(self, asa_extinction: WorldSave) -> None:
214
+ assert len(asa_extinction.get_artifact_crates()) == 3
215
+
216
+ def test_map_resource_count(self, asa_extinction: WorldSave) -> None:
217
+ assert len(asa_extinction.get_map_resources()) == 18
218
+
219
+ def test_supply_drops_empty(self, asa_extinction: WorldSave) -> None:
220
+ assert len(asa_extinction.get_supply_drops()) == 0
221
+
222
+ def test_creatures_have_class_name(self, asa_extinction: WorldSave) -> None:
223
+ for creature in asa_extinction.get_tamed_creatures()[:10]:
224
+ assert isinstance(creature.class_name, str)
225
+ assert len(creature.class_name) > 0
226
+
227
+ def test_tamed_creatures_have_location(self, asa_extinction: WorldSave) -> None:
228
+ for creature in asa_extinction.get_tamed_creatures()[:10]:
229
+ assert isinstance(creature.location, LocationData)
230
+
231
+ def test_terminals_have_class_name(self, asa_extinction: WorldSave) -> None:
232
+ for terminal in asa_extinction.get_terminals():
233
+ cn = terminal.class_name.lower()
234
+ assert "terminal" in cn or "tribute" in cn or "city" in cn
235
+
236
+ def test_artifacts_have_class_name(self, asa_extinction: WorldSave) -> None:
237
+ for art in asa_extinction.get_artifact_crates():
238
+ assert "artifact" in art.class_name.lower()
239
+
240
+ def test_resources_have_class_name(self, asa_extinction: WorldSave) -> None:
241
+ valid_patterns = ("oilvein", "watervein", "gasvein", "chargenode", "elementvein", "beaverdam")
242
+ for r in asa_extinction.get_map_resources():
243
+ cn = r.class_name.lower()
244
+ assert any(p in cn for p in valid_patterns), f"Unexpected resource class: {r.class_name}"
245
+
246
+ def test_version_is_asa(self, asa_extinction: WorldSave) -> None:
247
+ assert asa_extinction.version >= 7
248
+
249
+ def test_to_dict(self, asa_extinction: WorldSave) -> None:
250
+ d = asa_extinction.to_dict()
251
+ assert isinstance(d, dict)
252
+ assert d["is_asa"] is True
File without changes
File without changes
File without changes
File without changes
File without changes