arkparser 0.1.1__tar.gz → 0.1.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 (57) hide show
  1. {arkparser-0.1.1 → arkparser-0.1.2}/PKG-INFO +1 -1
  2. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/cloud_inventory.py +29 -13
  3. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser.egg-info/PKG-INFO +1 -1
  4. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser.egg-info/SOURCES.txt +2 -1
  5. {arkparser-0.1.1 → arkparser-0.1.2}/pyproject.toml +1 -1
  6. {arkparser-0.1.1 → arkparser-0.1.2}/tests/test_cloud_inventory.py +88 -1
  7. arkparser-0.1.2/tests/test_world_save.py +252 -0
  8. {arkparser-0.1.1 → arkparser-0.1.2}/LICENSE +0 -0
  9. {arkparser-0.1.1 → arkparser-0.1.2}/README.md +0 -0
  10. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/__init__.py +0 -0
  11. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/__init__.py +0 -0
  12. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/binary_reader.py +0 -0
  13. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/exceptions.py +0 -0
  14. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/map_config.py +0 -0
  15. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/types.py +0 -0
  16. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/common/version_detection.py +0 -0
  17. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/data_models.py +0 -0
  18. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/export.py +0 -0
  19. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/__init__.py +0 -0
  20. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/base.py +0 -0
  21. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/profile.py +0 -0
  22. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/tribe.py +0 -0
  23. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/files/world_save.py +0 -0
  24. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/game_objects/__init__.py +0 -0
  25. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/game_objects/container.py +0 -0
  26. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/game_objects/game_object.py +0 -0
  27. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/game_objects/location.py +0 -0
  28. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/__init__.py +0 -0
  29. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/character.py +0 -0
  30. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/creature.py +0 -0
  31. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/item.py +0 -0
  32. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/player.py +0 -0
  33. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/stats.py +0 -0
  34. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/structure.py +0 -0
  35. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/models/tribe.py +0 -0
  36. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/__init__.py +0 -0
  37. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/base.py +0 -0
  38. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/byte_property.py +0 -0
  39. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/compound.py +0 -0
  40. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/primitives.py +0 -0
  41. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/properties/registry.py +0 -0
  42. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/py.typed +0 -0
  43. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/__init__.py +0 -0
  44. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/base.py +0 -0
  45. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/colors.py +0 -0
  46. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/misc.py +0 -0
  47. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/property_list.py +0 -0
  48. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/registry.py +0 -0
  49. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser/structs/vectors.py +0 -0
  50. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser.egg-info/dependency_links.txt +0 -0
  51. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser.egg-info/requires.txt +0 -0
  52. {arkparser-0.1.1 → arkparser-0.1.2}/arkparser.egg-info/top_level.txt +0 -0
  53. {arkparser-0.1.1 → arkparser-0.1.2}/setup.cfg +0 -0
  54. {arkparser-0.1.1 → arkparser-0.1.2}/tests/test_models.py +0 -0
  55. {arkparser-0.1.1 → arkparser-0.1.2}/tests/test_profile.py +0 -0
  56. {arkparser-0.1.1 → arkparser-0.1.2}/tests/test_tribe.py +0 -0
  57. {arkparser-0.1.1 → arkparser-0.1.2}/tests/test_version_detection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.1.1
3
+ Version: 0.1.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
@@ -56,13 +56,19 @@ class CloudInventory(ArkFile):
56
56
  - Object headers (ASE format)
57
57
  - Properties
58
58
 
59
- ASA format:
60
- - Int32 version
61
- - Int32 unknown1 (extra field)
62
- - Int32 unknown2 (extra field)
59
+ ASA v7 format:
60
+ - Int32 version (7)
61
+ - Int32 unknown1 (extra field, v7+ only)
62
+ - Int32 unknown2 (extra field, v7+ only)
63
63
  - Int32 object_count
64
64
  - Object headers (ASA format with GUIDs)
65
65
  - Properties
66
+
67
+ ASA v6 format (solo-cluster / cross-ARK transfer files):
68
+ - Int32 version (6)
69
+ - Int32 object_count
70
+ - Object headers (ASA format with GUIDs — no extra header fields)
71
+ - Properties
66
72
  """
67
73
  # Read version
68
74
  version = reader.read_int32()
@@ -73,8 +79,8 @@ class CloudInventory(ArkFile):
73
79
  # Detect ASE vs ASA
74
80
  is_asa = cls._detect_asa(reader, version)
75
81
 
76
- if is_asa:
77
- # ASA has extra header fields
82
+ if is_asa and version >= 7:
83
+ # v7+ ASA has two extra header fields before object_count
78
84
  _unknown1 = reader.read_int32()
79
85
  _unknown2 = reader.read_int32()
80
86
 
@@ -88,17 +94,23 @@ class CloudInventory(ArkFile):
88
94
  objects: list[GameObject] = []
89
95
  for i in range(object_count):
90
96
  if is_asa:
91
- obj = cls._read_asa_object_header(reader, obj_id=i)
97
+ obj = cls._read_asa_object_header(reader, obj_id=i, version=version)
92
98
  else:
93
99
  obj = GameObject.read_header(reader, obj_id=i, is_asa=False)
94
100
  objects.append(obj)
95
101
 
96
- # Load properties for each object
102
+ # Load properties for each object.
103
+ # Version 6 ASA (cross-ARK / solecluster) uses ASA-style object headers
104
+ # but ASE-style (is_asa=False) properties. Only v7+ uses ASA properties.
105
+ properties_is_asa = version >= 7
97
106
  properties_block_offset = 0
98
107
  for i, obj in enumerate(objects):
99
108
  next_obj = objects[i + 1] if i + 1 < len(objects) else None
100
109
  obj.load_properties(
101
- reader, properties_block_offset=properties_block_offset, is_asa=is_asa, next_object=next_obj
110
+ reader,
111
+ properties_block_offset=properties_block_offset,
112
+ is_asa=properties_is_asa,
113
+ next_object=next_obj,
102
114
  )
103
115
 
104
116
  # Build container with lookups
@@ -113,7 +125,7 @@ class CloudInventory(ArkFile):
113
125
  )
114
126
 
115
127
  @classmethod
116
- def _read_asa_object_header(cls, reader: BinaryReader, obj_id: int) -> GameObject:
128
+ def _read_asa_object_header(cls, reader: BinaryReader, obj_id: int, version: int = 7) -> GameObject:
117
129
  """
118
130
  Read an ASA object header.
119
131
 
@@ -123,7 +135,10 @@ class CloudInventory(ArkFile):
123
135
  - Field1 (int32)
124
136
  - Field2 (int32)
125
137
  - Instance name (string)
126
- - Padding (21 bytes)
138
+ - Padding (21 bytes for v7+, 20 bytes for v6)
139
+
140
+ Version 6 (cross-ARK / solecluster files) uses a slightly older format
141
+ with one fewer padding byte before the properties block.
127
142
  """
128
143
  obj = GameObject(id=obj_id)
129
144
 
@@ -142,8 +157,9 @@ class CloudInventory(ArkFile):
142
157
  instance_name = reader.read_string()
143
158
  obj.names = [instance_name] if instance_name else []
144
159
 
145
- # Skip 21 bytes of padding
146
- reader.skip(21)
160
+ # v7+ uses 21 bytes of padding; v6 (cross-ARK / solecluster) uses 20
161
+ padding_size = 21 if version >= 7 else 20
162
+ reader.skip(padding_size)
147
163
 
148
164
  # Properties offset will be set by sequential reading
149
165
  obj.properties_offset = reader.position
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.1.1
3
+ Version: 0.1.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
@@ -51,4 +51,5 @@ tests/test_cloud_inventory.py
51
51
  tests/test_models.py
52
52
  tests/test_profile.py
53
53
  tests/test_tribe.py
54
- tests/test_version_detection.py
54
+ tests/test_version_detection.py
55
+ tests/test_world_save.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.1.1"
7
+ version = "0.1.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"
@@ -2,9 +2,10 @@
2
2
  Tests for cloud inventory (obelisk) parsing - both ASE and ASA formats.
3
3
  """
4
4
 
5
- import pytest
6
5
  from pathlib import Path
7
6
 
7
+ import pytest
8
+
8
9
  from arkparser import CloudInventory
9
10
 
10
11
 
@@ -126,6 +127,7 @@ class TestCloudInventoryStats:
126
127
  def test_ase_base_stats_object(self, ase_obelisk_path: Path) -> None:
127
128
  """Stats should return a DinoStats object."""
128
129
  from arkparser import DinoStats
130
+
129
131
  inv = CloudInventory.load(ase_obelisk_path)
130
132
  creature = inv.uploaded_creatures[0]
131
133
  assert isinstance(creature.stats, DinoStats)
@@ -133,6 +135,7 @@ class TestCloudInventoryStats:
133
135
  def test_asa_base_stats_object(self, asa_obelisk_path: Path) -> None:
134
136
  """Stats should return a DinoStats object."""
135
137
  from arkparser import DinoStats
138
+
136
139
  inv = CloudInventory.load(asa_obelisk_path)
137
140
  creature = inv.uploaded_creatures[0]
138
141
  assert isinstance(creature.stats, DinoStats)
@@ -146,3 +149,87 @@ class TestCloudInventoryStats:
146
149
  assert "level" in d
147
150
  assert "stats" in d
148
151
 
152
+
153
+ class TestASESolecluster:
154
+ """
155
+ Tests for ASE solecluster (cross-ARK transfer) files.
156
+
157
+ The solecluster directory contains 128 files; 15 are 0-byte empties.
158
+ All non-empty files must parse without errors.
159
+ """
160
+
161
+ def test_all_parse_without_errors(self, ase_solecluster_dir: Path) -> None:
162
+ """Every non-empty ASE solecluster file should load without raising."""
163
+ failures: list[str] = []
164
+ for f in sorted(ase_solecluster_dir.iterdir()):
165
+ if f.stat().st_size == 0:
166
+ continue
167
+ try:
168
+ CloudInventory.load(f)
169
+ except Exception as e:
170
+ failures.append(f"{f.name}: {e}")
171
+ assert failures == [], "Parse failures:\n" + "\n".join(failures)
172
+
173
+ def test_format_is_ase(self, ase_solecluster_dir: Path) -> None:
174
+ """All non-empty ASE solecluster files should be identified as ASE."""
175
+ for f in sorted(ase_solecluster_dir.iterdir())[:20]:
176
+ if f.stat().st_size == 0:
177
+ continue
178
+ inv = CloudInventory.load(f)
179
+ assert not inv.is_asa, f"{f.name} was wrongly detected as ASA"
180
+
181
+ def test_have_objects(self, ase_solecluster_dir: Path) -> None:
182
+ """Every non-empty ASE solecluster file should contain at least 1 object."""
183
+ for f in sorted(ase_solecluster_dir.iterdir())[:20]:
184
+ if f.stat().st_size == 0:
185
+ continue
186
+ inv = CloudInventory.load(f)
187
+ assert len(inv.objects) >= 1, f"{f.name} has no objects"
188
+
189
+ def test_nonempty_count(self, ase_solecluster_dir: Path) -> None:
190
+ """Sanity check: at least 100 non-empty files in the ASE solecluster dir."""
191
+ nonempty = [f for f in ase_solecluster_dir.iterdir() if f.stat().st_size > 0]
192
+ assert len(nonempty) >= 100
193
+
194
+
195
+ class TestASASolecluster:
196
+ """
197
+ Tests for ASA solecluster (cross-ARK transfer) files.
198
+
199
+ These are version-6 ASA files: GUID-based object headers but ASE-style
200
+ properties. The solecluster directory contains 173 files; 35 are 0-byte empties.
201
+ All non-empty files must parse without errors.
202
+ """
203
+
204
+ def test_all_parse_without_errors(self, asa_solecluster_dir: Path) -> None:
205
+ """Every non-empty ASA solecluster file should load without raising."""
206
+ failures: list[str] = []
207
+ for f in sorted(asa_solecluster_dir.iterdir()):
208
+ if f.stat().st_size == 0:
209
+ continue
210
+ try:
211
+ CloudInventory.load(f)
212
+ except Exception as e:
213
+ failures.append(f"{f.name}: {e}")
214
+ assert failures == [], "Parse failures:\n" + "\n".join(failures)
215
+
216
+ def test_format_is_asa(self, asa_solecluster_dir: Path) -> None:
217
+ """All non-empty ASA solecluster files should be identified as ASA."""
218
+ for f in sorted(asa_solecluster_dir.iterdir())[:20]:
219
+ if f.stat().st_size == 0:
220
+ continue
221
+ inv = CloudInventory.load(f)
222
+ assert inv.is_asa, f"{f.name} was wrongly detected as ASE"
223
+
224
+ def test_have_objects(self, asa_solecluster_dir: Path) -> None:
225
+ """Every non-empty ASA solecluster file should contain at least 1 object."""
226
+ for f in sorted(asa_solecluster_dir.iterdir())[:20]:
227
+ if f.stat().st_size == 0:
228
+ continue
229
+ inv = CloudInventory.load(f)
230
+ assert len(inv.objects) >= 1, f"{f.name} has no objects"
231
+
232
+ def test_nonempty_count(self, asa_solecluster_dir: Path) -> None:
233
+ """Sanity check: at least 130 non-empty files in the ASA solecluster dir."""
234
+ nonempty = [f for f in asa_solecluster_dir.iterdir() if f.stat().st_size > 0]
235
+ assert len(nonempty) >= 130
@@ -0,0 +1,252 @@
1
+ """
2
+ Comprehensive tests for WorldSave parsing across multiple maps and formats.
3
+
4
+ Baselines (verified against example save files):
5
+ ASE Extinction – objects=77392, tamed=37, wild=30074, pawns=65
6
+ terminals=32, artifacts=3, resources=37
7
+ ASE Ragnarok – objects=93194, tamed=1, wild=1, pawns=1245
8
+ terminals=3, artifacts=11, resources=118
9
+ ASA Extinction – objects=128751, tamed=181, wild=32438, pawns=67
10
+ terminals=31, artifacts=3, resources=18
11
+ """
12
+
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+
17
+ from arkparser import WorldSave
18
+ from arkparser.game_objects.game_object import GameObject
19
+ from arkparser.game_objects.location import LocationData
20
+
21
+ _EXAMPLES = Path(__file__).parent.parent / "references" / "examples"
22
+ _ASE_EXTINCTION = _EXAMPLES / "ase" / "maps" / "extinction" / "Extinction.ark"
23
+ _ASE_RAGNAROK = _EXAMPLES / "ase" / "maps" / "ragnarok" / "Ragnarok.ark"
24
+ _ASA_EXTINCTION = _EXAMPLES / "asa" / "maps" / "extinction" / "Extinction_WP.ark"
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Shared fixtures – the world-save objects are expensive to load, so we
29
+ # cache them at module scope via session-scoped fixtures.
30
+ # ---------------------------------------------------------------------------
31
+
32
+
33
+ @pytest.fixture(scope="session")
34
+ def ase_extinction() -> WorldSave:
35
+ return WorldSave.load(_ASE_EXTINCTION)
36
+
37
+
38
+ @pytest.fixture(scope="session")
39
+ def ase_ragnarok() -> WorldSave:
40
+ return WorldSave.load(_ASE_RAGNAROK)
41
+
42
+
43
+ @pytest.fixture(scope="session")
44
+ def asa_extinction() -> WorldSave:
45
+ return WorldSave.load(_ASA_EXTINCTION)
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # ASE Extinction
50
+ # ---------------------------------------------------------------------------
51
+
52
+
53
+ class TestASEWorldSaveExtinction:
54
+ """Tests against the ASE Extinction world save (Extinction.ark)."""
55
+
56
+ def test_loads(self, ase_extinction: WorldSave) -> None:
57
+ assert ase_extinction is not None
58
+
59
+ def test_is_not_asa(self, ase_extinction: WorldSave) -> None:
60
+ assert not ase_extinction.is_asa
61
+
62
+ def test_object_count(self, ase_extinction: WorldSave) -> None:
63
+ assert len(ase_extinction.objects) == 77392
64
+
65
+ def test_zero_parse_errors(self, ase_extinction: WorldSave) -> None:
66
+ assert ase_extinction.parse_error_count == 0
67
+
68
+ def test_tamed_creature_count(self, ase_extinction: WorldSave) -> None:
69
+ assert len(ase_extinction.get_tamed_creatures()) == 37
70
+
71
+ def test_wild_creature_count(self, ase_extinction: WorldSave) -> None:
72
+ assert len(ase_extinction.get_wild_creatures()) == 30074
73
+
74
+ def test_player_pawn_count(self, ase_extinction: WorldSave) -> None:
75
+ assert len(ase_extinction.get_player_pawns()) == 65
76
+
77
+ def test_terminal_count(self, ase_extinction: WorldSave) -> None:
78
+ assert len(ase_extinction.get_terminals()) == 32
79
+
80
+ def test_artifact_crate_count(self, ase_extinction: WorldSave) -> None:
81
+ assert len(ase_extinction.get_artifact_crates()) == 3
82
+
83
+ def test_map_resource_count(self, ase_extinction: WorldSave) -> None:
84
+ assert len(ase_extinction.get_map_resources()) == 37
85
+
86
+ def test_supply_drops_empty(self, ase_extinction: WorldSave) -> None:
87
+ """No active supply drops in this save."""
88
+ assert len(ase_extinction.get_supply_drops()) == 0
89
+
90
+ def test_creatures_have_class_name(self, ase_extinction: WorldSave) -> None:
91
+ for creature in ase_extinction.get_tamed_creatures():
92
+ assert isinstance(creature.class_name, str)
93
+ assert len(creature.class_name) > 0
94
+
95
+ def test_tamed_creatures_have_location(self, ase_extinction: WorldSave) -> None:
96
+ for creature in ase_extinction.get_tamed_creatures():
97
+ assert isinstance(creature.location, LocationData)
98
+
99
+ def test_wild_creature_is_game_object(self, ase_extinction: WorldSave) -> None:
100
+ for c in ase_extinction.get_wild_creatures()[:10]:
101
+ assert isinstance(c, GameObject)
102
+
103
+ def test_terminals_have_class_name(self, ase_extinction: WorldSave) -> None:
104
+ for terminal in ase_extinction.get_terminals():
105
+ cn = terminal.class_name.lower()
106
+ assert "terminal" in cn or "tribute" in cn or "city" in cn
107
+
108
+ def test_artifacts_have_class_name(self, ase_extinction: WorldSave) -> None:
109
+ for art in ase_extinction.get_artifact_crates():
110
+ assert "artifact" in art.class_name.lower()
111
+
112
+ def test_resources_have_class_name(self, ase_extinction: WorldSave) -> None:
113
+ valid_patterns = ("oilvein", "watervein", "gasvein", "chargenode", "elementvein", "beaverdam")
114
+ for r in ase_extinction.get_map_resources():
115
+ cn = r.class_name.lower()
116
+ assert any(p in cn for p in valid_patterns), f"Unexpected resource class: {r.class_name}"
117
+
118
+ def test_version_is_valid_ase(self, ase_extinction: WorldSave) -> None:
119
+ assert ase_extinction.version in (5, 6, 7, 8, 9, 10, 11, 12)
120
+ assert not ase_extinction.is_asa
121
+
122
+ def test_to_dict(self, ase_extinction: WorldSave) -> None:
123
+ d = ase_extinction.to_dict()
124
+ assert isinstance(d, dict)
125
+ assert "version" in d
126
+ assert "is_asa" in d
127
+ assert d["is_asa"] is False
128
+
129
+
130
+ # ---------------------------------------------------------------------------
131
+ # ASE Ragnarok
132
+ # ---------------------------------------------------------------------------
133
+
134
+
135
+ class TestASEWorldSaveRagnarok:
136
+ """Tests against the ASE Ragnarok world save (Ragnarok.ark)."""
137
+
138
+ def test_loads(self, ase_ragnarok: WorldSave) -> None:
139
+ assert ase_ragnarok is not None
140
+
141
+ def test_is_not_asa(self, ase_ragnarok: WorldSave) -> None:
142
+ assert not ase_ragnarok.is_asa
143
+
144
+ def test_object_count(self, ase_ragnarok: WorldSave) -> None:
145
+ assert len(ase_ragnarok.objects) == 93194
146
+
147
+ def test_zero_parse_errors(self, ase_ragnarok: WorldSave) -> None:
148
+ assert ase_ragnarok.parse_error_count == 0
149
+
150
+ def test_tamed_creature_count(self, ase_ragnarok: WorldSave) -> None:
151
+ assert len(ase_ragnarok.get_tamed_creatures()) == 1
152
+
153
+ def test_wild_creature_count(self, ase_ragnarok: WorldSave) -> None:
154
+ assert len(ase_ragnarok.get_wild_creatures()) == 1
155
+
156
+ def test_player_pawn_count(self, ase_ragnarok: WorldSave) -> None:
157
+ assert len(ase_ragnarok.get_player_pawns()) == 1245
158
+
159
+ def test_terminal_count(self, ase_ragnarok: WorldSave) -> None:
160
+ assert len(ase_ragnarok.get_terminals()) == 3
161
+
162
+ def test_artifact_crate_count(self, ase_ragnarok: WorldSave) -> None:
163
+ assert len(ase_ragnarok.get_artifact_crates()) == 11
164
+
165
+ def test_map_resource_count(self, ase_ragnarok: WorldSave) -> None:
166
+ assert len(ase_ragnarok.get_map_resources()) == 118
167
+
168
+ def test_supply_drops_empty(self, ase_ragnarok: WorldSave) -> None:
169
+ assert len(ase_ragnarok.get_supply_drops()) == 0
170
+
171
+ def test_version_is_valid_ase(self, ase_ragnarok: WorldSave) -> None:
172
+ assert ase_ragnarok.version in (5, 6, 7, 8, 9, 10, 11, 12)
173
+ assert not ase_ragnarok.is_asa
174
+
175
+ def test_to_dict(self, ase_ragnarok: WorldSave) -> None:
176
+ d = ase_ragnarok.to_dict()
177
+ assert isinstance(d, dict)
178
+ assert d["is_asa"] is False
179
+
180
+
181
+ # ---------------------------------------------------------------------------
182
+ # ASA Extinction
183
+ # ---------------------------------------------------------------------------
184
+
185
+
186
+ class TestASAWorldSaveExtinction:
187
+ """Tests against the ASA Extinction world save (Extinction_WP.ark)."""
188
+
189
+ def test_loads(self, asa_extinction: WorldSave) -> None:
190
+ assert asa_extinction is not None
191
+
192
+ def test_is_asa(self, asa_extinction: WorldSave) -> None:
193
+ assert asa_extinction.is_asa
194
+
195
+ def test_object_count(self, asa_extinction: WorldSave) -> None:
196
+ assert len(asa_extinction.objects) == 128751
197
+
198
+ def test_zero_parse_errors(self, asa_extinction: WorldSave) -> None:
199
+ assert asa_extinction.parse_error_count == 0
200
+
201
+ def test_tamed_creature_count(self, asa_extinction: WorldSave) -> None:
202
+ assert len(asa_extinction.get_tamed_creatures()) == 181
203
+
204
+ def test_wild_creature_count(self, asa_extinction: WorldSave) -> None:
205
+ assert len(asa_extinction.get_wild_creatures()) == 32438
206
+
207
+ def test_player_pawn_count(self, asa_extinction: WorldSave) -> None:
208
+ assert len(asa_extinction.get_player_pawns()) == 67
209
+
210
+ def test_terminal_count(self, asa_extinction: WorldSave) -> None:
211
+ assert len(asa_extinction.get_terminals()) == 31
212
+
213
+ def test_artifact_crate_count(self, asa_extinction: WorldSave) -> None:
214
+ assert len(asa_extinction.get_artifact_crates()) == 3
215
+
216
+ def test_map_resource_count(self, asa_extinction: WorldSave) -> None:
217
+ assert len(asa_extinction.get_map_resources()) == 18
218
+
219
+ def test_supply_drops_empty(self, asa_extinction: WorldSave) -> None:
220
+ assert len(asa_extinction.get_supply_drops()) == 0
221
+
222
+ def test_creatures_have_class_name(self, asa_extinction: WorldSave) -> None:
223
+ for creature in asa_extinction.get_tamed_creatures()[:10]:
224
+ assert isinstance(creature.class_name, str)
225
+ assert len(creature.class_name) > 0
226
+
227
+ def test_tamed_creatures_have_location(self, asa_extinction: WorldSave) -> None:
228
+ for creature in asa_extinction.get_tamed_creatures()[:10]:
229
+ assert isinstance(creature.location, LocationData)
230
+
231
+ def test_terminals_have_class_name(self, asa_extinction: WorldSave) -> None:
232
+ for terminal in asa_extinction.get_terminals():
233
+ cn = terminal.class_name.lower()
234
+ assert "terminal" in cn or "tribute" in cn or "city" in cn
235
+
236
+ def test_artifacts_have_class_name(self, asa_extinction: WorldSave) -> None:
237
+ for art in asa_extinction.get_artifact_crates():
238
+ assert "artifact" in art.class_name.lower()
239
+
240
+ def test_resources_have_class_name(self, asa_extinction: WorldSave) -> None:
241
+ valid_patterns = ("oilvein", "watervein", "gasvein", "chargenode", "elementvein", "beaverdam")
242
+ for r in asa_extinction.get_map_resources():
243
+ cn = r.class_name.lower()
244
+ assert any(p in cn for p in valid_patterns), f"Unexpected resource class: {r.class_name}"
245
+
246
+ def test_version_is_asa(self, asa_extinction: WorldSave) -> None:
247
+ assert asa_extinction.version >= 7
248
+
249
+ def test_to_dict(self, asa_extinction: WorldSave) -> None:
250
+ d = asa_extinction.to_dict()
251
+ assert isinstance(d, dict)
252
+ assert d["is_asa"] is True
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes