arkparser 0.7.0__tar.gz → 0.7.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 (63) hide show
  1. {arkparser-0.7.0 → arkparser-0.7.2}/PKG-INFO +10 -1
  2. {arkparser-0.7.0 → arkparser-0.7.2}/README.md +9 -0
  3. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/__init__.py +1 -1
  4. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/types.py +11 -0
  5. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/data_models.py +5 -1
  6. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/export.py +24 -19
  7. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/world_save.py +5 -7
  8. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/game_object.py +11 -1
  9. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/PKG-INFO +10 -1
  10. {arkparser-0.7.0 → arkparser-0.7.2}/pyproject.toml +1 -1
  11. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cryopod_export.py +76 -0
  12. {arkparser-0.7.0 → arkparser-0.7.2}/LICENSE +0 -0
  13. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/__init__.py +0 -0
  14. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/binary_reader.py +0 -0
  15. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/exceptions.py +0 -0
  16. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/map_config.py +0 -0
  17. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/normalization.py +0 -0
  18. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/version_detection.py +0 -0
  19. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/__init__.py +0 -0
  20. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/base.py +0 -0
  21. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/cloud_inventory.py +0 -0
  22. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/profile.py +0 -0
  23. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/tribe.py +0 -0
  24. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/__init__.py +0 -0
  25. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/container.py +0 -0
  26. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/location.py +0 -0
  27. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/__init__.py +0 -0
  28. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/base.py +0 -0
  29. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/byte_property.py +0 -0
  30. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/compound.py +0 -0
  31. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/primitives.py +0 -0
  32. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/registry.py +0 -0
  33. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/__init__.py +0 -0
  34. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/base.py +0 -0
  35. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/colors.py +0 -0
  36. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/misc.py +0 -0
  37. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/property_list.py +0 -0
  38. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/registry.py +0 -0
  39. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/vectors.py +0 -0
  40. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/SOURCES.txt +0 -0
  41. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/dependency_links.txt +0 -0
  42. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/requires.txt +0 -0
  43. {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/top_level.txt +0 -0
  44. {arkparser-0.7.0 → arkparser-0.7.2}/setup.cfg +0 -0
  45. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_asa_header_position.py +0 -0
  46. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_asa_name_table.py +0 -0
  47. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_ase_cluster_drift.py +0 -0
  48. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_binary_reader.py +0 -0
  49. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_binary_reader_layouts.py +0 -0
  50. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cloud_export.py +0 -0
  51. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cloud_inventory.py +0 -0
  52. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_current_stats.py +0 -0
  53. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_data_models.py +0 -0
  54. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_export.py +0 -0
  55. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_game_objects.py +0 -0
  56. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_golden_exports.py +0 -0
  57. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_lazy_properties.py +0 -0
  58. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_profile.py +0 -0
  59. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_review_fixes.py +0 -0
  60. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_tribe.py +0 -0
  61. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_v13_property_layouts.py +0 -0
  62. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_version_detection.py +0 -0
  63. {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_world_save.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.7.0
3
+ Version: 0.7.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
@@ -681,6 +681,15 @@ Schema policy:
681
681
 
682
682
  ASA worldsave property layouts differ between **v13** (`TheIsland_WP` and older single-player saves, legacy `AsaSavegameToolkit`-style `dataSize + position + typeRef + byte` body) and **v14+** (current production ASA, marker-based body). Both are parsed by version-aware property readers; `WorldSave.version` is the source of truth.
683
683
 
684
+ ### Parse diagnostics (logging)
685
+
686
+ The parser logs through the standard `logging` module on the `arkparser.*` logger hierarchy. At DEBUG level it reports recoverable parse anomalies, for example ASE objects whose property block terminates early (the run of successfully parsed properties is kept and the remaining bytes are preserved on `GameObject.extra_data`):
687
+
688
+ ```python
689
+ import logging
690
+ logging.getLogger("arkparser").setLevel(logging.DEBUG)
691
+ ```
692
+
684
693
  ### Known limitation: ASA cryopod property blocks are partially decoded
685
694
 
686
695
  ASA cryopods (`PrimalItem_WeaponEmptyCryopod_C`, `SoulTrap`, `Vivarium`, `DinoBall`) store the embedded creature snapshot inside the item's `CustomItemDatas.CustomDataBytes.ByteArrays[0]` as a **zlib + custom-RLE compressed `AsaDataStore` blob**. The parser surfaces the snapshot via the simpler `CustomDataStrings` / `CustomDataFloats` accessors, exposing **species, tamed name, level, color regions, and `CurrentStatusValues[0..11]`** on each cryopodded creature.
@@ -647,6 +647,15 @@ Schema policy:
647
647
 
648
648
  ASA worldsave property layouts differ between **v13** (`TheIsland_WP` and older single-player saves, legacy `AsaSavegameToolkit`-style `dataSize + position + typeRef + byte` body) and **v14+** (current production ASA, marker-based body). Both are parsed by version-aware property readers; `WorldSave.version` is the source of truth.
649
649
 
650
+ ### Parse diagnostics (logging)
651
+
652
+ The parser logs through the standard `logging` module on the `arkparser.*` logger hierarchy. At DEBUG level it reports recoverable parse anomalies, for example ASE objects whose property block terminates early (the run of successfully parsed properties is kept and the remaining bytes are preserved on `GameObject.extra_data`):
653
+
654
+ ```python
655
+ import logging
656
+ logging.getLogger("arkparser").setLevel(logging.DEBUG)
657
+ ```
658
+
650
659
  ### Known limitation: ASA cryopod property blocks are partially decoded
651
660
 
652
661
  ASA cryopods (`PrimalItem_WeaponEmptyCryopod_C`, `SoulTrap`, `Vivarium`, `DinoBall`) store the embedded creature snapshot inside the item's `CustomItemDatas.CustomDataBytes.ByteArrays[0]` as a **zlib + custom-RLE compressed `AsaDataStore` blob**. The parser surfaces the snapshot via the simpler `CustomDataStrings` / `CustomDataFloats` accessors, exposing **species, tamed name, level, color regions, and `CurrentStatusValues[0..11]`** on each cryopodded creature.
@@ -95,4 +95,4 @@ __all__ = [
95
95
  "ArkParseError",
96
96
  ]
97
97
 
98
- __version__ = "0.7.0"
98
+ __version__ = "0.7.2"
@@ -21,6 +21,17 @@ from dataclasses import dataclass
21
21
  # The "None" name is used as a terminator in property lists
22
22
  NAME_NONE = "None"
23
23
 
24
+ # Class-name / blueprint fragments that mark a creature-storage item.
25
+ # Single source of truth for cryopod detection: in-world iteration
26
+ # (WorldSave.iter_cryopod_creatures), inventory filtering (export), and
27
+ # uploaded-item checks (UploadedItem.is_cryopod) all match against this.
28
+ CRYOPOD_CLASS_PATTERNS: tuple[str, ...] = (
29
+ "Cryopod",
30
+ "SoulTrap",
31
+ "Vivarium",
32
+ "DinoBall",
33
+ )
34
+
24
35
 
25
36
  # =============================================================================
26
37
  # ArkName (FName)
@@ -15,10 +15,14 @@ from dataclasses import dataclass, field
15
15
 
16
16
  from .common.binary_reader import BinaryReader
17
17
  from .common.normalization import normalize_indexed_data, normalize_indexed_list
18
+ from .common.types import CRYOPOD_CLASS_PATTERNS
18
19
  from .properties.registry import read_properties
19
20
 
20
21
  logger = logging.getLogger(__name__)
21
22
 
23
+ # Uploaded items match against lowercased blueprint paths.
24
+ _CRYOPOD_PATTERNS_LOWER: tuple[str, ...] = tuple(p.lower() for p in CRYOPOD_CLASS_PATTERNS)
25
+
22
26
  # Trailing UE actor spawn-instance suffix on cryopod class names, e.g.
23
27
  # "Raptor_Character_BP_C_2145673735". ARK stores the full instance name in the
24
28
  # cryopod CustomDataStrings/SoftClasses blob; the worldsave name-table path
@@ -820,7 +824,7 @@ class UploadedItem:
820
824
  def is_cryopod(self) -> bool:
821
825
  """Check if this item is a cryopod (or similar creature storage item)."""
822
826
  bp_lower = self.blueprint.lower()
823
- return any(x in bp_lower for x in ["cryopod", "soultrap", "vivarium", "dinoball"])
827
+ return any(x in bp_lower for x in _CRYOPOD_PATTERNS_LOWER)
824
828
 
825
829
  @property
826
830
  def cryopod_creature(self) -> CryopodCreature | None:
@@ -44,21 +44,14 @@ from pathlib import Path
44
44
  from arkparser.common.exceptions import ArkParseError
45
45
  from arkparser.common.map_config import MapConfig
46
46
  from arkparser.common.normalization import normalize_indexed_data, normalize_indexed_list
47
+ from arkparser.common.types import CRYOPOD_CLASS_PATTERNS
47
48
  from arkparser.data_models import CryopodCreature
48
49
  from arkparser.files import CloudInventory, Profile, Tribe
49
50
 
50
51
  logger = logging.getLogger(__name__)
51
52
 
52
- _CRYOPOD_CLASS_PATTERNS: tuple[str, ...] = (
53
- "Cryopod",
54
- "SoulTrap",
55
- "Vivarium",
56
- "DinoBall",
57
- )
58
-
59
-
60
53
  def _is_cryopod_class(class_name: str) -> bool:
61
- return any(p in class_name for p in _CRYOPOD_CLASS_PATTERNS)
54
+ return any(p in class_name for p in CRYOPOD_CLASS_PATTERNS)
62
55
 
63
56
 
64
57
  def _decode_inventory_cryopod(item_obj: t.Any) -> CryopodCreature | None:
@@ -152,6 +145,10 @@ _LEGACY_STAT_ORDER: tuple[str, ...] = (
152
145
  )
153
146
  _EXTRA_STAT_ORDER: tuple[str, ...] = ("torp", "water", "temp", "fort")
154
147
  _FLAT_STAT_ORDER: tuple[str, ...] = _LEGACY_STAT_ORDER + _EXTRA_STAT_ORDER
148
+ # The order tuples are hand-maintained views of _STAT_NAMES; catch drift at
149
+ # import time before it silently misaligns exported stat JSON.
150
+ assert len(_FLAT_STAT_ORDER) == len(_STAT_NAMES), "stat order tuples out of sync"
151
+ assert set(_FLAT_STAT_ORDER) == set(_STAT_NAMES), "stat order tuples out of sync"
155
152
 
156
153
 
157
154
  def _prop(obj: t.Any, name: str, default: t.Any = None, index: int | None = None) -> t.Any:
@@ -1571,7 +1568,7 @@ def _build_item_owner_lookup(save: t.Any, lookup: dict[t.Any, t.Any]) -> dict[t.
1571
1568
  actor_infos = container._inv_actor_info.items()
1572
1569
  else:
1573
1570
  actor_infos = _iter_inv_actor_info(save)
1574
- for _actor_id, (inv_ref, team_raw, tribe_raw, owner_raw) in actor_infos:
1571
+ for actor_id, (inv_ref, team_raw, tribe_raw, owner_raw) in actor_infos:
1575
1572
  inv = _resolve(inv_ref, lookup)
1576
1573
  if inv is None:
1577
1574
  continue
@@ -1579,9 +1576,11 @@ def _build_item_owner_lookup(save: t.Any, lookup: dict[t.Any, t.Any]) -> dict[t.
1579
1576
  refs = _prop(inv, "InventoryItems")
1580
1577
  if not isinstance(refs, list) or not refs:
1581
1578
  continue
1579
+ owner_obj = lookup.get(actor_id) if actor_id is not None else None
1582
1580
  info = {
1583
1581
  "TargetingTeam": _int(team_raw),
1584
1582
  "TribeName": _str(tribe_raw) or _str(owner_raw),
1583
+ "OwnerLocation": getattr(owner_obj, "location", None),
1585
1584
  }
1586
1585
  for ref in refs:
1587
1586
  item_obj = _resolve(ref, lookup)
@@ -1670,7 +1669,14 @@ def _export_world_cryopods(
1670
1669
  summaries[item_id] = _cryo_summary(cryo)
1671
1670
  owner_info = owner_lookup.get(item_id) if item_id is not None else None
1672
1671
  if owner_info is not None:
1672
+ # Pods stored in inventories carry no actor transform of their
1673
+ # own (always the case on ASA); inherit the owning container's
1674
+ # location so GPS fields don't collapse to 0/0/0.
1675
+ if actor.location is None:
1676
+ actor.location = owner_info.get("OwnerLocation")
1673
1677
  for key, val in owner_info.items():
1678
+ if key == "OwnerLocation":
1679
+ continue
1674
1680
  if val and not cryo.creature_props.get(key):
1675
1681
  cryo.creature_props[key] = val
1676
1682
  record = _tamed_dict(actor, status, empty_lookup, map_config, save, stored=True)
@@ -2519,8 +2525,7 @@ def _tribe_from_object(
2519
2525
  tribe_id = _int(_prop(obj, "TribeID")) or _int(_prop(obj, "TribeId"))
2520
2526
  owner_id = _int(_prop(obj, "OwnerPlayerDataID")) or _int(_prop(obj, "OwnerPlayerDataId"))
2521
2527
  members: list[dict[str, t.Any]] = []
2522
- i = 0
2523
- while True:
2528
+ for i in range(_MAX_TRIBE_MEMBERS):
2524
2529
  pid = _prop(obj, "MembersPlayerDataID", index=i)
2525
2530
  if pid is None:
2526
2531
  break
@@ -2536,15 +2541,14 @@ def _tribe_from_object(
2536
2541
  "steamid": (profile.unique_id or "") if profile is not None else "",
2537
2542
  }
2538
2543
  )
2539
- i += 1
2544
+ assert len(members) < _MAX_TRIBE_MEMBERS, "tribe member list exceeded bound"
2540
2545
  alliances: list[int] = []
2541
- j = 0
2542
- while True:
2546
+ for j in range(_MAX_TRIBE_MEMBERS):
2543
2547
  val = _prop(obj, "TribeAlliances", index=j)
2544
2548
  if val is None:
2545
2549
  break
2546
2550
  alliances.append(_int(val))
2547
- j += 1
2551
+ assert len(alliances) < _MAX_TRIBE_MEMBERS, "tribe alliance list exceeded bound"
2548
2552
  c = counts.get(tribe_id, {})
2549
2553
  data: dict[str, t.Any] = {
2550
2554
  "tribeid": tribe_id,
@@ -2569,14 +2573,15 @@ def _tribe_object_logs(obj: t.Any) -> list[str]:
2569
2573
  if isinstance(log_val, list):
2570
2574
  return [e for e in log_val if isinstance(e, str) and e.strip()]
2571
2575
  out: list[str] = []
2572
- k = 0
2573
- while True:
2576
+ entries = 0
2577
+ for k in range(_MAX_TRIBE_MEMBERS):
2574
2578
  v = _prop(obj, "TribeLog", index=k)
2575
2579
  if v is None:
2576
2580
  break
2581
+ entries += 1
2577
2582
  if isinstance(v, str) and v.strip():
2578
2583
  out.append(v)
2579
- k += 1
2584
+ assert entries < _MAX_TRIBE_MEMBERS, "tribe log list exceeded bound"
2580
2585
  return out
2581
2586
 
2582
2587
 
@@ -34,6 +34,7 @@ from uuid import UUID
34
34
  from ..common.binary_reader import BinaryReader, guid_str_le
35
35
  from ..common.exceptions import ArkParseError, CorruptDataError
36
36
  from ..common.normalization import normalize_indexed_data, normalize_indexed_list
37
+ from ..common.types import CRYOPOD_CLASS_PATTERNS
37
38
  from ..data_models import CryopodCreature
38
39
  from ..game_objects.container import GameObjectContainer
39
40
  from ..game_objects.game_object import MAX_OBJECT_COUNT, GameObject
@@ -428,13 +429,10 @@ class WorldSave:
428
429
  """Return creature nest objects (wyvern, drake, etc.)."""
429
430
  return self.container.get_nests()
430
431
 
431
- # Class-name fragments that indicate a creature-storage item. Mirrors
432
- # data_models.UploadedItem.is_cryopod (which matches against blueprint
433
- # paths); the in-world version matches against the GameObject's
434
- # ``class_name`` directly.
435
- _CRYOPOD_PATTERNS: t.ClassVar[tuple[str, ...]] = (
436
- "Cryopod", "SoulTrap", "Vivarium", "DinoBall",
437
- )
432
+ # Shared with data_models.UploadedItem.is_cryopod (blueprint paths) and
433
+ # the export inventory filter; the in-world version matches against the
434
+ # GameObject's ``class_name`` directly.
435
+ _CRYOPOD_PATTERNS: t.ClassVar[tuple[str, ...]] = CRYOPOD_CLASS_PATTERNS
438
436
 
439
437
  def iter_cryopod_creatures(self) -> t.Iterator[tuple[GameObject, CryopodCreature]]:
440
438
  """Yield ``(item_obj, CryopodCreature)`` for every *filled* cryopod
@@ -7,6 +7,7 @@ Game objects represent creatures, items, structures, players, and other entities
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import logging
10
11
  import re
11
12
  import typing as t
12
13
  from collections import defaultdict
@@ -16,6 +17,8 @@ from ..common.exceptions import CorruptDataError
16
17
  from ..properties.registry import read_properties, read_property
17
18
  from .location import LocationData
18
19
 
20
+ logger = logging.getLogger(__name__)
21
+
19
22
  if t.TYPE_CHECKING:
20
23
  from ..common.binary_reader import BinaryReader
21
24
  from ..properties.base import Property
@@ -420,11 +423,18 @@ class GameObject:
420
423
  if prop is None:
421
424
  break
422
425
  properties.append(prop)
423
- except Exception:
426
+ except Exception as exc:
424
427
  self.properties = properties
425
428
  self._props_loaded = True
426
429
  if next_object is None:
427
430
  raise
431
+ logger.debug(
432
+ "property parse stopped early for %s after %d properties, "
433
+ "remainder kept as extra_data: %s",
434
+ self.class_name,
435
+ len(properties),
436
+ exc,
437
+ )
428
438
 
429
439
  next_offset = properties_block_offset + next_object.properties_offset
430
440
  remaining = next_offset - reader.position
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.7.0
3
+ Version: 0.7.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
@@ -681,6 +681,15 @@ Schema policy:
681
681
 
682
682
  ASA worldsave property layouts differ between **v13** (`TheIsland_WP` and older single-player saves, legacy `AsaSavegameToolkit`-style `dataSize + position + typeRef + byte` body) and **v14+** (current production ASA, marker-based body). Both are parsed by version-aware property readers; `WorldSave.version` is the source of truth.
683
683
 
684
+ ### Parse diagnostics (logging)
685
+
686
+ The parser logs through the standard `logging` module on the `arkparser.*` logger hierarchy. At DEBUG level it reports recoverable parse anomalies, for example ASE objects whose property block terminates early (the run of successfully parsed properties is kept and the remaining bytes are preserved on `GameObject.extra_data`):
687
+
688
+ ```python
689
+ import logging
690
+ logging.getLogger("arkparser").setLevel(logging.DEBUG)
691
+ ```
692
+
684
693
  ### Known limitation: ASA cryopod property blocks are partially decoded
685
694
 
686
695
  ASA cryopods (`PrimalItem_WeaponEmptyCryopod_C`, `SoulTrap`, `Vivarium`, `DinoBall`) store the embedded creature snapshot inside the item's `CustomItemDatas.CustomDataBytes.ByteArrays[0]` as a **zlib + custom-RLE compressed `AsaDataStore` blob**. The parser surfaces the snapshot via the simpler `CustomDataStrings` / `CustomDataFloats` accessors, exposing **species, tamed name, level, color regions, and `CurrentStatusValues[0..11]`** on each cryopodded creature.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.7.0"
7
+ version = "0.7.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"
@@ -19,7 +19,11 @@ with ``cryo=True``. Two regressions tested here:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
+ import typing as t
23
+
22
24
  from arkparser.data_models import CryopodCreature
25
+ from arkparser.export import export_tamed
26
+ from arkparser.game_objects.location import LocationData
23
27
 
24
28
 
25
29
  def _asa_blob(current: list[float], max_: list[float]) -> dict[str, object]:
@@ -97,6 +101,78 @@ def test_asa_cryopod_display_name_parses_level_and_species() -> None:
97
101
  assert cryo.species == "Argentavis"
98
102
 
99
103
 
104
+ class FakeObject:
105
+ """Minimal GameObject stand-in for exporter graph tests."""
106
+
107
+ def __init__(
108
+ self,
109
+ obj_id: int,
110
+ class_name: str,
111
+ props: dict[str, t.Any] | None = None,
112
+ location: LocationData | None = None,
113
+ is_item: bool = False,
114
+ ) -> None:
115
+ self.id = obj_id
116
+ self.guid = ""
117
+ self.names: list[str] = []
118
+ self.class_name = class_name
119
+ self.location = location
120
+ self.is_item = is_item
121
+ self.properties: list[t.Any] = []
122
+ self.components: dict[str, t.Any] = {}
123
+ self.props = props or {}
124
+
125
+ def get_property_value(self, name: str, default: t.Any = None, index: int | None = None) -> t.Any:
126
+ key = name if not index else f"{name}_{index}"
127
+ val = self.props.get(key)
128
+ return default if val is None else val
129
+
130
+
131
+ class FakeSave:
132
+ """Save stand-in exposing just what export_tamed needs."""
133
+
134
+ is_asa = True
135
+
136
+ def __init__(self, objects: list[FakeObject], cryos: list[tuple[FakeObject, CryopodCreature]]) -> None:
137
+ self.objects = objects
138
+ self.cryos = cryos
139
+
140
+ def get_tamed_creatures(self) -> list[FakeObject]:
141
+ return []
142
+
143
+ def iter_cryopod_creatures(self) -> t.Iterator[tuple[FakeObject, CryopodCreature]]:
144
+ yield from self.cryos
145
+
146
+
147
+ def test_cryopod_inherits_owning_container_location() -> None:
148
+ """ASA cryopod items live in inventories, not the world: they carry no
149
+ actor transform, so ``item_obj.location`` is None. The exported record
150
+ must fall back to the owning container's (cryofridge / pawn) location
151
+ instead of emitting 0/0/0."""
152
+ fridge = FakeObject(
153
+ 10,
154
+ "CryoFridge_C",
155
+ props={
156
+ "MyInventoryComponent": (None, 11),
157
+ "TargetingTeam": 1500000001,
158
+ "TribeName": "Frisky Dingo",
159
+ },
160
+ location=LocationData(x=10000.0, y=-20000.0, z=300.0),
161
+ )
162
+ inventory = FakeObject(11, "PrimalInventoryBP_C", props={"InventoryItems": [(None, 12)]})
163
+ pod = FakeObject(12, "PrimalItem_WeaponEmptyCryopod_C", is_item=True)
164
+ cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob([0.0] * 11, [0.0] * 11))
165
+ assert cryo is not None
166
+ save = FakeSave([fridge, inventory, pod], [(pod, cryo)])
167
+
168
+ records = export_tamed(save, None)
169
+ assert len(records) == 1
170
+ record = records[0]
171
+ assert record["cryo"] is True
172
+ assert record["tribe"] == "Frisky Dingo"
173
+ assert record["ccc"] == "10000.0 -20000.0 300.0"
174
+
175
+
100
176
  def test_ase_cryopod_keeps_twelve_stat_layout() -> None:
101
177
  """ASE cryopod blobs carry 12 current stats + 12 max stats (25 floats).
102
178
  The ASA fix must not break the ASE path: walking all 12 names is the
File without changes
File without changes
File without changes