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.
- {arkparser-0.7.0 → arkparser-0.7.2}/PKG-INFO +10 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/README.md +9 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/__init__.py +1 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/types.py +11 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/data_models.py +5 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/export.py +24 -19
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/world_save.py +5 -7
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/game_object.py +11 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/PKG-INFO +10 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/pyproject.toml +1 -1
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cryopod_export.py +76 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/LICENSE +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/__init__.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/binary_reader.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/exceptions.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/map_config.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/normalization.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/common/version_detection.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/__init__.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/base.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/cloud_inventory.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/profile.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/files/tribe.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/__init__.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/container.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/game_objects/location.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/__init__.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/base.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/byte_property.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/compound.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/primitives.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/properties/registry.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/__init__.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/base.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/colors.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/misc.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/property_list.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/registry.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser/structs/vectors.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/SOURCES.txt +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/dependency_links.txt +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/requires.txt +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/arkparser.egg-info/top_level.txt +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/setup.cfg +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_asa_header_position.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_asa_name_table.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_ase_cluster_drift.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_binary_reader.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_binary_reader_layouts.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cloud_export.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_cloud_inventory.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_current_stats.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_data_models.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_export.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_game_objects.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_golden_exports.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_lazy_properties.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_profile.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_review_fixes.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_tribe.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_v13_property_layouts.py +0 -0
- {arkparser-0.7.0 → arkparser-0.7.2}/tests/test_version_detection.py +0 -0
- {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.
|
|
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.
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2544
|
+
assert len(members) < _MAX_TRIBE_MEMBERS, "tribe member list exceeded bound"
|
|
2540
2545
|
alliances: list[int] = []
|
|
2541
|
-
j
|
|
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
|
-
|
|
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
|
-
|
|
2573
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
432
|
-
#
|
|
433
|
-
#
|
|
434
|
-
|
|
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.
|
|
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.
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|