arkparser 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {arkparser-0.3.2 → arkparser-0.3.3}/PKG-INFO +2 -2
  2. {arkparser-0.3.2 → arkparser-0.3.3}/README.md +1 -1
  3. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/__init__.py +1 -1
  4. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/data_models.py +45 -10
  5. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/export.py +44 -1
  6. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser.egg-info/PKG-INFO +2 -2
  7. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser.egg-info/SOURCES.txt +1 -0
  8. {arkparser-0.3.2 → arkparser-0.3.3}/pyproject.toml +1 -1
  9. arkparser-0.3.3/tests/test_cryopod_export.py +122 -0
  10. {arkparser-0.3.2 → arkparser-0.3.3}/LICENSE +0 -0
  11. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/__init__.py +0 -0
  12. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/binary_reader.py +0 -0
  13. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/exceptions.py +0 -0
  14. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/map_config.py +0 -0
  15. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/normalization.py +0 -0
  16. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/types.py +0 -0
  17. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/common/version_detection.py +0 -0
  18. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/__init__.py +0 -0
  19. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/base.py +0 -0
  20. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/cloud_inventory.py +0 -0
  21. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/profile.py +0 -0
  22. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/tribe.py +0 -0
  23. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/files/world_save.py +0 -0
  24. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/game_objects/__init__.py +0 -0
  25. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/game_objects/container.py +0 -0
  26. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/game_objects/game_object.py +0 -0
  27. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/game_objects/location.py +0 -0
  28. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/__init__.py +0 -0
  29. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/base.py +0 -0
  30. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/byte_property.py +0 -0
  31. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/compound.py +0 -0
  32. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/primitives.py +0 -0
  33. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/properties/registry.py +0 -0
  34. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/__init__.py +0 -0
  35. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/base.py +0 -0
  36. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/colors.py +0 -0
  37. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/misc.py +0 -0
  38. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/property_list.py +0 -0
  39. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/registry.py +0 -0
  40. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser/structs/vectors.py +0 -0
  41. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser.egg-info/dependency_links.txt +0 -0
  42. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser.egg-info/requires.txt +0 -0
  43. {arkparser-0.3.2 → arkparser-0.3.3}/arkparser.egg-info/top_level.txt +0 -0
  44. {arkparser-0.3.2 → arkparser-0.3.3}/setup.cfg +0 -0
  45. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_asa_header_position.py +0 -0
  46. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_binary_reader.py +0 -0
  47. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_binary_reader_layouts.py +0 -0
  48. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_cloud_inventory.py +0 -0
  49. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_current_stats.py +0 -0
  50. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_data_models.py +0 -0
  51. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_export.py +0 -0
  52. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_game_objects.py +0 -0
  53. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_profile.py +0 -0
  54. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_tribe.py +0 -0
  55. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_v13_property_layouts.py +0 -0
  56. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_version_detection.py +0 -0
  57. {arkparser-0.3.2 → arkparser-0.3.3}/tests/test_world_save.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -210,7 +210,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
210
210
  | `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
211
211
  | `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
212
212
  | `mut-f`, `mut-m` | legacy | **Ancestor-line totals.** `RandomMutationsFemale` and `RandomMutationsMale` — single integers counting the total number of mutations that occurred down the maternal and paternal ancestry lines respectively. These are *not* per-stat — they share the `-m` token with the per-stat mutation block below but mean a different thing. Kept under the legacy names for ASVExport parity. |
213
- | `cryo` | legacy | `IsInCryo` |
213
+ | `cryo` | legacy | `True` for creatures embedded inside cryopod / soultrap / vivarium / dinoball items in the world save, `False` for actor-in-world tames. `export_tamed` walks `WorldSave.iter_cryopod_creatures()` and emits one ASV_Tamed record per embedded creature in addition to the actor-in-world tames; on busy PvE servers cryopodded tames are the majority of the roster (e.g. 10,277 of 11,054 on a live Ragnarok_WP). Cluster-uploaded tames also surface here (via `export_cluster_uploads`) with `cryo=True`. |
214
214
  | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
215
215
  | `dinoid` | legacy | string form of `id` |
216
216
  | `isMating` | legacy | `bEnableTamedMating` |
@@ -176,7 +176,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
176
176
  | `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
177
177
  | `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
178
178
  | `mut-f`, `mut-m` | legacy | **Ancestor-line totals.** `RandomMutationsFemale` and `RandomMutationsMale` — single integers counting the total number of mutations that occurred down the maternal and paternal ancestry lines respectively. These are *not* per-stat — they share the `-m` token with the per-stat mutation block below but mean a different thing. Kept under the legacy names for ASVExport parity. |
179
- | `cryo` | legacy | `IsInCryo` |
179
+ | `cryo` | legacy | `True` for creatures embedded inside cryopod / soultrap / vivarium / dinoball items in the world save, `False` for actor-in-world tames. `export_tamed` walks `WorldSave.iter_cryopod_creatures()` and emits one ASV_Tamed record per embedded creature in addition to the actor-in-world tames; on busy PvE servers cryopodded tames are the majority of the roster (e.g. 10,277 of 11,054 on a live Ragnarok_WP). Cluster-uploaded tames also surface here (via `export_cluster_uploads`) with `cryo=True`. |
180
180
  | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
181
181
  | `dinoid` | legacy | string form of `id` |
182
182
  | `isMating` | legacy | `bEnableTamedMating` |
@@ -91,4 +91,4 @@ __all__ = [
91
91
  "ArkParseError",
92
92
  ]
93
93
 
94
- __version__ = "0.3.2"
94
+ __version__ = "0.3.3"
@@ -547,20 +547,29 @@ class CryopodCreature:
547
547
  ]
548
548
 
549
549
  if len(floats) >= 22:
550
- # Determine offset based on array length
551
- # ASA has 36 floats with offset 11, ASE has 25 floats with offset 12
552
- max_offset = 11 if len(floats) >= 36 else 12
553
-
554
- # Current stats start at 0
555
- for i, stat_name in enumerate(stat_names):
550
+ # ASA cryopod blob: 11 current stats + 11 max stats + extras (36 floats).
551
+ # ASE cryopod blob: 12 current stats + 12 max stats + 1 extra (25 floats).
552
+ # Without the per-format count, the ASA loop walks 12 names against an
553
+ # 11-wide current block, leaking max[0] (which equals current[0] at full
554
+ # HP) into current_stats["CraftingSkill"] and surfacing hp=craft on every
555
+ # cryopod tame in exports.
556
+ if len(floats) >= 36:
557
+ current_count = 11
558
+ max_offset = 11
559
+ else:
560
+ current_count = 12
561
+ max_offset = 12
562
+
563
+ # Current stats: only consume the per-format slot count
564
+ for i in range(min(current_count, len(stat_names))):
556
565
  if i < len(floats):
557
- cryo.current_stats[stat_name] = floats[i]
566
+ cryo.current_stats[stat_names[i]] = floats[i]
558
567
 
559
- # Max stats start at offset
560
- for i, stat_name in enumerate(stat_names):
568
+ # Max stats: same count, starting at offset
569
+ for i in range(min(current_count, len(stat_names))):
561
570
  max_idx = i + max_offset
562
571
  if max_idx < len(floats):
563
- cryo.max_stats[stat_name] = floats[max_idx]
572
+ cryo.max_stats[stat_names[i]] = floats[max_idx]
564
573
 
565
574
  # Parse soft class for blueprint reference
566
575
  soft_classes = normalize_indexed_list(custom_data.get("CustomDataSoftClasses"))
@@ -569,6 +578,32 @@ class CryopodCreature:
569
578
  if isinstance(first_class, dict):
570
579
  cryo.class_name = first_class.get("name", cryo.class_name)
571
580
 
581
+ # Populate raw creature_props / status_props so the export
582
+ # pipeline's _SyntheticGameObject adapter (which calls
583
+ # ``get_property_value``) sees the fields it expects. ARK
584
+ # cryopod CustomItemDatas blobs only carry a small subset of
585
+ # the original creature properties (no DinoID, no TribeID, no
586
+ # tamer string, etc.) — surface what we have, leave the rest
587
+ # absent so consumers can detect the gap.
588
+ _stat_to_idx = {
589
+ "Health": 0, "Stamina": 1, "Torpidity": 2, "Oxygen": 3,
590
+ "Food": 4, "Water": 5, "Temperature": 6, "Weight": 7,
591
+ "MeleeDamage": 8, "MovementSpeed": 9, "Fortitude": 10,
592
+ "CraftingSkill": 11,
593
+ }
594
+ if cryo.name:
595
+ cryo.creature_props["TamedName"] = cryo.name
596
+ for i, color in enumerate(cryo.colors[:6]):
597
+ cryo.creature_props[f"ColorSetIndices_{i}" if i > 0 else "ColorSetIndices"] = color
598
+ if cryo.level:
599
+ cryo.status_props["BaseCharacterLevel"] = cryo.level
600
+ for stat_name, value in cryo.current_stats.items():
601
+ idx = _stat_to_idx.get(stat_name)
602
+ if idx is None:
603
+ continue
604
+ key = "CurrentStatusValues" if idx == 0 else f"CurrentStatusValues_{idx}"
605
+ cryo.status_props[key] = value
606
+
572
607
  return cryo
573
608
 
574
609
  except Exception:
@@ -771,12 +771,55 @@ def _tamed_dict(
771
771
 
772
772
 
773
773
  def export_tamed(save: t.Any, map_config: MapConfig | None = None) -> list[dict[str, t.Any]]:
774
+ """Emit ASV_Tamed records.
775
+
776
+ Pre-conditions: ``save`` exposes ``get_tamed_creatures()`` (in-world
777
+ tames) and ideally ``iter_cryopod_creatures()`` (creatures whose actor
778
+ has been removed from the world and re-embedded inside a cryopod /
779
+ soultrap / vivarium / dinoball item).
780
+
781
+ Post-conditions: returned list combines (a) every in-world tame and
782
+ (b) every cryopod-embedded tame, with the latter carrying ``cryo=True``
783
+ and inheriting the cryopod item's world location for GPS fields.
784
+ """
774
785
  objects = _world_objects(save, "get_tamed_creatures", "tamed_objects")
775
786
  lookup = _save_lookup(save)
776
- return [
787
+ results: list[dict[str, t.Any]] = [
777
788
  _tamed_dict(obj, _status_for(obj, lookup), lookup, map_config, save)
778
789
  for obj in objects
779
790
  ]
791
+ results.extend(_export_world_cryopods(save, map_config))
792
+ return results
793
+
794
+
795
+ def _export_world_cryopods(
796
+ save: t.Any,
797
+ map_config: MapConfig | None,
798
+ ) -> list[dict[str, t.Any]]:
799
+ """Build ASV_Tamed records for cryopod-embedded creatures on the map.
800
+
801
+ ARK strips the actor for any creature stuffed into a cryopod / soultrap
802
+ / vivarium / dinoball and serialises a snapshot into the item's
803
+ ``CustomItemDatas``. ``get_tamed_creatures()`` therefore misses them
804
+ entirely. We walk ``iter_cryopod_creatures()`` (when available),
805
+ decode each embedded blob, and produce ``ASV_Tamed`` entries with
806
+ ``cryo=True`` and the cryopod item's location.
807
+ """
808
+ iter_cryos = getattr(save, "iter_cryopod_creatures", None)
809
+ if not callable(iter_cryos):
810
+ return []
811
+ out: list[dict[str, t.Any]] = []
812
+ empty_lookup: dict[t.Any, t.Any] = {}
813
+ for item_obj, cryo in iter_cryos():
814
+ actor, status = _cryo_props_to_synthetic(cryo)
815
+ # Inherit the cryopod's world location so GPS fields populate.
816
+ actor.location = getattr(item_obj, "location", None)
817
+ record = _tamed_dict(actor, status, empty_lookup, map_config, save)
818
+ # The synthetic actor carries no IsInCryo property; force the legacy
819
+ # flag so consumers can distinguish in-world tames from stored ones.
820
+ record["cryo"] = True
821
+ out.append(record)
822
+ return out
780
823
 
781
824
 
782
825
  class _SyntheticGameObject:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -210,7 +210,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
210
210
  | `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
211
211
  | `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
212
212
  | `mut-f`, `mut-m` | legacy | **Ancestor-line totals.** `RandomMutationsFemale` and `RandomMutationsMale` — single integers counting the total number of mutations that occurred down the maternal and paternal ancestry lines respectively. These are *not* per-stat — they share the `-m` token with the per-stat mutation block below but mean a different thing. Kept under the legacy names for ASVExport parity. |
213
- | `cryo` | legacy | `IsInCryo` |
213
+ | `cryo` | legacy | `True` for creatures embedded inside cryopod / soultrap / vivarium / dinoball items in the world save, `False` for actor-in-world tames. `export_tamed` walks `WorldSave.iter_cryopod_creatures()` and emits one ASV_Tamed record per embedded creature in addition to the actor-in-world tames; on busy PvE servers cryopodded tames are the majority of the roster (e.g. 10,277 of 11,054 on a live Ragnarok_WP). Cluster-uploaded tames also surface here (via `export_cluster_uploads`) with `cryo=True`. |
214
214
  | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
215
215
  | `dinoid` | legacy | string form of `id` |
216
216
  | `isMating` | legacy | `bEnableTamedMating` |
@@ -43,6 +43,7 @@ tests/test_asa_header_position.py
43
43
  tests/test_binary_reader.py
44
44
  tests/test_binary_reader_layouts.py
45
45
  tests/test_cloud_inventory.py
46
+ tests/test_cryopod_export.py
46
47
  tests/test_current_stats.py
47
48
  tests/test_data_models.py
48
49
  tests/test_export.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.3.2"
7
+ version = "0.3.3"
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"
@@ -0,0 +1,122 @@
1
+ """Tests pinning ASA cryopod creature decode + export integration.
2
+
3
+ ARK strips the actor for every cryopodded / soultrapped / vivariumed /
4
+ dinoballed creature and embeds a serialised snapshot into the item's
5
+ ``CustomItemDatas``. ``WorldSave.iter_cryopod_creatures`` walks these
6
+ items, and ``export_tamed`` then surfaces them as ``ASV_Tamed`` entries
7
+ with ``cryo=True``. Two regressions tested here:
8
+
9
+ - ASA cryopod blobs carry 11 current stats (no CraftingSkill in the
10
+ current block), 11 max stats, and 14 extras (36 floats total). The
11
+ pre-0.3.3 parser walked 12 names against an 11-wide block, leaking
12
+ ``max[0]`` (Health) into ``current_stats["CraftingSkill"]`` and
13
+ surfacing ``hp == craft`` on every cryopod tame.
14
+ - ``from_asa_cryopod_data`` did not populate ``creature_props`` /
15
+ ``status_props``; the ``_SyntheticGameObject`` adapter the exporter
16
+ uses then saw empty dicts and emitted records with ``lvl=1``,
17
+ ``name=""``, all-zero colors, and ``current_stats: null``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from arkparser.data_models import CryopodCreature
23
+
24
+
25
+ def _asa_blob(current: list[float], max_: list[float]) -> dict[str, object]:
26
+ """Synthesise the smallest ASA cryopod CustomItemDatas entry we can.
27
+
28
+ 36 floats total: 11 current + 11 max + 14 extras (zeros).
29
+ """
30
+ assert len(current) == 11
31
+ assert len(max_) == 11
32
+ floats = current + max_ + [0.0] * 14
33
+ return {
34
+ "CustomDataName": "Dino",
35
+ "CustomDataStrings": [
36
+ "Argent_Character_BP_C_2094073921", # [0] class_name
37
+ "dada - Lvl 259 (Argentavis)", # [1] display
38
+ "37,0,37,0,0,0,", # [2] colors
39
+ "", "", "", "", "", "", "Argentavis", # [9] species
40
+ ],
41
+ "CustomDataFloats": floats,
42
+ "CustomDataNames": [],
43
+ }
44
+
45
+
46
+ def test_asa_cryopod_eleven_current_stats_does_not_leak_max_into_craft() -> None:
47
+ """ASA cryopod blob: current[0..10] then max[0..10]. craft must come
48
+ from current[11] — which does not exist, so it must stay 0.0, NOT pick
49
+ up max[0]."""
50
+ current = [6206.46, 2411.2, 0.0, 750.0, 13153.03, 100.0, 0.0, 63.6, 6.69, 0.09, 0.0]
51
+ max_ = [6206.46, 2411.2, 0.0, 750.0, 13153.03, 100.0, 0.0, 63.6, 6.69, 0.09, 0.0]
52
+ cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob(current, max_))
53
+ assert cryo is not None
54
+ assert "Health" in cryo.current_stats
55
+ assert cryo.current_stats["Health"] == 6206.46
56
+ # CraftingSkill must NOT be in current_stats on ASA cryopods (only
57
+ # 11 slots are populated). Pre-0.3.3 set it equal to max[0]=Health.
58
+ assert "CraftingSkill" not in cryo.current_stats
59
+ # Max block round-trips properly
60
+ assert cryo.max_stats["Health"] == 6206.46
61
+
62
+
63
+ def test_asa_cryopod_populates_creature_and_status_props() -> None:
64
+ """The exporter adapter (``_SyntheticGameObject``) reads via
65
+ ``get_property_value`` keyed by ARK property name + index suffix. The
66
+ ASA cryopod decoder must mirror those keys into ``creature_props`` /
67
+ ``status_props`` so the synthetic adapter surfaces tamed-name, level,
68
+ colors, and CurrentStatusValues per stat index."""
69
+ current = [3000.0, 800.0, 0.0, 500.0, 5000.0, 100.0, 0.0, 200.0, 2.5, 0.05, 0.0]
70
+ max_ = [3000.0] * 11
71
+ cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob(current, max_))
72
+ assert cryo is not None
73
+
74
+ # TamedName from display-name parse
75
+ assert cryo.creature_props.get("TamedName") == "dada"
76
+ # Level from display-name parse
77
+ assert cryo.status_props.get("BaseCharacterLevel") == 259
78
+ # Colors at expected indices (first non-zero is c0=37, then c2=37)
79
+ assert cryo.creature_props.get("ColorSetIndices") == 37
80
+ assert cryo.creature_props.get("ColorSetIndices_2") == 37
81
+ # CurrentStatusValues indexed by EPrimalCharacterStatusValue
82
+ # 0=Health, 1=Stamina, 3=Oxygen, 4=Food, 7=Weight, 8=MeleeDamage
83
+ assert cryo.status_props.get("CurrentStatusValues") == 3000.0 # health
84
+ assert cryo.status_props.get("CurrentStatusValues_1") == 800.0 # stam
85
+ assert cryo.status_props.get("CurrentStatusValues_4") == 5000.0 # food
86
+ assert cryo.status_props.get("CurrentStatusValues_7") == 200.0 # weight
87
+ assert cryo.status_props.get("CurrentStatusValues_8") == 2.5 # melee
88
+
89
+
90
+ def test_asa_cryopod_display_name_parses_level_and_species() -> None:
91
+ """Display-name format ``"Name - Lvl N (Species)"`` must yield all three."""
92
+ cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob([0.0] * 11, [0.0] * 11))
93
+ assert cryo is not None
94
+ assert cryo.name == "dada"
95
+ assert cryo.level == 259
96
+ # Species comes from strings[9] when present, falling back to display parse
97
+ assert cryo.species == "Argentavis"
98
+
99
+
100
+ def test_ase_cryopod_keeps_twelve_stat_layout() -> None:
101
+ """ASE cryopod blobs carry 12 current stats + 12 max stats (25 floats).
102
+ The ASA fix must not break the ASE path: walking all 12 names is the
103
+ correct behaviour when ``len(floats) < 36``."""
104
+ # 25 floats = 12 current + 12 max + 1 extra
105
+ floats = [float(i + 1) for i in range(12)] + [float(i + 100) for i in range(12)] + [0.0]
106
+ blob = {
107
+ "CustomDataName": "Dino",
108
+ "CustomDataStrings": [
109
+ "Raptor_Character_BP_C", "Bluey - Lvl 30 (Raptor)", "0,0,0,0,0,0,",
110
+ ],
111
+ "CustomDataFloats": floats,
112
+ "CustomDataNames": [],
113
+ }
114
+ cryo = CryopodCreature.from_asa_cryopod_data(blob)
115
+ assert cryo is not None
116
+ # All 12 current and 12 max should be populated on ASE blobs
117
+ assert len(cryo.current_stats) == 12
118
+ assert len(cryo.max_stats) == 12
119
+ assert cryo.current_stats["Health"] == 1.0
120
+ assert cryo.current_stats["CraftingSkill"] == 12.0
121
+ assert cryo.max_stats["Health"] == 100.0
122
+ assert cryo.max_stats["CraftingSkill"] == 111.0
File without changes
File without changes
File without changes