arkparser 0.1.0__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.
- {arkparser-0.1.0 → arkparser-0.1.2}/PKG-INFO +9 -1
- {arkparser-0.1.0 → arkparser-0.1.2}/README.md +8 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/cloud_inventory.py +29 -13
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/world_save.py +92 -24
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/container.py +59 -4
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/PKG-INFO +9 -1
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/SOURCES.txt +2 -1
- {arkparser-0.1.0 → arkparser-0.1.2}/pyproject.toml +1 -1
- {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_cloud_inventory.py +88 -1
- arkparser-0.1.2/tests/test_world_save.py +252 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/LICENSE +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/binary_reader.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/exceptions.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/map_config.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/types.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/common/version_detection.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/data_models.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/export.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/base.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/profile.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/files/tribe.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/game_object.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/game_objects/location.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/character.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/creature.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/item.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/player.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/stats.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/structure.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/models/tribe.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/base.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/byte_property.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/compound.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/primitives.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/properties/registry.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/py.typed +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/__init__.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/base.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/colors.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/misc.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/property_list.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/registry.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser/structs/vectors.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/dependency_links.txt +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/requires.txt +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/arkparser.egg-info/top_level.txt +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/setup.cfg +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_models.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_profile.py +0 -0
- {arkparser-0.1.0 → arkparser-0.1.2}/tests/test_tribe.py +0 -0
- {arkparser-0.1.0 → 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.
|
|
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
|
|
@@ -338,6 +338,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
338
338
|
| `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
|
|
339
339
|
| `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
|
|
340
340
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
|
|
341
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
342
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
|
|
343
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
344
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
341
345
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
342
346
|
| `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
|
|
343
347
|
| `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
|
|
@@ -390,6 +394,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
390
394
|
| `get_creatures()` | `list[GameObject]` | All creatures |
|
|
391
395
|
| `get_structures()` | `list[GameObject]` | Tribe-owned structures |
|
|
392
396
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on map |
|
|
397
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
398
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates |
|
|
399
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
400
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
393
401
|
| `get_players()` | `list[GameObject]` | Player data objects |
|
|
394
402
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
395
403
|
|
|
@@ -304,6 +304,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
304
304
|
| `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
|
|
305
305
|
| `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
|
|
306
306
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
|
|
307
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
308
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
|
|
309
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
310
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
307
311
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
308
312
|
| `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
|
|
309
313
|
| `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
|
|
@@ -356,6 +360,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
356
360
|
| `get_creatures()` | `list[GameObject]` | All creatures |
|
|
357
361
|
| `get_structures()` | `list[GameObject]` | Tribe-owned structures |
|
|
358
362
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on map |
|
|
363
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
364
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates |
|
|
365
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
366
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
359
367
|
| `get_players()` | `list[GameObject]` | Player data objects |
|
|
360
368
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
361
369
|
|
|
@@ -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,
|
|
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
|
-
#
|
|
146
|
-
|
|
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
|
|
@@ -221,43 +221,31 @@ class WorldSave:
|
|
|
221
221
|
# Class-name patterns that are never structures even though they may
|
|
222
222
|
# carry ``TargetingTeam``. Checked via ``any(pat in cn for pat ...)``.
|
|
223
223
|
_NON_STRUCTURE_PATTERNS: t.ClassVar[tuple[str, ...]] = (
|
|
224
|
-
"_Character_BP",
|
|
225
|
-
"DinoCharacter",
|
|
226
|
-
"PlayerPawn",
|
|
227
|
-
"Buff_",
|
|
228
|
-
"PrimalBuff",
|
|
229
|
-
"Weap",
|
|
224
|
+
"_Character_BP", # Creatures / tamed dinos
|
|
225
|
+
"DinoCharacter", # Creature variants
|
|
226
|
+
"PlayerPawn", # Player avatars on the map
|
|
227
|
+
"Buff_", # Active buffs
|
|
228
|
+
"PrimalBuff", # Persistent buff data
|
|
229
|
+
"Weap", # Held weapons
|
|
230
230
|
"StatusComponent", # Character/dino status components
|
|
231
|
-
"Inventory",
|
|
232
|
-
"DroppedItem",
|
|
231
|
+
"Inventory", # Inventory components
|
|
232
|
+
"DroppedItem", # Dropped items
|
|
233
233
|
"DeathItemCache", # Death caches
|
|
234
|
-
"NPCZone",
|
|
234
|
+
"NPCZone", # NPC spawn zones
|
|
235
235
|
"DinoDropInventory", # Dino death drops
|
|
236
236
|
)
|
|
237
237
|
|
|
238
238
|
def get_creatures(self) -> list[GameObject]:
|
|
239
239
|
"""Return all creature objects (tamed **and** wild)."""
|
|
240
|
-
return [
|
|
241
|
-
obj
|
|
242
|
-
for obj in self.objects
|
|
243
|
-
if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name
|
|
244
|
-
]
|
|
240
|
+
return [obj for obj in self.objects if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name]
|
|
245
241
|
|
|
246
242
|
def get_tamed_creatures(self) -> list[GameObject]:
|
|
247
243
|
"""Return tamed creatures (have ``TamingTeamID`` property)."""
|
|
248
|
-
return [
|
|
249
|
-
obj
|
|
250
|
-
for obj in self.get_creatures()
|
|
251
|
-
if obj.get_property_value("TamingTeamID") is not None
|
|
252
|
-
]
|
|
244
|
+
return [obj for obj in self.get_creatures() if obj.get_property_value("TamingTeamID") is not None]
|
|
253
245
|
|
|
254
246
|
def get_wild_creatures(self) -> list[GameObject]:
|
|
255
247
|
"""Return wild creatures (no ``TamingTeamID`` property)."""
|
|
256
|
-
return [
|
|
257
|
-
obj
|
|
258
|
-
for obj in self.get_creatures()
|
|
259
|
-
if obj.get_property_value("TamingTeamID") is None
|
|
260
|
-
]
|
|
248
|
+
return [obj for obj in self.get_creatures() if obj.get_property_value("TamingTeamID") is None]
|
|
261
249
|
|
|
262
250
|
def get_structures(self) -> list[GameObject]:
|
|
263
251
|
"""Return tribe-owned placed structures.
|
|
@@ -294,6 +282,86 @@ class WorldSave:
|
|
|
294
282
|
"""Return objects with ``is_item`` set."""
|
|
295
283
|
return [obj for obj in self.objects if obj.is_item]
|
|
296
284
|
|
|
285
|
+
# ---- Map / engine-placed entities --------------------------------
|
|
286
|
+
|
|
287
|
+
def get_terminals(self) -> list[GameObject]:
|
|
288
|
+
"""Return map-placed terminal objects.
|
|
289
|
+
|
|
290
|
+
Covers tribute terminals (obelisks on The Island, Ragnarok, etc.),
|
|
291
|
+
Extinction city terminals, and any variant using ``TributeTerminal``
|
|
292
|
+
or ``CityTerminal`` in the class name. These are engine-placed and
|
|
293
|
+
have **no** ``TargetingTeam``.
|
|
294
|
+
|
|
295
|
+
Inventory components and item sub-objects attached to terminals are
|
|
296
|
+
excluded — only the top-level structure actors are returned.
|
|
297
|
+
"""
|
|
298
|
+
return [
|
|
299
|
+
obj
|
|
300
|
+
for obj in self.objects
|
|
301
|
+
if ("TributeTerminal" in obj.class_name or "CityTerminal" in obj.class_name)
|
|
302
|
+
and not obj.is_item
|
|
303
|
+
and "Inventory" not in obj.class_name
|
|
304
|
+
and "PrimalItem" not in obj.class_name
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
def get_supply_drops(self) -> list[GameObject]:
|
|
308
|
+
"""Return active supply-drop / loot-crate objects on the map.
|
|
309
|
+
|
|
310
|
+
Matches ``SupplyCrate``, ``OrbitalSupply``, and ``SupplyDrop``
|
|
311
|
+
class-name substrings. Inventory components are excluded.
|
|
312
|
+
"""
|
|
313
|
+
_SUPPLY_PATTERNS = ("SupplyCrate", "OrbitalSupply", "SupplyDrop")
|
|
314
|
+
return [
|
|
315
|
+
obj
|
|
316
|
+
for obj in self.objects
|
|
317
|
+
if any(p in obj.class_name for p in _SUPPLY_PATTERNS)
|
|
318
|
+
and "Inventory" not in obj.class_name
|
|
319
|
+
and not obj.is_item
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
def get_artifact_crates(self) -> list[GameObject]:
|
|
323
|
+
"""Return artifact-crate spawn objects.
|
|
324
|
+
|
|
325
|
+
These are the map-placed crates that contain artifacts for boss
|
|
326
|
+
fights (e.g. ``ArtifactCrate_Desert_Kaiju_EX_C``).
|
|
327
|
+
Inventory components are excluded.
|
|
328
|
+
"""
|
|
329
|
+
return [
|
|
330
|
+
obj
|
|
331
|
+
for obj in self.objects
|
|
332
|
+
if "ArtifactCrate" in obj.class_name and "Inventory" not in obj.class_name and not obj.is_item
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
def get_map_resources(self) -> list[GameObject]:
|
|
336
|
+
"""Return engine-placed resource / vein / node objects.
|
|
337
|
+
|
|
338
|
+
Covers map-specific harvestable resource objects:
|
|
339
|
+
|
|
340
|
+
* Oil veins (The Island, Ragnarok, …) — ``OilVein``
|
|
341
|
+
* Water veins (Scorched Earth, …) — ``WaterVein``
|
|
342
|
+
* Gas veins (Scorched Earth, …) — ``GasVein``
|
|
343
|
+
* Charge nodes (Aberration) — ``ChargeNode``
|
|
344
|
+
* Element veins (Extinction) — ``ElementVein``
|
|
345
|
+
* Beaver dams — ``BeaverDam``
|
|
346
|
+
|
|
347
|
+
Inventory components attached to these are excluded.
|
|
348
|
+
"""
|
|
349
|
+
_RESOURCE_PATTERNS = (
|
|
350
|
+
"OilVein",
|
|
351
|
+
"WaterVein",
|
|
352
|
+
"GasVein",
|
|
353
|
+
"ChargeNode",
|
|
354
|
+
"ElementVein",
|
|
355
|
+
"BeaverDam",
|
|
356
|
+
)
|
|
357
|
+
return [
|
|
358
|
+
obj
|
|
359
|
+
for obj in self.objects
|
|
360
|
+
if any(p in obj.class_name for p in _RESOURCE_PATTERNS)
|
|
361
|
+
and "Inventory" not in obj.class_name
|
|
362
|
+
and not obj.is_item
|
|
363
|
+
]
|
|
364
|
+
|
|
297
365
|
# ------------------------------------------------------------------
|
|
298
366
|
# Properties
|
|
299
367
|
# ------------------------------------------------------------------
|
|
@@ -123,10 +123,7 @@ class GameObjectContainer:
|
|
|
123
123
|
|
|
124
124
|
def get_creatures(self) -> list[GameObject]:
|
|
125
125
|
"""Get all creature objects (tamed and wild)."""
|
|
126
|
-
return [
|
|
127
|
-
obj for obj in self.objects
|
|
128
|
-
if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name
|
|
129
|
-
]
|
|
126
|
+
return [obj for obj in self.objects if "_Character_BP" in obj.class_name or "DinoCharacter" in obj.class_name]
|
|
130
127
|
|
|
131
128
|
def get_items(self) -> list[GameObject]:
|
|
132
129
|
"""Get all item objects."""
|
|
@@ -150,6 +147,64 @@ class GameObjectContainer:
|
|
|
150
147
|
"""Get player character objects on the map."""
|
|
151
148
|
return [obj for obj in self.objects if "PlayerPawn" in obj.class_name]
|
|
152
149
|
|
|
150
|
+
def get_terminals(self) -> list[GameObject]:
|
|
151
|
+
"""Get map-placed terminal objects (tribute terminals, city terminals).
|
|
152
|
+
|
|
153
|
+
Inventory components and item sub-objects are excluded.
|
|
154
|
+
"""
|
|
155
|
+
return [
|
|
156
|
+
obj
|
|
157
|
+
for obj in self.objects
|
|
158
|
+
if ("TributeTerminal" in obj.class_name or "CityTerminal" in obj.class_name)
|
|
159
|
+
and not obj.is_item
|
|
160
|
+
and "Inventory" not in obj.class_name
|
|
161
|
+
and "PrimalItem" not in obj.class_name
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
def get_supply_drops(self) -> list[GameObject]:
|
|
165
|
+
"""Get active supply-drop / loot-crate objects on the map.
|
|
166
|
+
|
|
167
|
+
Inventory components are excluded.
|
|
168
|
+
"""
|
|
169
|
+
_SUPPLY_PATTERNS = ("SupplyCrate", "OrbitalSupply", "SupplyDrop")
|
|
170
|
+
return [
|
|
171
|
+
obj
|
|
172
|
+
for obj in self.objects
|
|
173
|
+
if any(p in obj.class_name for p in _SUPPLY_PATTERNS)
|
|
174
|
+
and "Inventory" not in obj.class_name
|
|
175
|
+
and not obj.is_item
|
|
176
|
+
]
|
|
177
|
+
|
|
178
|
+
def get_artifact_crates(self) -> list[GameObject]:
|
|
179
|
+
"""Get artifact-crate spawn objects. Inventory components are excluded."""
|
|
180
|
+
return [
|
|
181
|
+
obj
|
|
182
|
+
for obj in self.objects
|
|
183
|
+
if "ArtifactCrate" in obj.class_name and "Inventory" not in obj.class_name and not obj.is_item
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
def get_map_resources(self) -> list[GameObject]:
|
|
187
|
+
"""Get engine-placed resource / vein / node objects.
|
|
188
|
+
|
|
189
|
+
Covers oil veins, water veins, gas veins, charge nodes, element
|
|
190
|
+
veins, and beaver dams. Inventory components are excluded.
|
|
191
|
+
"""
|
|
192
|
+
_RESOURCE_PATTERNS = (
|
|
193
|
+
"OilVein",
|
|
194
|
+
"WaterVein",
|
|
195
|
+
"GasVein",
|
|
196
|
+
"ChargeNode",
|
|
197
|
+
"ElementVein",
|
|
198
|
+
"BeaverDam",
|
|
199
|
+
)
|
|
200
|
+
return [
|
|
201
|
+
obj
|
|
202
|
+
for obj in self.objects
|
|
203
|
+
if any(p in obj.class_name for p in _RESOURCE_PATTERNS)
|
|
204
|
+
and "Inventory" not in obj.class_name
|
|
205
|
+
and not obj.is_item
|
|
206
|
+
]
|
|
207
|
+
|
|
153
208
|
def get_players(self) -> list[GameObject]:
|
|
154
209
|
"""Get all player data objects."""
|
|
155
210
|
return self.get_by_class("PrimalPlayerData") + self.find_by_class_pattern("PlayerPawnTest")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arkparser
|
|
3
|
-
Version: 0.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
|
|
@@ -338,6 +338,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
338
338
|
| `get_wild_creatures()` | `list[GameObject]` | Wild creatures only |
|
|
339
339
|
| `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
|
|
340
340
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
|
|
341
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
342
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates on the map |
|
|
343
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
344
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
341
345
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
342
346
|
| `get_objects_by_class(class_name: str)` | `list[GameObject]` | Objects matching class name substring |
|
|
343
347
|
| `get_object_by_guid(guid: str)` | `GameObject \| None` | Lookup by GUID (ASA) |
|
|
@@ -390,6 +394,10 @@ All file parsers support `load(source)` which accepts `str`, `Path`, or `bytes`
|
|
|
390
394
|
| `get_creatures()` | `list[GameObject]` | All creatures |
|
|
391
395
|
| `get_structures()` | `list[GameObject]` | Tribe-owned structures |
|
|
392
396
|
| `get_player_pawns()` | `list[GameObject]` | Player characters on map |
|
|
397
|
+
| `get_terminals()` | `list[GameObject]` | Obelisks / tribute terminals / city terminals |
|
|
398
|
+
| `get_supply_drops()` | `list[GameObject]` | Active supply crates |
|
|
399
|
+
| `get_artifact_crates()` | `list[GameObject]` | Artifact spawn crates |
|
|
400
|
+
| `get_map_resources()` | `list[GameObject]` | Oil/water/gas veins, charge nodes, beaver dams |
|
|
393
401
|
| `get_players()` | `list[GameObject]` | Player data objects |
|
|
394
402
|
| `get_items()` | `list[GameObject]` | Item objects |
|
|
395
403
|
|
|
@@ -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
|
|
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
|