arkparser 0.4.1__tar.gz → 0.4.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.4.1 → arkparser-0.4.2}/PKG-INFO +6 -6
- {arkparser-0.4.1 → arkparser-0.4.2}/README.md +5 -5
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/__init__.py +1 -1
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/export.py +159 -37
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser.egg-info/PKG-INFO +6 -6
- {arkparser-0.4.1 → arkparser-0.4.2}/pyproject.toml +1 -1
- arkparser-0.4.2/tests/test_cloud_export.py +98 -0
- arkparser-0.4.1/tests/test_cloud_export.py +0 -45
- {arkparser-0.4.1 → arkparser-0.4.2}/LICENSE +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/__init__.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/binary_reader.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/exceptions.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/map_config.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/normalization.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/types.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/common/version_detection.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/data_models.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/__init__.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/base.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/cloud_inventory.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/profile.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/tribe.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/files/world_save.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/game_objects/__init__.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/game_objects/container.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/game_objects/game_object.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/game_objects/location.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/__init__.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/base.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/byte_property.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/compound.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/primitives.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/properties/registry.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/__init__.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/base.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/colors.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/misc.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/property_list.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/registry.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser/structs/vectors.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser.egg-info/SOURCES.txt +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser.egg-info/dependency_links.txt +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser.egg-info/requires.txt +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/arkparser.egg-info/top_level.txt +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/setup.cfg +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_asa_header_position.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_binary_reader.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_binary_reader_layouts.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_cloud_inventory.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_cryopod_export.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_current_stats.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_data_models.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_export.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_game_objects.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_profile.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_tribe.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_v13_property_layouts.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.2}/tests/test_version_detection.py +0 -0
- {arkparser-0.4.1 → arkparser-0.4.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.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -410,11 +410,11 @@ data = export_all(save, map_config, cluster="path/to/cluster")
|
|
|
410
410
|
# data["ASV_Tamed"] - now includes cluster cryopod tames (cryo=True)
|
|
411
411
|
# data["ASV_Players"] - each player's inventory now contains their
|
|
412
412
|
# uploaded items (entries tagged "uploaded": true),
|
|
413
|
-
# matched by cloud-file stem
|
|
414
|
-
#
|
|
413
|
+
# matched by cloud-file stem == the player's
|
|
414
|
+
# .arkprofile filename stem.
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
-
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file
|
|
417
|
+
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file and the player's `.arkprofile` share a stem — the Steam id on ASE, the hex platform UUID on ASA — so the join is keyed on the profile's source filename stem. (`Profile.unique_id` equals that stem only on ASE; on ASA it is the numeric net id, not the UUID filename, so loading profiles from real file paths — which sets `Profile.source_path` — is required for the ASA splice.)
|
|
418
418
|
|
|
419
419
|
Pre-loaded `CloudInventory` instances also work (`cluster=[inv1, inv2, ...]`).
|
|
420
420
|
|
|
@@ -436,7 +436,7 @@ data = export_cloud_inventory(cloud)
|
|
|
436
436
|
# }
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
-
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `
|
|
439
|
+
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `uploadedTime` (ISO 8601 with UTC offset), and item-class-specific stats (`durability_max`, `damage`, `armor`, `hypo`, `hyper`, `crafter`, `crafter_tribe`, `skill_bonus`, `loaded_ammo`, `quality`, `rating`, `c0`..`c5` paint regions, `drop_location`, `egg_*` for fertilized eggs, `dino_*` for cryopod items, etc). Default / unset fields are filtered, NaN floats dropped, `"Unknown"` crafter strings nulled.
|
|
440
440
|
|
|
441
441
|
The low-level helpers are also available:
|
|
442
442
|
- `export_cluster_uploads(cluster_invs, map_config=None) -> list[dict]` — tamed-shape records (incl. cryopod-as-item dinos) across the supplied cloud files
|
|
@@ -589,7 +589,7 @@ All four expose `from_*` constructors and `to_dict()` for serialization.
|
|
|
589
589
|
|---|---|---|
|
|
590
590
|
| `export_tamed(save, map_config=None)` | `list[dict]` | `ASV_Tamed` records |
|
|
591
591
|
| `export_wild(save, map_config=None)` | `list[dict]` | `ASV_Wild` records |
|
|
592
|
-
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by
|
|
592
|
+
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by cloud-file stem == the player's `.arkprofile` filename stem) |
|
|
593
593
|
| `export_tribes(save)` | `list[dict]` | `ASV_Tribes` records |
|
|
594
594
|
| `export_structures(save, map_config=None)` | `list[dict]` | `ASV_Structures` records |
|
|
595
595
|
| `export_tribe_logs(save)` | `list[dict]` | `ASV_TribeLogs` records |
|
|
@@ -376,11 +376,11 @@ data = export_all(save, map_config, cluster="path/to/cluster")
|
|
|
376
376
|
# data["ASV_Tamed"] - now includes cluster cryopod tames (cryo=True)
|
|
377
377
|
# data["ASV_Players"] - each player's inventory now contains their
|
|
378
378
|
# uploaded items (entries tagged "uploaded": true),
|
|
379
|
-
# matched by cloud-file stem
|
|
380
|
-
#
|
|
379
|
+
# matched by cloud-file stem == the player's
|
|
380
|
+
# .arkprofile filename stem.
|
|
381
381
|
```
|
|
382
382
|
|
|
383
|
-
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file
|
|
383
|
+
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file and the player's `.arkprofile` share a stem — the Steam id on ASE, the hex platform UUID on ASA — so the join is keyed on the profile's source filename stem. (`Profile.unique_id` equals that stem only on ASE; on ASA it is the numeric net id, not the UUID filename, so loading profiles from real file paths — which sets `Profile.source_path` — is required for the ASA splice.)
|
|
384
384
|
|
|
385
385
|
Pre-loaded `CloudInventory` instances also work (`cluster=[inv1, inv2, ...]`).
|
|
386
386
|
|
|
@@ -402,7 +402,7 @@ data = export_cloud_inventory(cloud)
|
|
|
402
402
|
# }
|
|
403
403
|
```
|
|
404
404
|
|
|
405
|
-
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `
|
|
405
|
+
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `uploadedTime` (ISO 8601 with UTC offset), and item-class-specific stats (`durability_max`, `damage`, `armor`, `hypo`, `hyper`, `crafter`, `crafter_tribe`, `skill_bonus`, `loaded_ammo`, `quality`, `rating`, `c0`..`c5` paint regions, `drop_location`, `egg_*` for fertilized eggs, `dino_*` for cryopod items, etc). Default / unset fields are filtered, NaN floats dropped, `"Unknown"` crafter strings nulled.
|
|
406
406
|
|
|
407
407
|
The low-level helpers are also available:
|
|
408
408
|
- `export_cluster_uploads(cluster_invs, map_config=None) -> list[dict]` — tamed-shape records (incl. cryopod-as-item dinos) across the supplied cloud files
|
|
@@ -555,7 +555,7 @@ All four expose `from_*` constructors and `to_dict()` for serialization.
|
|
|
555
555
|
|---|---|---|
|
|
556
556
|
| `export_tamed(save, map_config=None)` | `list[dict]` | `ASV_Tamed` records |
|
|
557
557
|
| `export_wild(save, map_config=None)` | `list[dict]` | `ASV_Wild` records |
|
|
558
|
-
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by
|
|
558
|
+
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by cloud-file stem == the player's `.arkprofile` filename stem) |
|
|
559
559
|
| `export_tribes(save)` | `list[dict]` | `ASV_Tribes` records |
|
|
560
560
|
| `export_structures(save, map_config=None)` | `list[dict]` | `ASV_Structures` records |
|
|
561
561
|
| `export_tribe_logs(save)` | `list[dict]` | `ASV_TribeLogs` records |
|
|
@@ -655,6 +655,7 @@ _EGG_ONLY_FIELDS: frozenset[str] = frozenset({
|
|
|
655
655
|
"egg_random_mutations_female",
|
|
656
656
|
"egg_random_mutations_male",
|
|
657
657
|
"egg_number_mutations_applied",
|
|
658
|
+
"egg_number_of_mutations_applied", # "NumberOf" variant (matches EggNumberOfLevelUpPointsApplied naming)
|
|
658
659
|
"egg_dino_gene_traits",
|
|
659
660
|
})
|
|
660
661
|
|
|
@@ -711,7 +712,10 @@ def _is_default(key: str, value: t.Any) -> bool:
|
|
|
711
712
|
a == b for a, b in zip(value, default)
|
|
712
713
|
)
|
|
713
714
|
if isinstance(default, dict) and isinstance(value, dict):
|
|
714
|
-
|
|
715
|
+
# Vector structs are sometimes keyed X/Y/Z (ASA) instead of x/y/z;
|
|
716
|
+
# compare case-insensitively so a zero drop_location still prunes.
|
|
717
|
+
lowered = {str(k).lower(): v for k, v in value.items()}
|
|
718
|
+
return all(lowered.get(k, 0) == v for k, v in default.items())
|
|
715
719
|
return value == default
|
|
716
720
|
|
|
717
721
|
|
|
@@ -732,7 +736,10 @@ def _flatten_color_array(value: t.Any) -> dict[str, int]:
|
|
|
732
736
|
return out
|
|
733
737
|
for idx, v in iterable:
|
|
734
738
|
if 0 <= idx < 6 and v:
|
|
735
|
-
|
|
739
|
+
try:
|
|
740
|
+
out[f"c{idx}"] = int(v)
|
|
741
|
+
except (TypeError, ValueError):
|
|
742
|
+
continue # struct-valued color (LinearColor) etc; not a palette index
|
|
736
743
|
return out
|
|
737
744
|
|
|
738
745
|
# ARK universal 8-slot ItemStatValues map. Each slot is raw uint16; the
|
|
@@ -778,14 +785,27 @@ _STAT_NAME_ALIASES: dict[str, str] = {
|
|
|
778
785
|
|
|
779
786
|
|
|
780
787
|
def _combine_item_id(value: t.Any) -> str | None:
|
|
781
|
-
"""Combine ItemID struct ``{ItemID1, ItemID2}`` to single string.
|
|
788
|
+
"""Combine ItemID struct ``{ItemID1, ItemID2}`` to single string.
|
|
789
|
+
|
|
790
|
+
Uses explicit ``is None`` fallback (not ``or``) so a legitimate
|
|
791
|
+
``ItemID1 == 0`` is not silently replaced by an ``ItemID1_0`` variant
|
|
792
|
+
key. Non-numeric / corrupt id components return ``None`` rather than
|
|
793
|
+
raising and aborting the whole inventory export.
|
|
794
|
+
"""
|
|
782
795
|
if not isinstance(value, dict):
|
|
783
796
|
return None
|
|
784
|
-
id1 = value.get("ItemID1")
|
|
785
|
-
|
|
797
|
+
id1 = value.get("ItemID1")
|
|
798
|
+
if id1 is None:
|
|
799
|
+
id1 = value.get("ItemID1_0")
|
|
800
|
+
id2 = value.get("ItemID2")
|
|
801
|
+
if id2 is None:
|
|
802
|
+
id2 = value.get("ItemID2_0")
|
|
786
803
|
if id1 is None and id2 is None:
|
|
787
804
|
return None
|
|
788
|
-
|
|
805
|
+
try:
|
|
806
|
+
return f"{int(id1 or 0)}_{int(id2 or 0)}"
|
|
807
|
+
except (TypeError, ValueError):
|
|
808
|
+
return None
|
|
789
809
|
|
|
790
810
|
|
|
791
811
|
def _normalize_stat_value(name: str, value: t.Any) -> t.Any:
|
|
@@ -832,14 +852,40 @@ def _apply_stat_aliases(
|
|
|
832
852
|
if _is_default(alias, val):
|
|
833
853
|
continue
|
|
834
854
|
out[alias] = val
|
|
855
|
+
out.update(_expand_stat_slots(raw_slot_values))
|
|
856
|
+
return out
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def _expand_stat_slots(raw_slot_values: t.Any) -> dict[str, t.Any]:
|
|
860
|
+
"""Map raw ItemStatValues to named slots, accepting either input shape.
|
|
861
|
+
|
|
862
|
+
Two shapes occur in practice:
|
|
863
|
+
|
|
864
|
+
- GameObject inventory path → a sparse ``{index: value}`` dict (see
|
|
865
|
+
:func:`_indexed_property_map`, which preserves the index even for a
|
|
866
|
+
single populated slot).
|
|
867
|
+
- Cloud / uploaded path → a dense 8-element ``list`` indexed 0..7
|
|
868
|
+
(``normalize_indexed_data`` collapses the indexed property to a list).
|
|
869
|
+
|
|
870
|
+
Anything else (a bare scalar from an upstream single-entry collapse that
|
|
871
|
+
lost its index) carries no recoverable slot and is ignored. Raw 0 means
|
|
872
|
+
"no stat roll", so zero slots are skipped.
|
|
873
|
+
"""
|
|
874
|
+
pairs: t.Iterable[tuple[t.Any, t.Any]]
|
|
835
875
|
if isinstance(raw_slot_values, dict):
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
876
|
+
pairs = raw_slot_values.items()
|
|
877
|
+
elif isinstance(raw_slot_values, list):
|
|
878
|
+
pairs = enumerate(raw_slot_values)
|
|
879
|
+
else:
|
|
880
|
+
return {}
|
|
881
|
+
out: dict[str, t.Any] = {}
|
|
882
|
+
for k, v in pairs:
|
|
883
|
+
try:
|
|
884
|
+
idx = int(k)
|
|
885
|
+
except (TypeError, ValueError):
|
|
886
|
+
continue
|
|
887
|
+
if 0 <= idx < len(_ITEM_STAT_SLOT_NAMES) and v:
|
|
888
|
+
out[_ITEM_STAT_SLOT_NAMES[idx]] = v
|
|
843
889
|
return out
|
|
844
890
|
|
|
845
891
|
_PASCAL_SNAKE_RE_1 = re.compile(r"(.)([A-Z][a-z]+)")
|
|
@@ -852,6 +898,27 @@ def _pascal_to_snake(name: str) -> str:
|
|
|
852
898
|
return _PASCAL_SNAKE_RE_2.sub(r"\1_\2", s).lower()
|
|
853
899
|
|
|
854
900
|
|
|
901
|
+
def _indexed_property_map(item_obj: t.Any, prop_name: str) -> dict[int, t.Any]:
|
|
902
|
+
"""Read a multi-index property as ``{index: value}`` straight from the object.
|
|
903
|
+
|
|
904
|
+
``_serialize_properties`` collapses a single-entry non-Byte property group
|
|
905
|
+
to a bare scalar, which destroys the slot index for indexed arrays such as
|
|
906
|
+
``ItemStatValues`` / ``ItemColorID`` (an item with only one populated stat
|
|
907
|
+
slot would otherwise lose both the value and which slot it was). Reading the
|
|
908
|
+
raw ``Property`` list keeps every index, mirroring :func:`_colors`.
|
|
909
|
+
"""
|
|
910
|
+
getter = getattr(item_obj, "get_properties_by_name", None)
|
|
911
|
+
if not callable(getter):
|
|
912
|
+
return {}
|
|
913
|
+
out: dict[int, t.Any] = {}
|
|
914
|
+
for prop in getter(prop_name):
|
|
915
|
+
idx = int(getattr(prop, "index", 0) or 0)
|
|
916
|
+
val = getattr(prop, "value", None)
|
|
917
|
+
if val is not None:
|
|
918
|
+
out[idx] = val
|
|
919
|
+
return out
|
|
920
|
+
|
|
921
|
+
|
|
855
922
|
def _item_stats_dict(item_obj: t.Any, item_class: str = "") -> dict[str, t.Any]:
|
|
856
923
|
"""Surface every parseable property on an inventory item as snake_case.
|
|
857
924
|
|
|
@@ -880,6 +947,16 @@ def _item_stats_dict(item_obj: t.Any, item_class: str = "") -> dict[str, t.Any]:
|
|
|
880
947
|
continue
|
|
881
948
|
snake = _pascal_to_snake(name)
|
|
882
949
|
pre[snake] = _normalize_stat_value(snake, value)
|
|
950
|
+
# ItemStatValues / ItemColorID are indexed UInt16 arrays. _serialize_properties
|
|
951
|
+
# collapses a single populated slot to a bare scalar (losing the index), so
|
|
952
|
+
# re-read them straight from the object to preserve {index: value}; a
|
|
953
|
+
# single-stat saddle/weapon would otherwise drop its only roll.
|
|
954
|
+
isv = _indexed_property_map(item_obj, "ItemStatValues")
|
|
955
|
+
if isv:
|
|
956
|
+
pre["item_stat_values"] = isv
|
|
957
|
+
color = _indexed_property_map(item_obj, "ItemColorID")
|
|
958
|
+
if color:
|
|
959
|
+
pre["item_color_id"] = color
|
|
883
960
|
return _apply_stat_aliases(pre, item_class=item_class)
|
|
884
961
|
|
|
885
962
|
|
|
@@ -1231,6 +1308,38 @@ def _cryo_props_to_synthetic(
|
|
|
1231
1308
|
return actor, status
|
|
1232
1309
|
|
|
1233
1310
|
|
|
1311
|
+
def _cryo_tamed_record(
|
|
1312
|
+
cryo: CryopodCreature,
|
|
1313
|
+
map_config: MapConfig | None,
|
|
1314
|
+
empty_lookup: dict[t.Any, t.Any],
|
|
1315
|
+
) -> dict[str, t.Any]:
|
|
1316
|
+
"""Decode one cryopod blob into a tamed record flagged ``cryo=True``."""
|
|
1317
|
+
actor, status = _cryo_props_to_synthetic(cryo)
|
|
1318
|
+
record = _tamed_dict(actor, status, empty_lookup, map_config)
|
|
1319
|
+
record["cryo"] = True
|
|
1320
|
+
return record
|
|
1321
|
+
|
|
1322
|
+
|
|
1323
|
+
def _append_unique_tame(
|
|
1324
|
+
out: list[dict[str, t.Any]],
|
|
1325
|
+
seen_ids: set[int],
|
|
1326
|
+
record: dict[str, t.Any],
|
|
1327
|
+
) -> None:
|
|
1328
|
+
"""Append ``record`` unless a tame with the same non-zero dino id is present.
|
|
1329
|
+
|
|
1330
|
+
Cluster cryopods can land in either ``ArkTamedDinosData`` or ``ArkItems``;
|
|
1331
|
+
a creature present in both would otherwise be emitted twice. Records whose
|
|
1332
|
+
dino id is 0 (unresolved, common on ASA partial decodes) are never deduped
|
|
1333
|
+
so distinct unidentified tames are all preserved.
|
|
1334
|
+
"""
|
|
1335
|
+
did = _int(record.get("dinoid"))
|
|
1336
|
+
if did and did in seen_ids:
|
|
1337
|
+
return
|
|
1338
|
+
if did:
|
|
1339
|
+
seen_ids.add(did)
|
|
1340
|
+
out.append(record)
|
|
1341
|
+
|
|
1342
|
+
|
|
1234
1343
|
def export_cluster_uploads(
|
|
1235
1344
|
cluster_inventories: t.Iterable[CloudInventory],
|
|
1236
1345
|
map_config: MapConfig | None = None,
|
|
@@ -1258,6 +1367,7 @@ def export_cluster_uploads(
|
|
|
1258
1367
|
always stored in cryopods).
|
|
1259
1368
|
"""
|
|
1260
1369
|
out: list[dict[str, t.Any]] = []
|
|
1370
|
+
seen_ids: set[int] = set()
|
|
1261
1371
|
empty_lookup: dict[t.Any, t.Any] = {}
|
|
1262
1372
|
for inv in cluster_inventories:
|
|
1263
1373
|
my_ark_data = normalize_indexed_data(inv.get_property_value("MyArkData"))
|
|
@@ -1281,10 +1391,7 @@ def export_cluster_uploads(
|
|
|
1281
1391
|
cryo = CryopodCreature.from_cryopod_bytes(byte_arr)
|
|
1282
1392
|
if cryo is None:
|
|
1283
1393
|
continue
|
|
1284
|
-
|
|
1285
|
-
record = _tamed_dict(actor, status, empty_lookup, map_config)
|
|
1286
|
-
record["cryo"] = True
|
|
1287
|
-
out.append(record)
|
|
1394
|
+
_append_unique_tame(out, seen_ids, _cryo_tamed_record(cryo, map_config, empty_lookup))
|
|
1288
1395
|
# Cryopods uploaded as items (rare but present, especially in ASA
|
|
1289
1396
|
# tribute transfers) embed a CryopodCreature in CustomItemDatas.
|
|
1290
1397
|
for item in inv.uploaded_items:
|
|
@@ -1293,10 +1400,7 @@ def export_cluster_uploads(
|
|
|
1293
1400
|
cryo = item.cryopod_creature
|
|
1294
1401
|
if cryo is None:
|
|
1295
1402
|
continue
|
|
1296
|
-
|
|
1297
|
-
record = _tamed_dict(actor, status, empty_lookup, map_config)
|
|
1298
|
-
record["cryo"] = True
|
|
1299
|
-
out.append(record)
|
|
1403
|
+
_append_unique_tame(out, seen_ids, _cryo_tamed_record(cryo, map_config, empty_lookup))
|
|
1300
1404
|
return out
|
|
1301
1405
|
|
|
1302
1406
|
|
|
@@ -1349,12 +1453,14 @@ def _uploaded_item_dict(item: t.Any) -> dict[str, t.Any]:
|
|
|
1349
1453
|
entry.update(_apply_stat_aliases(pre, item_class=class_name))
|
|
1350
1454
|
upload_time = item.upload_time
|
|
1351
1455
|
if upload_time:
|
|
1456
|
+
# Key name matches the legacy ASV item schema (uploadedTime), which
|
|
1457
|
+
# downstream consumers use as the "is uploaded" discriminator.
|
|
1352
1458
|
try:
|
|
1353
|
-
entry["
|
|
1459
|
+
entry["uploadedTime"] = dt.datetime.fromtimestamp(
|
|
1354
1460
|
float(upload_time), tz=dt.timezone.utc
|
|
1355
1461
|
).isoformat()
|
|
1356
|
-
except (OverflowError, OSError, ValueError):
|
|
1357
|
-
entry["
|
|
1462
|
+
except (OverflowError, OSError, ValueError, TypeError):
|
|
1463
|
+
entry["uploadedTime"] = upload_time
|
|
1358
1464
|
return entry
|
|
1359
1465
|
|
|
1360
1466
|
|
|
@@ -1383,9 +1489,10 @@ def export_cluster_items(
|
|
|
1383
1489
|
) -> list[dict[str, t.Any]]:
|
|
1384
1490
|
"""Export every uploaded item across the supplied cloud inventories.
|
|
1385
1491
|
|
|
1386
|
-
Each item carries its
|
|
1387
|
-
|
|
1388
|
-
etc)
|
|
1492
|
+
Each item carries its snake_case stats flattened in at the top level
|
|
1493
|
+
(``itemId``/``qty``/``blueprint`` plus ``armor``/``durability_max``/
|
|
1494
|
+
``damage``/``rating``/``crafter`` etc) surfacing the full
|
|
1495
|
+
``ArkTributeItem`` payload. Cryopod items also include ``dino_*`` keys with the embedded
|
|
1389
1496
|
creature's identifying info; the dino's full stats record is emitted
|
|
1390
1497
|
by :func:`export_cluster_uploads`.
|
|
1391
1498
|
"""
|
|
@@ -1409,8 +1516,8 @@ def export_cloud_inventory(
|
|
|
1409
1516
|
- ``ASV_Tamed``: every dino in the file (both ``ArkTamedDinosData``
|
|
1410
1517
|
entries and cryopod items in ``ArkItems``), shaped like a regular
|
|
1411
1518
|
tamed record so consumers can reuse existing rendering.
|
|
1412
|
-
- ``ASV_Items``: every uploaded item, snake_case
|
|
1413
|
-
|
|
1519
|
+
- ``ASV_Items``: every uploaded item, snake_case stats flattened in
|
|
1520
|
+
at the top level (see :func:`export_cluster_items`).
|
|
1414
1521
|
|
|
1415
1522
|
Useful for inspecting a single user's cluster transfer file in
|
|
1416
1523
|
isolation, without scanning a whole cluster directory.
|
|
@@ -1623,9 +1730,11 @@ def _cluster_items_by_xuid(
|
|
|
1623
1730
|
"""Group uploaded items by cloud-file stem (= player's unique_id / xuid).
|
|
1624
1731
|
|
|
1625
1732
|
Each cluster file is named after the owning player's Steam id (ASE) or
|
|
1626
|
-
platform UUID (ASA).
|
|
1627
|
-
|
|
1628
|
-
|
|
1733
|
+
platform UUID (ASA). That same stem names the player's ``.arkprofile``,
|
|
1734
|
+
so :func:`export_players` joins on the profile's source filename stem
|
|
1735
|
+
(see there) — a stable key on both platforms without cracking any
|
|
1736
|
+
in-file ownership data. (``Profile.unique_id`` equals the stem only on
|
|
1737
|
+
ASE; on ASA it is the numeric net id, not the UUID filename.)
|
|
1629
1738
|
"""
|
|
1630
1739
|
out: dict[str, list[dict[str, t.Any]]] = {}
|
|
1631
1740
|
for inv in cluster_inventories:
|
|
@@ -1658,8 +1767,19 @@ def export_players(
|
|
|
1658
1767
|
results: list[dict[str, t.Any]] = []
|
|
1659
1768
|
for entry in profiles:
|
|
1660
1769
|
record: dict[str, t.Any]
|
|
1770
|
+
# Keys to match against the cloud-file stem, in priority order.
|
|
1771
|
+
join_keys: list[str] = []
|
|
1661
1772
|
if isinstance(entry, Profile):
|
|
1662
1773
|
record = _player_from_profile(entry, save, pawn_status_by_id)
|
|
1774
|
+
# The cloud file and the .arkprofile for one player share a stem:
|
|
1775
|
+
# the Steam id on ASE, the hex platform UUID on ASA. The profile's
|
|
1776
|
+
# own source filename stem is therefore the reliable cross-platform
|
|
1777
|
+
# join key. ``unique_id`` only equals it on ASE (on ASA it is the
|
|
1778
|
+
# numeric net id, not the UUID filename), so it is a fallback.
|
|
1779
|
+
if entry.source_path is not None and entry.source_path.stem:
|
|
1780
|
+
join_keys.append(entry.source_path.stem)
|
|
1781
|
+
if entry.unique_id:
|
|
1782
|
+
join_keys.append(entry.unique_id)
|
|
1663
1783
|
else:
|
|
1664
1784
|
profile_obj = getattr(entry, "profile", None)
|
|
1665
1785
|
if profile_obj is None and getattr(entry, "objects", None):
|
|
@@ -1673,17 +1793,19 @@ def export_players(
|
|
|
1673
1793
|
status_obj = o
|
|
1674
1794
|
break
|
|
1675
1795
|
record = _player_from_object(profile_obj, status_obj, lookup, map_config, save)
|
|
1796
|
+
sid = record.get("steamid")
|
|
1797
|
+
if sid:
|
|
1798
|
+
join_keys.append(str(sid))
|
|
1676
1799
|
# Splice cluster-uploaded items into the player's inventory list,
|
|
1677
1800
|
# tagged ``uploaded: true`` so consumers can distinguish them from
|
|
1678
|
-
# carried items.
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
if steam_id and steam_id in cluster_items:
|
|
1801
|
+
# carried items.
|
|
1802
|
+
spliced = next((cluster_items[k] for k in join_keys if k in cluster_items), None)
|
|
1803
|
+
if spliced:
|
|
1682
1804
|
inv_list = record.get("inventory")
|
|
1683
1805
|
if not isinstance(inv_list, list):
|
|
1684
1806
|
inv_list = []
|
|
1685
1807
|
record["inventory"] = inv_list
|
|
1686
|
-
inv_list.extend(
|
|
1808
|
+
inv_list.extend(spliced)
|
|
1687
1809
|
results.append(record)
|
|
1688
1810
|
return results
|
|
1689
1811
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arkparser
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -410,11 +410,11 @@ data = export_all(save, map_config, cluster="path/to/cluster")
|
|
|
410
410
|
# data["ASV_Tamed"] - now includes cluster cryopod tames (cryo=True)
|
|
411
411
|
# data["ASV_Players"] - each player's inventory now contains their
|
|
412
412
|
# uploaded items (entries tagged "uploaded": true),
|
|
413
|
-
# matched by cloud-file stem
|
|
414
|
-
#
|
|
413
|
+
# matched by cloud-file stem == the player's
|
|
414
|
+
# .arkprofile filename stem.
|
|
415
415
|
```
|
|
416
416
|
|
|
417
|
-
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file
|
|
417
|
+
Cluster items are spliced into the owning player's `inventory`; no separate `ASV_ClusterItems` file is emitted. The match works because every cluster file and the player's `.arkprofile` share a stem — the Steam id on ASE, the hex platform UUID on ASA — so the join is keyed on the profile's source filename stem. (`Profile.unique_id` equals that stem only on ASE; on ASA it is the numeric net id, not the UUID filename, so loading profiles from real file paths — which sets `Profile.source_path` — is required for the ASA splice.)
|
|
418
418
|
|
|
419
419
|
Pre-loaded `CloudInventory` instances also work (`cluster=[inv1, inv2, ...]`).
|
|
420
420
|
|
|
@@ -436,7 +436,7 @@ data = export_cloud_inventory(cloud)
|
|
|
436
436
|
# }
|
|
437
437
|
```
|
|
438
438
|
|
|
439
|
-
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `
|
|
439
|
+
Each `ASV_Items` entry carries `itemId`, `qty`, `blueprint`, `id` (combined ItemID1_ItemID2), `uploadedTime` (ISO 8601 with UTC offset), and item-class-specific stats (`durability_max`, `damage`, `armor`, `hypo`, `hyper`, `crafter`, `crafter_tribe`, `skill_bonus`, `loaded_ammo`, `quality`, `rating`, `c0`..`c5` paint regions, `drop_location`, `egg_*` for fertilized eggs, `dino_*` for cryopod items, etc). Default / unset fields are filtered, NaN floats dropped, `"Unknown"` crafter strings nulled.
|
|
440
440
|
|
|
441
441
|
The low-level helpers are also available:
|
|
442
442
|
- `export_cluster_uploads(cluster_invs, map_config=None) -> list[dict]` — tamed-shape records (incl. cryopod-as-item dinos) across the supplied cloud files
|
|
@@ -589,7 +589,7 @@ All four expose `from_*` constructors and `to_dict()` for serialization.
|
|
|
589
589
|
|---|---|---|
|
|
590
590
|
| `export_tamed(save, map_config=None)` | `list[dict]` | `ASV_Tamed` records |
|
|
591
591
|
| `export_wild(save, map_config=None)` | `list[dict]` | `ASV_Wild` records |
|
|
592
|
-
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by
|
|
592
|
+
| `export_players(save, map_config=None, cluster_inventories=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers). Pass `cluster_inventories` to splice each player's uploaded items into their `inventory` (entries tagged `uploaded: true`, matched by cloud-file stem == the player's `.arkprofile` filename stem) |
|
|
593
593
|
| `export_tribes(save)` | `list[dict]` | `ASV_Tribes` records |
|
|
594
594
|
| `export_structures(save, map_config=None)` | `list[dict]` | `ASV_Structures` records |
|
|
595
595
|
| `export_tribe_logs(save)` | `list[dict]` | `ASV_TribeLogs` records |
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Tests for cloud-inventory export functions + inventory item stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from arkparser import (
|
|
8
|
+
CloudInventory,
|
|
9
|
+
export_cloud_inventory,
|
|
10
|
+
export_cluster_items,
|
|
11
|
+
)
|
|
12
|
+
from arkparser.export import (
|
|
13
|
+
_apply_stat_aliases,
|
|
14
|
+
_combine_item_id,
|
|
15
|
+
_expand_stat_slots,
|
|
16
|
+
_pascal_to_snake,
|
|
17
|
+
_uploaded_item_dict,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Top-level inventory-entry keys that intentionally keep legacy (camelCase)
|
|
21
|
+
# casing for ASV schema parity; everything else is snake_case.
|
|
22
|
+
_LEGACY_CAMEL_KEYS = {"itemId"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_pascal_to_snake_basic() -> None:
|
|
26
|
+
assert _pascal_to_snake("ItemStatValues") == "item_stat_values"
|
|
27
|
+
assert _pascal_to_snake("CrafterCharacterName") == "crafter_character_name"
|
|
28
|
+
assert _pascal_to_snake("bIsBlueprint") == "b_is_blueprint"
|
|
29
|
+
assert _pascal_to_snake("ItemID") == "item_id"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_expand_stat_slots_dict_and_list_agree() -> None:
|
|
33
|
+
# Sparse dict (GameObject inventory path) and dense list (cloud path) must
|
|
34
|
+
# produce identical named slots. Regression: the list shape was dropped
|
|
35
|
+
# entirely because only dict was handled.
|
|
36
|
+
sparse = {1: 6378, 2: 7106, 5: 11736, 7: 12857}
|
|
37
|
+
dense = [0, 6378, 7106, 0, 0, 11736, 0, 12857]
|
|
38
|
+
expected = {
|
|
39
|
+
"armor": 6378,
|
|
40
|
+
"durability_max": 7106,
|
|
41
|
+
"hypo": 11736,
|
|
42
|
+
"hyper": 12857,
|
|
43
|
+
}
|
|
44
|
+
assert _expand_stat_slots(sparse) == expected
|
|
45
|
+
assert _expand_stat_slots(dense) == expected
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_expand_stat_slots_single_slot_dict() -> None:
|
|
49
|
+
# A single populated slot must keep its index (regression: collapsed to a
|
|
50
|
+
# bare scalar upstream, which dropped the stat entirely).
|
|
51
|
+
assert _expand_stat_slots({2: 2624}) == {"durability_max": 2624}
|
|
52
|
+
# A bare scalar has no recoverable index -> ignored, no crash.
|
|
53
|
+
assert _expand_stat_slots(2624) == {}
|
|
54
|
+
assert _expand_stat_slots(None) == {}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_apply_stat_aliases_emits_slots_for_list_input() -> None:
|
|
58
|
+
out = _apply_stat_aliases({"item_stat_values": [0, 6378, 7106, 0, 0, 0, 0, 0]})
|
|
59
|
+
assert out["armor"] == 6378
|
|
60
|
+
assert out["durability_max"] == 7106
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_combine_item_id_keeps_legit_zero() -> None:
|
|
64
|
+
# Falsy-zero trap: ItemID1 == 0 must not be replaced by an _0 variant key.
|
|
65
|
+
assert _combine_item_id({"ItemID1": 0, "ItemID1_0": 77, "ItemID2": 5}) == "0_5"
|
|
66
|
+
assert _combine_item_id({"ItemID1": 12, "ItemID2": 34}) == "12_34"
|
|
67
|
+
# Non-numeric / corrupt components return None instead of raising.
|
|
68
|
+
assert _combine_item_id({"ItemID1": "NaN", "ItemID2": 5}) is None
|
|
69
|
+
assert _combine_item_id("not-a-dict") is None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_export_cloud_inventory_shape(ase_obelisk_path: Path) -> None:
|
|
73
|
+
cloud = CloudInventory.load(ase_obelisk_path)
|
|
74
|
+
out = export_cloud_inventory(cloud)
|
|
75
|
+
assert set(out.keys()) == {"ASV_Tamed", "ASV_Items"}
|
|
76
|
+
assert isinstance(out["ASV_Tamed"], list)
|
|
77
|
+
assert isinstance(out["ASV_Items"], list)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_cloud_items_have_flat_stats(ase_obelisk_path: Path) -> None:
|
|
81
|
+
cloud = CloudInventory.load(ase_obelisk_path)
|
|
82
|
+
items = export_cluster_items([cloud])
|
|
83
|
+
assert items, "ASE obelisk fixture has uploaded items"
|
|
84
|
+
item = items[0]
|
|
85
|
+
assert "itemId" in item
|
|
86
|
+
assert "qty" in item
|
|
87
|
+
# Stat keys are snake_case; only the legacy top-level keys keep camelCase.
|
|
88
|
+
assert all(
|
|
89
|
+
k == k.lower() or k in _LEGACY_CAMEL_KEYS for k in item
|
|
90
|
+
), f"unexpected non-snake_case key in {sorted(item)}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_uploaded_item_dict_no_skipped_keys(ase_obelisk_path: Path) -> None:
|
|
94
|
+
cloud = CloudInventory.load(ase_obelisk_path)
|
|
95
|
+
item = cloud.uploaded_items[0]
|
|
96
|
+
entry = _uploaded_item_dict(item)
|
|
97
|
+
for skip in ("ItemQuantity", "bIsBlueprint", "CustomItemDatas"):
|
|
98
|
+
assert _pascal_to_snake(skip) not in entry
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
"""Tests for cloud-inventory export functions + inventory item stats."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
from arkparser import (
|
|
8
|
-
CloudInventory,
|
|
9
|
-
export_cloud_inventory,
|
|
10
|
-
export_cluster_items,
|
|
11
|
-
)
|
|
12
|
-
from arkparser.export import _pascal_to_snake, _uploaded_item_dict
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_pascal_to_snake_basic() -> None:
|
|
16
|
-
assert _pascal_to_snake("ItemStatValues") == "item_stat_values"
|
|
17
|
-
assert _pascal_to_snake("CrafterCharacterName") == "crafter_character_name"
|
|
18
|
-
assert _pascal_to_snake("bIsBlueprint") == "b_is_blueprint"
|
|
19
|
-
assert _pascal_to_snake("ItemID") == "item_id"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def test_export_cloud_inventory_shape(ase_obelisk_path: Path) -> None:
|
|
23
|
-
cloud = CloudInventory.load(ase_obelisk_path)
|
|
24
|
-
out = export_cloud_inventory(cloud)
|
|
25
|
-
assert set(out.keys()) == {"ASV_Tamed", "ASV_Items"}
|
|
26
|
-
assert isinstance(out["ASV_Tamed"], list)
|
|
27
|
-
assert isinstance(out["ASV_Items"], list)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def test_cloud_items_have_flat_stats(ase_obelisk_path: Path) -> None:
|
|
31
|
-
cloud = CloudInventory.load(ase_obelisk_path)
|
|
32
|
-
items = export_cluster_items([cloud])
|
|
33
|
-
assert items, "ASE obelisk fixture has uploaded items"
|
|
34
|
-
item = items[0]
|
|
35
|
-
assert "itemId" in item
|
|
36
|
-
assert "qty" in item
|
|
37
|
-
assert all(k == k.lower() for k in item), "all keys snake_case-ish"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def test_uploaded_item_dict_no_skipped_keys(ase_obelisk_path: Path) -> None:
|
|
41
|
-
cloud = CloudInventory.load(ase_obelisk_path)
|
|
42
|
-
item = cloud.uploaded_items[0]
|
|
43
|
-
entry = _uploaded_item_dict(item)
|
|
44
|
-
for skip in ("ItemQuantity", "bIsBlueprint", "CustomItemDatas"):
|
|
45
|
-
assert _pascal_to_snake(skip) not in entry
|
|
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
|