arkparser 0.1.8__tar.gz → 0.1.11__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 (61) hide show
  1. {arkparser-0.1.8 → arkparser-0.1.11}/PKG-INFO +1 -1
  2. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/__init__.py +1 -1
  3. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/export.py +99 -15
  4. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/world_save.py +17 -21
  5. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/game_objects/container.py +79 -13
  6. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser.egg-info/PKG-INFO +1 -1
  7. {arkparser-0.1.8 → arkparser-0.1.11}/pyproject.toml +1 -1
  8. {arkparser-0.1.8 → arkparser-0.1.11}/LICENSE +0 -0
  9. {arkparser-0.1.8 → arkparser-0.1.11}/README.md +0 -0
  10. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/__init__.py +0 -0
  11. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/binary_reader.py +0 -0
  12. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/exceptions.py +0 -0
  13. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/map_config.py +0 -0
  14. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/normalization.py +0 -0
  15. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/types.py +0 -0
  16. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/common/version_detection.py +0 -0
  17. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/data_models.py +0 -0
  18. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/__init__.py +0 -0
  19. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/base.py +0 -0
  20. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/cloud_inventory.py +0 -0
  21. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/profile.py +0 -0
  22. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/files/tribe.py +0 -0
  23. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/game_objects/__init__.py +0 -0
  24. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/game_objects/game_object.py +0 -0
  25. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/game_objects/location.py +0 -0
  26. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/__init__.py +0 -0
  27. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/character.py +0 -0
  28. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/creature.py +0 -0
  29. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/item.py +0 -0
  30. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/player.py +0 -0
  31. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/stats.py +0 -0
  32. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/structure.py +0 -0
  33. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/models/tribe.py +0 -0
  34. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/__init__.py +0 -0
  35. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/base.py +0 -0
  36. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/byte_property.py +0 -0
  37. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/compound.py +0 -0
  38. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/primitives.py +0 -0
  39. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/properties/registry.py +0 -0
  40. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/__init__.py +0 -0
  41. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/base.py +0 -0
  42. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/colors.py +0 -0
  43. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/misc.py +0 -0
  44. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/property_list.py +0 -0
  45. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/registry.py +0 -0
  46. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser/structs/vectors.py +0 -0
  47. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser.egg-info/SOURCES.txt +0 -0
  48. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser.egg-info/dependency_links.txt +0 -0
  49. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser.egg-info/requires.txt +0 -0
  50. {arkparser-0.1.8 → arkparser-0.1.11}/arkparser.egg-info/top_level.txt +0 -0
  51. {arkparser-0.1.8 → arkparser-0.1.11}/setup.cfg +0 -0
  52. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_binary_reader.py +0 -0
  53. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_cloud_inventory.py +0 -0
  54. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_data_models.py +0 -0
  55. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_export.py +0 -0
  56. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_game_objects.py +0 -0
  57. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_models.py +0 -0
  58. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_profile.py +0 -0
  59. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_tribe.py +0 -0
  60. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_version_detection.py +0 -0
  61. {arkparser-0.1.8 → arkparser-0.1.11}/tests/test_world_save.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.1.8
3
+ Version: 0.1.11
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
@@ -115,4 +115,4 @@ __all__ = [
115
115
  "ArkParseError",
116
116
  ]
117
117
 
118
- __version__ = "0.1.8"
118
+ __version__ = "0.1.11"
@@ -351,27 +351,84 @@ def export_tribe_logs(
351
351
 
352
352
 
353
353
  # -------------------------------------------------------------------------
354
- # Map Structures (ASV_MapStructures): structures with GPS coords
354
+ # Map Structures (ASV_MapStructures): untribed environmental map elements
355
355
  # -------------------------------------------------------------------------
356
356
 
357
+ # Canonical match rules. Mirrors C# ContentContainer.cs:846.
358
+ # (pattern, label, match_kind) where match_kind is "startswith" or "contains".
359
+ # Order matters: first match wins.
360
+ _MAP_STRUCT_RULES: tuple[tuple[str, str, str], ...] = (
361
+ ("TributeTerminal_", "ASV_Terminal", "startswith"),
362
+ ("CityTerminal_", "ASV_Terminal", "contains"),
363
+ ("PowerNodeCharge", "ASV_ChargeNode", "startswith"),
364
+ ("BeaverDam_C", "ASV_BeaverDam", "startswith"),
365
+ ("DeinonychusNest_C", "ASV_DeinoNest", "startswith"),
366
+ ("RockDrakeNest_C", "ASV_DrakeNest", "startswith"),
367
+ ("CherufeNest_C", "ASV_MagmaNest", "startswith"),
368
+ ("WyvernNest_", "ASV_WyvernNest", "startswith"),
369
+ ("OilVein_", "ASV_OilVein", "startswith"),
370
+ ("WaterVein_", "ASV_WaterVein", "startswith"),
371
+ ("GasVein_", "ASV_GasVein", "startswith"),
372
+ ("ArtifactCrate_", "ASV_Artifact", "startswith"),
373
+ # PlantSpeciesZ_Wild_C is in the C# prefix list but legacy doesn't actually
374
+ # emit them — only player-grown plants are surfaced.
375
+ ("Structure_PlantSpeciesZ_PlayerGrown", "ASV_PlantSpeciesZ", "startswith"),
376
+ ("BeeHive_C", "ASV_BeeHive", "startswith"),
377
+ )
378
+
379
+
380
+ def _asv_map_struct_label(class_name: str) -> str | None:
381
+ for pattern, label, kind in _MAP_STRUCT_RULES:
382
+ if kind == "startswith":
383
+ if class_name.startswith(pattern):
384
+ return label
385
+ elif pattern in class_name:
386
+ return label
387
+ return None
388
+
357
389
 
358
390
  def export_map_structures(
359
391
  save: t.Any,
360
392
  map_config: MapConfig | None = None,
361
393
  ) -> list[dict[str, t.Any]]:
362
394
  """
363
- Export structures with map coordinates in ASV_MapStructures format.
364
-
365
- Same as ASV_Structures but intended for map visualisation with GPS coords.
395
+ Export environmental map elements in ASV_MapStructures format.
366
396
 
367
- Args:
368
- save: A parsed savegame.
369
- map_config: Map config for GPS coordinate conversion.
397
+ Mirrors C# ContentContainer.cs:846 filter:
398
+ * has a Location
399
+ * TargetingTeam is null
400
+ * class_name starts with one of the canonical map-element prefixes.
370
401
 
371
- Returns:
372
- List of structure dictionaries with GPS coordinates.
402
+ Does NOT include player structures.
373
403
  """
374
- return export_structures(save, map_config)
404
+ assert hasattr(save, "objects"), "save must expose .objects"
405
+ objects = save.objects if hasattr(save, "objects") else {}
406
+ obj_lookup = _build_lookup(objects)
407
+ all_objs = list(objects.values()) if isinstance(objects, dict) else list(objects)
408
+
409
+ results: list[dict[str, t.Any]] = []
410
+ bound = len(all_objs) + 1
411
+ for i, obj in enumerate(all_objs):
412
+ assert i < bound
413
+ cn = getattr(obj, "class_name", "") or ""
414
+ label = _asv_map_struct_label(cn)
415
+ if label is None:
416
+ continue
417
+ loc = getattr(obj, "location", None)
418
+ if loc is None:
419
+ continue
420
+ if obj.get_property_value("TargetingTeam") is not None:
421
+ continue
422
+ ccc = f"{loc.x} {loc.y} {loc.z}"
423
+ data: dict[str, t.Any] = {"struct": label, "ccc": ccc}
424
+ if map_config is not None:
425
+ data["lat"] = map_config.ue_to_lat(loc.y)
426
+ data["lon"] = map_config.ue_to_lon(loc.x)
427
+ data["inventory"] = _get_inventory_items(obj, obj_lookup)
428
+ results.append(data)
429
+
430
+ assert isinstance(results, list)
431
+ return results
375
432
 
376
433
 
377
434
  # -------------------------------------------------------------------------
@@ -478,6 +535,9 @@ def _get_collection(source: t.Any, attr_name: str, parser_type: type[t.Any]) ->
478
535
  """Normalize a parser collection or single parser instance to a list."""
479
536
  if isinstance(source, parser_type):
480
537
  return [source]
538
+ # source may already be a list of parser objects
539
+ if isinstance(source, (list, tuple)):
540
+ return list(source)
481
541
 
482
542
  values = getattr(source, attr_name, None)
483
543
  if isinstance(values, (list, tuple)):
@@ -500,9 +560,18 @@ def _get_worldsave_objects(save: t.Any, getter_name: str, legacy_attr: str) -> l
500
560
 
501
561
 
502
562
  def _resolve_reference(ref: t.Any, lookup: dict[t.Any, t.Any]) -> t.Any:
503
- """Resolve an object reference stored as an ID, GUID, or name."""
563
+ """Resolve an object reference to a game object.
564
+
565
+ Handles three reference forms:
566
+ - int or str: direct lookup (ASE object IDs, GUID strings, name strings)
567
+ - (type_tag, value) tuple: array-property format where type_tag is "id",
568
+ "name", or "path" and value is the actual lookup key
569
+ """
504
570
  if ref is None:
505
571
  return None
572
+ if isinstance(ref, tuple) and len(ref) == 2:
573
+ _, key = ref
574
+ return lookup.get(key)
506
575
  return lookup.get(ref)
507
576
 
508
577
 
@@ -598,14 +667,29 @@ def _find_status_component(obj: t.Any, lookup: dict[t.Any, t.Any]) -> t.Any:
598
667
 
599
668
 
600
669
  def _get_inventory_items(obj: t.Any, lookup: dict[t.Any, t.Any]) -> list[dict[str, t.Any]]:
601
- """Get inventory items for a creature."""
670
+ """Get inventory items for a game object (creature or structure).
671
+
672
+ Tries two strategies in order:
673
+ 1. MyInventoryComponent property + reference lookup (works for ASE and some ASA objects).
674
+ 2. Iterate obj.components to find the first component whose class_name contains
675
+ "Inventory" (fallback for ASA, where the property may be absent).
676
+ """
602
677
  items: list[dict[str, t.Any]] = []
603
678
 
604
- inv_ref = None
679
+ inv_obj = None
680
+
681
+ # Strategy 1: property reference
605
682
  if hasattr(obj, "get_property_value"):
606
683
  inv_ref = obj.get_property_value("MyInventoryComponent", default=None)
607
-
608
- inv_obj = _resolve_reference(inv_ref, lookup)
684
+ if inv_ref is not None:
685
+ inv_obj = _resolve_reference(inv_ref, lookup)
686
+
687
+ # Strategy 2: component dict (ASA or when property is absent)
688
+ if inv_obj is None and hasattr(obj, "components"):
689
+ for comp in obj.components.values():
690
+ if "Inventory" in getattr(comp, "class_name", ""):
691
+ inv_obj = comp
692
+ break
609
693
 
610
694
  if inv_obj is None:
611
695
  return items
@@ -217,31 +217,23 @@ class WorldSave:
217
217
  """Return all objects whose ``class_name`` contains *class_name*."""
218
218
  return self.container.find_by_class_pattern(class_name)
219
219
 
220
- _TAMED_MARKER_PROPERTIES: t.ClassVar[tuple[str, ...]] = (
221
- "TamerString",
222
- "TamedName",
223
- "TamedAtTime",
224
- "TribeName",
225
- "TamedOnServerName",
226
- "UploadedFromServerName",
227
- "ImprinterName",
228
- "ImprinterPlayerDataID",
229
- )
220
+ # TargetingTeam threshold: the C# reference (TeamType.cs) uses 50_000 as the
221
+ # boundary between non-player (wild/AI) and player-owned teams.
222
+ _PLAYER_TEAM_THRESHOLD: t.ClassVar[int] = 50_000
223
+ # Unclaimed babies use 2_000_000_000 as a sentinel; still tamed.
224
+ _BREEDING_SENTINEL: t.ClassVar[int] = 2_000_000_000
230
225
 
231
226
  def _is_tamed_creature(self, obj: GameObject) -> bool:
232
- """Return ``True`` if a creature carries tame ownership markers."""
233
- taming_team = obj.get_property_value("TamingTeamID")
234
- if isinstance(taming_team, (int, float)) and taming_team > 0:
235
- return True
227
+ """Return ``True`` if a creature is player-owned.
236
228
 
229
+ Mirrors C# GameObjectExtensions.IsTamed: targeting_team >= 50_000.
230
+ The breeding sentinel (2_000_000_000) is also considered tamed.
231
+ """
237
232
  targeting_team = obj.get_property_value("TargetingTeam")
238
- if isinstance(targeting_team, (int, float)) and targeting_team > 1000:
239
- return True
240
-
241
- return any(
242
- obj.get_property_value(prop_name) not in (None, "", 0, 0.0, False)
243
- for prop_name in self._TAMED_MARKER_PROPERTIES
244
- )
233
+ if not isinstance(targeting_team, (int, float)):
234
+ return False
235
+ team = int(targeting_team)
236
+ return team >= self._PLAYER_TEAM_THRESHOLD
245
237
 
246
238
  def get_creatures(self) -> list[GameObject]:
247
239
  """Return all creature objects (tamed **and** wild)."""
@@ -283,6 +275,10 @@ class WorldSave:
283
275
  """Return engine-placed resource / vein / node objects."""
284
276
  return self.container.get_map_resources()
285
277
 
278
+ def get_nests(self) -> list[GameObject]:
279
+ """Return creature nest objects (wyvern, drake, etc.)."""
280
+ return self.container.get_nests()
281
+
286
282
  # ------------------------------------------------------------------
287
283
  # Properties
288
284
  # ------------------------------------------------------------------
@@ -121,14 +121,35 @@ class GameObjectContainer:
121
121
  "DinoDropInventory",
122
122
  )
123
123
 
124
+ # Vehicles that carry DinoID1/bServerInitializedDino but are not creatures.
125
+ # Source: C# GameObjectExtensions.IsCreature (SavegameToolkitAdditions).
126
+ _VEHICLE_CLASS_NAMES: t.ClassVar[frozenset[str]] = frozenset({
127
+ "MotorRaft_BP_C",
128
+ "Raft_BP_C",
129
+ "TekHoverSkiff_Character_BP_C",
130
+ "CogRaft_BP_C",
131
+ "DingyRaft_BP_C",
132
+ "LongshipRaft_BP_C",
133
+ "SRaft_BP_C",
134
+ })
135
+
124
136
  def _is_creature_object(self, obj: GameObject) -> bool:
125
- """Return ``True`` for top-level creature actors only."""
126
- class_name = obj.class_name
127
- if not class_name or obj.is_item:
128
- return False
137
+ """Return ``True`` for top-level creature actors only.
129
138
 
130
- is_creature = "_Character_BP" in class_name or "DinoCharacter" in class_name
131
- return is_creature and "StatusComponent" not in class_name and "Inventory" not in class_name
139
+ Primary check mirrors C# GameObjectExtensions.IsCreature: presence of
140
+ ``bServerInitializedDino`` AND not a vehicle. Falls back to class name
141
+ patterns for unit tests and edge cases where properties are absent.
142
+ """
143
+ if obj.is_item:
144
+ return False
145
+ if obj.class_name in self._VEHICLE_CLASS_NAMES:
146
+ return False
147
+ if obj.get_property_value("bServerInitializedDino") is not None:
148
+ return True
149
+ # Class-name fallback for minimal test objects or pre-property-load pass.
150
+ cn = obj.class_name
151
+ is_character = "_Character_" in cn or "DinoCharacter" in cn
152
+ return is_character and "StatusComponent" not in cn and "Inventory" not in cn
132
153
 
133
154
  def get_creatures(self) -> list[GameObject]:
134
155
  """Get all creature objects (tamed and wild)."""
@@ -138,8 +159,48 @@ class GameObjectContainer:
138
159
  """Get all item objects."""
139
160
  return [obj for obj in self.objects if obj.is_item]
140
161
 
162
+ # Class-name substrings for environmental map elements that should NOT
163
+ # appear in get_structures() (they go through get_terminals/get_nests/etc).
164
+ _MAP_ELEMENT_PATTERNS: t.ClassVar[tuple[str, ...]] = (
165
+ "TributeTerminal",
166
+ "CityTerminal",
167
+ "ArtifactCrate",
168
+ "OilVein",
169
+ "WaterVein",
170
+ "GasVein",
171
+ "ChargeNode",
172
+ "ElementVein",
173
+ "BeaverDam",
174
+ "Nest",
175
+ )
176
+
177
+ def _is_structure(self, obj: GameObject) -> bool:
178
+ """Return ``True`` if the object is a structure per the C# IsStructure rule.
179
+
180
+ Mirrors C# GameObjectExtensions.IsStructure: has OwnerName OR
181
+ bHasResetDecayTime OR is one of the known vehicle/special class names;
182
+ excludes the Structure_LoadoutDummy_Hotbar_C, cryo-pinned objects,
183
+ DeathItemCache_ entries, and environmental map elements (which belong
184
+ in map_structures, not player structures).
185
+ """
186
+ cn = obj.class_name
187
+ if cn == "Structure_LoadoutDummy_Hotbar_C":
188
+ return False
189
+ if cn.startswith("DeathItemCache_"):
190
+ return False
191
+ if obj.get_property_value("IsInCryo"):
192
+ return False
193
+ # CherufeNest_C is explicitly a structure per the C# rule.
194
+ if cn != "CherufeNest_C" and any(p in cn for p in self._MAP_ELEMENT_PATTERNS):
195
+ return False
196
+ if obj.get_property_value("OwnerName") is not None:
197
+ return True
198
+ if obj.get_property_value("bHasResetDecayTime") is not None:
199
+ return True
200
+ return cn == "CherufeNest_C" or cn in self._VEHICLE_CLASS_NAMES
201
+
141
202
  def get_structures(self) -> list[GameObject]:
142
- """Get all tribe-owned placed structures.
203
+ """Get all placed structures.
143
204
 
144
205
  ASE saves include the same actor in both the main level and sub-levels,
145
206
  producing duplicate entries with identical Names[0]. Deduplicating by
@@ -149,12 +210,7 @@ class GameObjectContainer:
149
210
  seen_names: set[str] = set()
150
211
  results: list[GameObject] = []
151
212
  for obj in self.objects:
152
- cn = obj.class_name
153
- if obj.get_property_value("TargetingTeam") is None:
154
- continue
155
- if obj.get_property_value("DinoID1") is not None:
156
- continue
157
- if any(pat in cn for pat in self._NON_STRUCTURE_PATTERNS):
213
+ if not self._is_structure(obj):
158
214
  continue
159
215
  name = obj.primary_name
160
216
  if name is not None:
@@ -226,6 +282,16 @@ class GameObjectContainer:
226
282
  and not obj.is_item
227
283
  ]
228
284
 
285
+ def get_nests(self) -> list[GameObject]:
286
+ """Get creature nest objects (wyvern, drake, etc.). Inventory excluded."""
287
+ return [
288
+ obj
289
+ for obj in self.objects
290
+ if "Nest" in obj.class_name
291
+ and "Inventory" not in obj.class_name
292
+ and not obj.is_item
293
+ ]
294
+
229
295
  def get_players(self) -> list[GameObject]:
230
296
  """Get all player data objects."""
231
297
  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.8
3
+ Version: 0.1.11
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.1.8"
7
+ version = "0.1.11"
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"
File without changes
File without changes
File without changes