arkparser 0.1.17__tar.gz → 0.3.0__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 (68) hide show
  1. arkparser-0.3.0/PKG-INFO +627 -0
  2. arkparser-0.3.0/README.md +593 -0
  3. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/__init__.py +20 -44
  4. arkparser-0.3.0/arkparser/common/binary_reader.py +290 -0
  5. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/normalization.py +18 -6
  6. arkparser-0.3.0/arkparser/export.py +1575 -0
  7. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/profile.py +23 -1
  8. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/world_save.py +48 -29
  9. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/game_objects/container.py +5 -5
  10. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/game_objects/game_object.py +36 -36
  11. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/game_objects/location.py +1 -1
  12. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/base.py +24 -19
  13. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/byte_property.py +35 -0
  14. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/compound.py +126 -0
  15. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/primitives.py +13 -14
  16. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/property_list.py +15 -7
  17. arkparser-0.3.0/arkparser.egg-info/PKG-INFO +627 -0
  18. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser.egg-info/SOURCES.txt +2 -9
  19. {arkparser-0.1.17 → arkparser-0.3.0}/pyproject.toml +1 -1
  20. arkparser-0.3.0/tests/test_binary_reader_layouts.py +52 -0
  21. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_export.py +92 -120
  22. arkparser-0.3.0/tests/test_v13_property_layouts.py +255 -0
  23. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_world_save.py +29 -20
  24. arkparser-0.1.17/PKG-INFO +0 -856
  25. arkparser-0.1.17/README.md +0 -822
  26. arkparser-0.1.17/arkparser/common/binary_reader.py +0 -406
  27. arkparser-0.1.17/arkparser/export.py +0 -726
  28. arkparser-0.1.17/arkparser/models/__init__.py +0 -29
  29. arkparser-0.1.17/arkparser/models/character.py +0 -230
  30. arkparser-0.1.17/arkparser/models/creature.py +0 -603
  31. arkparser-0.1.17/arkparser/models/item.py +0 -207
  32. arkparser-0.1.17/arkparser/models/player.py +0 -267
  33. arkparser-0.1.17/arkparser/models/stats.py +0 -226
  34. arkparser-0.1.17/arkparser/models/structure.py +0 -176
  35. arkparser-0.1.17/arkparser/models/tribe.py +0 -295
  36. arkparser-0.1.17/arkparser.egg-info/PKG-INFO +0 -856
  37. arkparser-0.1.17/tests/test_models.py +0 -242
  38. {arkparser-0.1.17 → arkparser-0.3.0}/LICENSE +0 -0
  39. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/__init__.py +0 -0
  40. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/exceptions.py +0 -0
  41. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/map_config.py +0 -0
  42. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/types.py +0 -0
  43. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/common/version_detection.py +0 -0
  44. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/data_models.py +0 -0
  45. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/__init__.py +0 -0
  46. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/base.py +0 -0
  47. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/cloud_inventory.py +0 -0
  48. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/files/tribe.py +0 -0
  49. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/game_objects/__init__.py +0 -0
  50. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/__init__.py +0 -0
  51. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/properties/registry.py +0 -0
  52. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/__init__.py +0 -0
  53. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/base.py +0 -0
  54. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/colors.py +0 -0
  55. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/misc.py +0 -0
  56. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/registry.py +0 -0
  57. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser/structs/vectors.py +0 -0
  58. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser.egg-info/dependency_links.txt +0 -0
  59. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser.egg-info/requires.txt +0 -0
  60. {arkparser-0.1.17 → arkparser-0.3.0}/arkparser.egg-info/top_level.txt +0 -0
  61. {arkparser-0.1.17 → arkparser-0.3.0}/setup.cfg +0 -0
  62. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_binary_reader.py +0 -0
  63. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_cloud_inventory.py +0 -0
  64. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_data_models.py +0 -0
  65. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_game_objects.py +0 -0
  66. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_profile.py +0 -0
  67. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_tribe.py +0 -0
  68. {arkparser-0.1.17 → arkparser-0.3.0}/tests/test_version_detection.py +0 -0
@@ -0,0 +1,627 @@
1
+ Metadata-Version: 2.4
2
+ Name: arkparser
3
+ Version: 0.3.0
4
+ Summary: Pure Python parser for ARK: Survival Evolved and ARK: Survival Ascended save files
5
+ Author: Vertyco
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/vertyco/arkparser
8
+ Project-URL: Repository, https://github.com/vertyco/arkparser
9
+ Project-URL: Documentation, https://github.com/vertyco/arkparser#readme
10
+ Project-URL: Issues, https://github.com/vertyco/arkparser/issues
11
+ Keywords: ark,survival,evolved,ascended,savegame,parser,binary,ase,asa,game,save-file
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Games/Entertainment
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=9.0; extra == "dev"
30
+ Requires-Dist: ruff>=0.15; extra == "dev"
31
+ Requires-Dist: build>=1.4; extra == "dev"
32
+ Requires-Dist: twine>=6.0; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # ARK Save Parser
36
+
37
+ [![Python 3.10](https://img.shields.io/badge/python-3.10-blue?logo=python&logoColor=white)](https://www.python.org/)
38
+ [![Python 3.11](https://img.shields.io/badge/python-3.11-blue?logo=python&logoColor=white)](https://www.python.org/)
39
+ [![Python 3.12](https://img.shields.io/badge/python-3.12-blue?logo=python&logoColor=white)](https://www.python.org/)
40
+ [![Python 3.13](https://img.shields.io/badge/python-3.13-blue?logo=python&logoColor=white)](https://www.python.org/)
41
+ [![Python 3.14](https://img.shields.io/badge/python-3.14-blue?logo=python&logoColor=white)](https://www.python.org/)
42
+
43
+ ![Typed](https://img.shields.io/badge/typing-py.typed-brightgreen)
44
+ ![No Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)
45
+ ![Ruff](https://img.shields.io/badge/style-ruff-D7FF64?logo=ruff&logoColor=D7FF64)
46
+ ![license](https://img.shields.io/github/license/Vertyco/arkparser)
47
+
48
+ A pure-Python library for parsing ARK: Survival Evolved (ASE) and ARK: Survival Ascended (ASA) save files. Zero third-party dependencies.
49
+
50
+ ---
51
+
52
+ ## Features
53
+
54
+ - **Player Profiles** (`.arkprofile`): platform gamertag, character name, level, stats, engrams
55
+ - **Tribe Data** (`.arktribe`): members, ranks, logs, alliances
56
+ - **Cloud Inventory / Obelisk**: uploaded creatures, items, cryopod contents
57
+ - **World Saves** (`.ark`): full map state (creatures, structures, items, players)
58
+ - **Dual format**: automatic ASE (v5-12) / ASA (v13-14+, SQLite) detection
59
+ - **Legacy-parity export**: drop-in JSON output matching `ASVExport.exe` schema, plus parser-only extras under `extra_*` keys
60
+ - **Fast**: pure-Python `BinaryReader` (`int.from_bytes` + `struct.Struct` unpackers, slots-based dataclasses) — a 30 MB ASE save (65k objects) loads in ~3s on CPython 3.14
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install arkparser
66
+ # or editable install for development
67
+ pip install -e .
68
+ ```
69
+
70
+ ## Quick Start
71
+
72
+ ### Player Profile
73
+
74
+ ```python
75
+ from arkparser import Profile
76
+
77
+ profile = Profile.load("path/to/player.arkprofile") # auto-detects ASE/ASA
78
+
79
+ print(profile.player_name) # Platform gamertag / display name
80
+ print(profile.character_name) # In-game character name
81
+ print(profile.level) # 105
82
+ print(profile.tribe_id) # 1729028872
83
+ print(profile.engram_blueprints) # ["EngramEntry_Campfire_C", ...]
84
+ print(profile.get_stat(0)) # Health level-up points
85
+ print(profile.to_dict()) # Full dict export
86
+ ```
87
+
88
+ ### Tribe Data
89
+
90
+ ```python
91
+ from arkparser import Tribe
92
+
93
+ tribe = Tribe.load("path/to/tribe.arktribe")
94
+
95
+ print(tribe.name) # "My Tribe"
96
+ print(tribe.tribe_id) # 1729028872
97
+ print(tribe.member_count) # 3
98
+ for member in tribe.get_members():
99
+ print(f" {member['name']} (ID: {member['player_id']})")
100
+ print(tribe.log_entries) # ["Day 45: Tamed a Rex", ...]
101
+ ```
102
+
103
+ ### Cloud Inventory / Obelisk
104
+
105
+ ```python
106
+ from arkparser import CloudInventory
107
+
108
+ inv = CloudInventory.load("path/to/obelisk_file") # or use Obelisk alias
109
+
110
+ for creature in inv.uploaded_creatures:
111
+ print(f" {creature.species} Lv{creature.level} - {creature.name}")
112
+
113
+ for item in inv.uploaded_items:
114
+ print(f" {item.display_name} x{item.quantity} ({item.quality_name})")
115
+ if item.is_cryopod and item.cryopod_creature:
116
+ cryo = item.cryopod_creature
117
+ print(f" Contains: {cryo.species} Lv{cryo.level}")
118
+ ```
119
+
120
+ ### World Save
121
+
122
+ ```python
123
+ from arkparser import WorldSave
124
+
125
+ save = WorldSave.load("path/to/Extinction.ark") # ASE
126
+ save = WorldSave.load("path/to/Extinction_WP.ark") # ASA
127
+
128
+ print(f"Objects: {save.object_count}")
129
+ print(f"Creatures: {len(save.get_creatures())}")
130
+ print(f"Structures: {len(save.get_structures())}")
131
+ print(f"Parse errors: {save.parse_error_count}")
132
+ print(f"Is ASA: {save.is_asa}")
133
+ ```
134
+
135
+ ### Cryopod-stored Creatures
136
+
137
+ Tamed creatures stored inside in-world cryopods aren't returned by `get_tamed_creatures()`. Iterate them via `WorldSave.iter_cryopod_creatures()`:
138
+
139
+ ```python
140
+ for item_obj, cryo in save.iter_cryopod_creatures():
141
+ print(f"{cryo.species} Lv{cryo.level} (in {item_obj.class_name})")
142
+ ```
143
+
144
+ ### Direct GameObject Access
145
+
146
+ `arkparser` exposes the raw `GameObject` graph for callers that need flexibility beyond the default exports:
147
+
148
+ ```python
149
+ for obj in save.get_tamed_creatures():
150
+ name = obj.get_property_value("TamedName") or "<unnamed>"
151
+ tribe = obj.get_property_value("TargetingTeam") or 0
152
+ print(f"{obj.class_name} - {name} (tribe {tribe})")
153
+ ```
154
+
155
+ ### JSON Export (Legacy ASV Schema)
156
+
157
+ ```python
158
+ from arkparser import export_all, export_to_files
159
+ from arkparser.common import get_map_config
160
+
161
+ map_config = get_map_config("extinction.ark")
162
+
163
+ data = export_all(save, map_config)
164
+ # {"ASV_Tamed": [...], "ASV_Wild": [...], "ASV_Players": [...],
165
+ # "ASV_Tribes": [...], "ASV_Structures": [...], "ASV_TribeLogs": [...],
166
+ # "ASV_MapStructures": [...]}
167
+
168
+ export_to_files(save, "output/", map_config)
169
+ # Writes ASV_Tamed.json, ASV_Wild.json, ..., ASV_MapStructures.json
170
+ # Each file is wrapped with the legacy `{map, day, time, data}` envelope.
171
+ ```
172
+
173
+ Each export wraps a flat list of record dicts in the legacy `{map, day, time, data}` envelope (with `wrap=True`). Tables below document every field in each record. Fields present in the original `ASVExport.exe` output are marked **legacy**; fields added by this parser are marked **added** and use plain descriptive names (no namespace prefix).
174
+
175
+ #### Stat-token convention
176
+
177
+ The parser reuses the legacy short stat tokens consistently across every export and every stat block:
178
+
179
+ `hp` `stam` `torp` `oxy` `food` `water` `temp` `weight` `melee` `speed` `fort` `craft`
180
+
181
+ These map 1:1 to in-game stat indices 0..11. Stat blocks use suffixes to disambiguate:
182
+
183
+ - **no suffix** — base wild stats (wild creatures, players)
184
+ - **`-w`** — base wild stats (tamed creatures, points the creature had before being tamed)
185
+ - **`-t`** — post-tame level-up points (tamed creatures only)
186
+ - **`-m`** — mutation points applied (tamed creatures only)
187
+
188
+ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`, `weight`, `speed`, `food`, `oxy`, `craft`) under each suffix. The parser appends the four legacy never surfaced (`torp`, `water`, `temp`, `fort`) at the end of each block, so e.g. a tamed creature now carries `torp-w`, `water-w`, `temp-w`, `fort-w` alongside the legacy `hp-w`..`craft-w`.
189
+
190
+ #### `ASV_Tamed` schema
191
+
192
+ | Field | Origin | Source / formula |
193
+ |---|---|---|
194
+ | `id` | legacy | `(DinoID1 << 32) \| DinoID2` |
195
+ | `tribeid` | legacy | `TargetingTeam` |
196
+ | `tribe` | legacy | `TribeName` (or `null` when blank) |
197
+ | `tamer` | legacy | `TamerString` |
198
+ | `imprinter` | legacy | `ImprinterName` |
199
+ | `imprint` | legacy | `DinoImprintingQuality` (status) |
200
+ | `creature` | legacy | `GameObject.class_name` |
201
+ | `name` | legacy | `TamedName` |
202
+ | `sex` | legacy | `"Female"` if `bIsFemale` else `"Male"` |
203
+ | `base` | legacy | `BaseCharacterLevel` (status) |
204
+ | `lvl` | legacy | `BaseCharacterLevel + ExtraCharacterLevel` |
205
+ | `lat`, `lon` | legacy | `MapConfig.ue_to_lat(y)` / `ue_to_lon(x)` |
206
+ | `hp-w` .. `craft-w` | legacy | `NumberOfLevelUpPointsApplied[i]` (wild stat points) |
207
+ | `torp-w`, `water-w`, `temp-w`, `fort-w` | added | same source, indices legacy never emitted |
208
+ | `hp-t` .. `craft-t` | legacy | `NumberOfLevelUpPointsAppliedTamed[i]` |
209
+ | `torp-t`, `water-t`, `temp-t`, `fort-t` | added | same source, indices legacy never emitted |
210
+ | `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
211
+ | `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
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` |
214
+ | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
215
+ | `dinoid` | legacy | string form of `id` |
216
+ | `isMating` | legacy | `bEnableTamedMating` |
217
+ | `isNeutered` | legacy | `bNeutered` |
218
+ | `isClone` | legacy | `bIsClone` or `bIsCloneDino` |
219
+ | `tamedServer` | legacy | `TamedOnServerName` |
220
+ | `uploadedServer` | legacy | `UploadedFromServerName` |
221
+ | `maturation` | legacy | `str(int(BabyAge * 100))` (string for legacy parity) |
222
+ | `traits` | legacy | `CreatureTraits` (full list of mutation trait class names) |
223
+ | `inventory` | legacy | items from `MyInventoryComponent.InventoryItems`. Each entry carries `itemId`, `qty`, `blueprint`. When the item is a **cryopod / soultrap / vivarium / dinoball** with an embedded creature, the entry is enriched with `dino_id` (combined 64-bit id matching the corresponding `ASV_Tamed` record), `dino_creature` (species / class name), and `dino_name` (`TamedName` if set). Cryopods stored in containers (cryofridges, vaults, dedicated storage) get the same enrichment. |
224
+ | `father_id`, `mother_id` | added | combined dino id from the first `DinoAncestors` entry (`null` when missing) |
225
+ | `father_name`, `mother_name` | added | name strings from the first `DinoAncestors` entry |
226
+ | `level_added` | added | `ExtraCharacterLevel` (post-tame levels, broken out from `lvl`) |
227
+ | `experience` | added | `ExperiencePoints` (status), integer |
228
+ | `wandering` | added | `bEnableTamedWandering` |
229
+ | `tamed_at` | added | ISO 8601 datetime with local TZ, converted from `TamedAtTime` (in-game seconds) via `file_mtime + (tamed_at - game_time)`. `null` when the save lacks the anchors. |
230
+ | `last_ally_in_range` | added | ISO 8601 datetime with local TZ — converted from `LastInAllyRangeTime` / `LastInAllyRangeSerialized` via the same anchor formula as `tamed_at`. `null` when the save lacks the anchors. |
231
+ | `imprinter_player_id` | added | `ImprinterPlayerDataID` |
232
+ | `imprinter_net_id` | added | `ImprinterPlayerUniqueNetId` (ASA only) |
233
+ | `taming_team_id` | added | `TamingTeamID` (fallback tribe id when `TargetingTeam` was stripped on cryo) |
234
+ | `owning_player_id`, `owning_player_name` | added | `OwningPlayerID` / `OwningPlayerName` — current owner (may differ from the original `tamer` after transfer / cryo). |
235
+ | `aggression_level` | added | `TamedAggressionLevel` — 0 Passive, 1 Neutral, 2 Aggressive, 3 Passive Flee, 4 Attack-My-Target (ARK in-game order). |
236
+ | `ai_targeting_range` | added | `TamedAITargetingRange` — aggro range (UE units). |
237
+ | `follow_stopping_distance` | added | `FollowStoppingDistance` — follow-AI stopping radius. |
238
+ | `is_flying` | added | `bIsFlying`. |
239
+ | `is_turret_mode` | added | `bIsInTurretMode` — e.g. plant Y, Tek tape sentry mode. |
240
+ | `ignore_whistles`, `only_target_conscious`, `attack_team_member_dinos` | added | `bIgnoreAllWhistles` / `bOnlyTargetConscious` / `bAttackTeamMemberDinos` — behavior toggles. |
241
+ | `next_cuddle_food`, `next_cuddle_type` | added | `BabyCuddleFood` / `BabyCuddleType` — next imprint requirement. |
242
+ | `latest_uploaded_server`, `previous_uploaded_server` | added | `LatestUploadedFromServerName` / `PreviousUploadedFromServerName` — recent upload history alongside the legacy `uploadedServer`. |
243
+ | `saddle_structures` | added | List of structure object-ref strings placed on this creature's platform saddle (paracer / brontosaurus / titanosaur etc.). |
244
+ | `harvest_resource_levels` | added | `HarvestResourceLevels` — per-resource harvest levels (mortar / feeding trough variants). |
245
+ | `wild_spawn_region` | added | `OriginalNPCVolumeName` — name of the `NPCZoneVolume` where this creature first spawned. Lets consumers answer "where on the map did this tame originate". |
246
+ | `downloaded_at` | added | ISO 8601 datetime of the last cluster/obelisk download (`DinoDownloadedAtTime`). `null` when the creature was never cluster-downloaded. |
247
+ | `original_created` | added | ISO 8601 datetime of the dino's first spawn (`OriginalCreationTime`), e.g. when the egg was laid or the wild dino spawned. Distinct from `tamed_at`. |
248
+ | `next_mating_at` | added | ISO 8601 datetime when the next mating is allowed (`NextAllowedMatingTime`). `null` when no cooldown is set. |
249
+ | `last_stasis` | added | ISO 8601 datetime of `LastEnterStasisTime` (last time the creature entered stasis). |
250
+ | `last_baby_age_update` | added | ISO 8601 datetime of `LastUpdatedBabyAgeAtTime`. |
251
+ | `last_gestation_update` | added | ISO 8601 datetime of `LastUpdatedGestationAtTime`. |
252
+ | `next_cuddle` | added | ISO 8601 datetime of `BabyNextCuddleTime`. |
253
+
254
+ #### `ASV_Wild` schema
255
+
256
+ | Field | Origin | Source / formula |
257
+ |---|---|---|
258
+ | `id`, `dinoid` | legacy | `(DinoID1 << 32) \| DinoID2` and its string form |
259
+ | `creature` | legacy | `GameObject.class_name` |
260
+ | `sex` | legacy | `"Female"` if `bIsFemale` else `"Male"` |
261
+ | `lvl` | legacy | `BaseCharacterLevel` (status) |
262
+ | `lat`, `lon`, `ccc` | legacy | location, via `MapConfig` |
263
+ | `hp` .. `craft` | legacy | `NumberOfLevelUpPointsApplied[i]` (8 visible stats) |
264
+ | `torp`, `water`, `temp`, `fort` | added | same source, indices legacy never emitted |
265
+ | `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
266
+ | `tameable` | legacy | mirror of legacy `ContentWildCreature.IsTameable` rule |
267
+ | `trait` | legacy | first entry of `CreatureTraits` (or empty string) |
268
+ | `traits` | added | full `CreatureTraits` list |
269
+ | `wild_spawn_region` | added | `OriginalNPCVolumeName` — `NPCZoneVolume` the creature spawned in. |
270
+
271
+ #### Player data: `.arkprofile` vs in-world pawn
272
+
273
+ ARK keeps player data in **two distinct places**, and the parser builds `ASV_Players` records from whichever source you hand it:
274
+
275
+ | Source | Persistence | Coverage | Has location? | Has inventory? |
276
+ |---|---|---|---|---|
277
+ | **`.arkprofile`** parsed via `Profile.load(...)` | Survives logout, server restart, and character death | Every player who ever logged in | No (profile carries no in-world position) | No (profile only stores engrams / hairstyles / unlocked customizations, not the live inventory) |
278
+ | **In-world `PlayerPawn`** GameObject inside `WorldSave` | Only present while the character is spawned on the map (i.e. not logged out / dead with corpse cleared) | Currently-online or recently-disconnected players | Yes (`obj.location` → `lat`/`lon`/`ccc`) | Yes (live `MyInventoryComponent` contents) |
279
+
280
+ The export pipeline (`export_players`) loops over `save.profiles` (a list the caller assembles). For each entry:
281
+
282
+ - If it's a `Profile` instance → `_player_from_profile` runs: fills core identity (name, gender, level, stats, tribe id, engram count, experience, active datetime from `LastLoginTime`). Leaves `lat`/`lon`/`ccc` as zero, `inventory` empty, pawn-state flags (`is_sleeping`, `is_dead`, `chibi_levels`, …) absent.
283
+ - Otherwise it's treated as a wrapped `(profile, objects)` pair pointing at an in-world `PlayerPawn` → `_player_from_object` runs: fills location, inventory, pawn-state flags, body/hair cosmetics, death timestamps, and `active` from `SavedLastTimeHadController`.
284
+
285
+ For the richest output, hand `export_players` **both** — assemble a wrapper for each player that carries the `Profile` (for offline identity) *and* the in-world pawn (when present) so the merged record gets identity + live state. The current validation script only passes `Profile` instances, which is why fields like `is_sleeping` / `body_colors` / `current_weapon` show up empty in the validation output even though the parser supports them.
286
+
287
+ #### `ASV_Players` schema
288
+
289
+ | Field | Origin | Source / formula |
290
+ |---|---|---|
291
+ | `playerid` | legacy | `PlayerDataID` |
292
+ | `steam` | legacy | platform gamertag (`PlatformProfileName` / profile `PlayerName`) |
293
+ | `name` | legacy | in-game character name |
294
+ | `tribeid` | legacy | `TribeId` / `TribeID` |
295
+ | `tribe` | legacy | tribe name |
296
+ | `sex` | legacy | `"Female"` / `"Male"` |
297
+ | `lvl` | legacy | `BaseCharacterLevel + ExtraCharacterLevel` |
298
+ | `lat`, `lon` | legacy | `0.0` (profile/cluster files carry no in-world location) |
299
+ | `hp` .. `craft`, `fort` | legacy | `NumberOfLevelUpPointsApplied[i]` (10 visible stats) |
300
+ | `torp`, `temp` | added | same source, indices legacy never emitted |
301
+ | `active` | legacy | last-active datetime; currently `null` (placeholder for parity) |
302
+ | `ccc` | legacy | `"0 0 0"` (no in-world position from profile/cluster source) |
303
+ | `achievements`, `inventory`, `netAddress` | legacy | reserved arrays/string (currently empty for parity) |
304
+ | `steamid`, `dataFile` | legacy | platform net id and `{steamid}.arkprofile` filename |
305
+ | `active` | legacy (now populated) | ISO 8601 datetime of last login, converted from profile `LastLoginTime` or in-world pawn `SavedLastTimeHadController`. Legacy schema reserved the field but the old C# exporter only filled it for in-world pawns; the parser fills it for profiles too. `null` when neither source is present. |
306
+ | `lat`, `lon`, `ccc` | legacy (now populated) | In-world position when the record was built from a `PlayerPawn` GameObject. Records sourced from profile / cluster files keep the legacy `0` / `"0 0 0"` placeholders since profiles carry no world position. |
307
+ | `inventory` | legacy (now populated) | Items from the pawn's `MyInventoryComponent` when built from an in-world pawn. Empty list otherwise. |
308
+ | `engram_points` | added | `TotalEngramPoints` |
309
+ | `experience` | added | `ExperiencePoints` (status), integer |
310
+ | `is_sleeping`, `is_dead` | added | `bIsSleeping` / `bIsDead` — pawn state flags. Always `false` for profile-sourced records (no pawn). |
311
+ | `is_prone`, `is_crouched`, `hat_hidden` | added | `bIsProne` / `bIsCrouched` / `bHatHidden`. |
312
+ | `current_weapon` | added | `CurrentWeapon` ref — equipped weapon identifier. |
313
+ | `seated_on_ref` | added | `SeatingStructure` ref — what the player is sitting on (chair / saddle). |
314
+ | `original_hair_color` | added | `OriginalHairColor` — color index at character creation. |
315
+ | `head_hair_growth`, `facial_hair_growth` | added | `PercentOfFullHeadHairGrowth` / `PercentOfFullFacialHairGrowth`. |
316
+ | `body_colors` | added | `BodyColors` — per-region skin color indices. |
317
+ | `died_at` | added | ISO 8601 datetime of `LocalDiedAtTime`. |
318
+ | `corpse_destruction` | added | ISO 8601 datetime of `CorpseDestructionTime`. |
319
+ | `chibi_levels` | added | `NumChibiLevelUps` — bonus levels from chibi pets. |
320
+ | `ascensions_scorched` | added | `NumAscensionsScorched` — ASE ascension counter (legacy `ContentPlayer` parses the ASA ascension block differently; this is the ASE-specific field). |
321
+
322
+ #### `ASV_Tribes` schema
323
+
324
+ | Field | Origin | Source / formula |
325
+ |---|---|---|
326
+ | `tribeid` | legacy | `TribeID` / parser `Tribe.tribe_id` |
327
+ | `tribe` | legacy | tribe name |
328
+ | `players` | legacy | member count |
329
+ | `members` | legacy | list of `{ign, lvl, playerid, playername, steamid}`. `lvl` and `steamid` only populate when a matching `.arkprofile` is loaded into `save.profiles` — the `.arktribe` file itself doesn't carry per-member level / platform id, so the parser cross-references by `player_id`. Empty (`""`) when no profile is available for that member. |
330
+ | `tames`, `structures` | legacy | counts derived from `WorldSave` (creatures + structures whose `TargetingTeam` matches) |
331
+ | `uploadedTames` | legacy | reserved (currently `0`) |
332
+ | `active` | legacy | ISO 8601 datetime of the most recent tribe log entry, converted from in-game "Day N, HH:MM:SS" via the save's anchor. `null` when no parseable log entries or anchors are missing. |
333
+ | `dataFile` | legacy | `"{tribeid}.arktribe"` filename pattern. |
334
+ | `owner_id` | added | `OwnerPlayerDataID` / parser `Tribe.owner_player_id` |
335
+ | `owner_name` | added | `OwnerPlayerName` (object form only; parser-tribe form has no equivalent) |
336
+ | `alliance_ids` | added | `TribeAlliances[i]` |
337
+
338
+ #### `ASV_TribeLogs` schema
339
+
340
+ | Field | Origin | Source / formula |
341
+ |---|---|---|
342
+ | `tribeid` | legacy | `TribeID` / `Tribe.tribe_id` |
343
+ | `tribe` | legacy | tribe name |
344
+ | `logs` | legacy | raw `TribeLog` strings (newest-first, formatted by ARK with `Day N, HH:MM:SS: <message>`) |
345
+
346
+ #### `ASV_Structures` schema
347
+
348
+ | Field | Origin | Source / formula |
349
+ |---|---|---|
350
+ | `id` | added | `GameObject.id` — internal numeric identifier. Cross-references the values in other records' `linked_structures` / `saddle_structures` / `attached_dino_id` lists. |
351
+ | `tribeid` | legacy | `TargetingTeam` |
352
+ | `tribe` | legacy | `OwnerName` |
353
+ | `struct` | legacy | `GameObject.class_name` |
354
+ | `name` | legacy | `BoxName` (empty when it matches the class name, mirroring legacy's no-rename strip) |
355
+ | `locked` | legacy | `bIsPinLocked` or `bIsLocked` |
356
+ | `created` | legacy (richer) | ISO 8601 datetime with the local TZ of the parser machine, computed `save.file_mtime + (OriginalCreationTime - game_time)` (mirrors legacy `ContentContainer.GetApproxDateTimeOf`). `null` when the anchors are missing. |
357
+ | `inventory` | legacy | items from `MyInventoryComponent.InventoryItems` |
358
+ | `lat`, `lon`, `ccc` | legacy | location via `MapConfig`, **rounded to 2 decimals** (parser-only nicety, not legacy parity) |
359
+ | `powered` | added | `bIsPowered` or `bHasFuel` |
360
+ | `switched_on` | added | `bContainerActivated` (lamps / fridges / etc.) |
361
+ | `decay_reset` | added | `bHasResetDecayTime` |
362
+ | `last_ally_in_range_seconds` | added | raw `LastInAllyRangeTime` / `LastInAllyRangeTimeSerialized` / `LastInAllyRangeSerialized` (in-game seconds, float) |
363
+ | `last_ally_in_range` | added | ISO 8601 datetime with local TZ. `null` when the save lacks the anchors. |
364
+ | `painting_id` | added | `UniquePaintingId` |
365
+ | `feeding_inclusions` | added | `FeedingDinoList` class names when `DinoFeedingListType == 1` (ASA feeding troughs) |
366
+ | `feeding_exclusions` | added | `FeedingDinoList` class names when `DinoFeedingListType == 2` |
367
+ | `health` | added | `Health` (often `0.0` because ARK strips live health on save) |
368
+ | `max_health` | added | `MaxHealth` |
369
+ | `owning_player_id`, `owning_player_name` | added | `OwningPlayerID` / `OwningPlayerName` — who placed the structure (distinct from `tribe`, which is the current owning tribe). |
370
+ | `colors` | added | `StructureColors` — list of 6 color region indices (0 = unpainted). |
371
+ | `current_item_count`, `max_item_count` | added | `CurrentItemCount` / `MaxItemCount` — container fullness (e.g. dedicated storage, generators). Both `0` for non-container structures. |
372
+ | `num_bullets` | added | `NumBullets` — loaded ammo count on auto-turrets. `0` for non-turrets. |
373
+ | `range_setting` | added | `RangeSetting` — turret targeting range tier (`0` Low, `1` Med, `2` High, `3` Highest). `0` for non-turrets too — disambiguate via `num_bullets` / `struct`. |
374
+ | `has_fuel` | added | `bHasFuel` — generator / lamp fuel state. |
375
+ | `is_foundation` | added | `bIsFoundation`. |
376
+ | `placement_snapped` | added | `bWasPlacementSnapped` — placement-snap flag. |
377
+ | `variant` | added | `CurrentVariant` — structure variant index (e.g. flexible pipe / gate style). |
378
+ | `selected_resource_class` | added | `SelectedResourceClass` ref — resource type chosen on dedicated storage / similar. |
379
+ | `resource_count` | added | `ResourceCount`. |
380
+ | `dedicated_storage_version` | added | `SavedDedicatedStorageVersion`. |
381
+ | `painting_ref` | added | `PaintingComponent` ref — canvas component identifier when painted. |
382
+ | `saddle_dino_ref`, `attached_dino_id` | added | `SaddleDino` ref + `AttachedToDinoID1/2` combined — links saddle-mounted structures back to their host dino. |
383
+ | `linked_structures` | added | `LinkedStructures` — refs of network-linked structures (pipes / wires / gates → motors). Useful for reconstructing infrastructure topology. |
384
+ | `last_activated` | added | ISO 8601 datetime of `LastActivatedTime`. |
385
+ | `last_deactivated` | added | ISO 8601 datetime of `LastDeactivatedTime`. |
386
+ | `last_fire` | added | ISO 8601 datetime of `LastFireTime` — last turret discharge. |
387
+ | `last_reload` | added | ISO 8601 datetime of `LastLongReloadStartTime`. |
388
+ | `last_fuel_check` | added | ISO 8601 datetime of `LastCheckedFuelTime`. |
389
+ | `pin_code` | added | `CurrentPinCode` — the active PIN code on the structure as a single integer. Falls back to the first non-zero entry of the legacy `CurrentPinCodes` array (always zero on every observed save). Pruned when `0`. **Sensitive credential** — exposed for tribe-admin auditing; downstream UIs should gate this behind admin-only views. |
390
+
391
+ #### `ASV_MapStructures` schema
392
+
393
+ | Field | Origin | Source / formula |
394
+ |---|---|---|
395
+ | `struct` | legacy | ASV label assigned by class-name match (e.g. `ASV_Terminal`, `ASV_BeaverDam`, `ASV_Artifact`) |
396
+ | `lat`, `lon`, `ccc` | legacy | location via `MapConfig` |
397
+ | `inventory` | legacy | inventory items (only meaningful for beaver dams / dropped supply crates) |
398
+
399
+ ### Cluster-uploaded creatures
400
+
401
+ To match the legacy exporter's behaviour of including cluster-uploaded tames (creatures stored in obelisk/cloud-inventory files), pass the cluster directory to `export_all` / `export_to_files`:
402
+
403
+ ```python
404
+ export_to_files(save, "output/", map_config, cluster="path/to/cluster")
405
+ # or equivalently:
406
+ data = export_all(save, map_config, cluster="path/to/cluster")
407
+ # data["ASV_Tamed"] now includes both world tames and cluster uploads.
408
+ ```
409
+
410
+ Pre-loaded `CloudInventory` instances also work (`cluster=[inv1, inv2, ...]`). The low-level helper `export_cluster_uploads(cluster_invs, map_config)` is still available if you need cluster-only output.
411
+
412
+ ## Package Structure
413
+
414
+ ```
415
+ arkparser/
416
+ ├── __init__.py # Public API
417
+ ├── data_models.py # UploadedCreature, UploadedItem, CryopodCreature, DinoStats
418
+ ├── export.py # Legacy-parity JSON export functions
419
+ ├── common/ # Binary reader, types, exceptions, map configs, version detection
420
+ ├── files/ # File parsers (Profile, Tribe, CloudInventory, WorldSave)
421
+ ├── game_objects/ # GameObject, GameObjectContainer, LocationData
422
+ ├── properties/ # Property parsing system (ArrayProperty, StructProperty, etc.)
423
+ └── structs/ # Struct types (Vector, Color, Guid, etc.)
424
+ ```
425
+
426
+ ## API Reference
427
+
428
+ ### File Parsers
429
+
430
+ All file parsers expose `load(source)` which accepts `str`, `Path`, or `bytes` and auto-detects ASE/ASA format.
431
+
432
+ #### Profile (`arkparser.Profile`)
433
+
434
+ Parser for `.arkprofile` player profile files.
435
+
436
+ | Property | Type | Description |
437
+ |---|---|---|
438
+ | `player_name` | `str \| None` | Platform gamertag / display name |
439
+ | `character_name` | `str \| None` | In-game character name (falls back to `player_name`) |
440
+ | `player_id` | `int \| None` | Unique player ID |
441
+ | `unique_id` | `str \| None` | Platform ID (Steam/Xbox numeric ID) |
442
+ | `tribe_id` | `int \| None` | Tribe ID (auto-tribe = `player_id` when no explicit tribe) |
443
+ | `is_female` | `bool \| None` | Gender flag |
444
+ | `level` | `int` | Current level |
445
+ | `experience` | `float` | Total XP |
446
+ | `total_engram_points` | `int` | Engram points spent |
447
+ | `engram_blueprints` | `list[str]` | Learned engram blueprint paths |
448
+ | `objects` | `list[GameObject]` | Raw parsed game objects |
449
+
450
+ `get_stat(index)` returns the level-up points allocated to the given stat (0=Health … 11=Crafting). `to_dict()` returns a flat profile dictionary.
451
+
452
+ #### Tribe (`arkparser.Tribe`)
453
+
454
+ Parser for `.arktribe` tribe data files.
455
+
456
+ | Property | Type | Description |
457
+ |---|---|---|
458
+ | `name` | `str \| None` | Tribe name |
459
+ | `tribe_id` | `int \| None` | Unique tribe ID |
460
+ | `owner_player_id` | `int \| None` | Tribe owner's player ID |
461
+ | `member_ids` / `member_names` / `member_ranks` | `list[...]` | Member arrays |
462
+ | `member_count` | `int` | Number of members |
463
+ | `log_entries` | `list[str]` | Raw tribe log strings |
464
+ | `alliance_ids` | `list[int]` | Allied tribe IDs |
465
+ | `government_type` | `int` | 0=Player, 1=Tribe, 2=Personal |
466
+
467
+ `get_members()` returns a list of `{player_id, name, rank}` dicts.
468
+
469
+ #### CloudInventory (`arkparser.CloudInventory`, alias `Obelisk`)
470
+
471
+ Parser for obelisk / cloud-inventory data files.
472
+
473
+ | Property | Type | Description |
474
+ |---|---|---|
475
+ | `uploaded_creatures` | `list[UploadedCreature]` | Uploaded creatures with stats |
476
+ | `uploaded_items` | `list[UploadedItem]` | Uploaded items (includes cryopods) |
477
+ | `creatures` / `items` / `characters` | `list[GameObject]` | Raw GameObjects |
478
+ | `creature_count` / `item_count` / `character_count` | `int` | Counts |
479
+
480
+ #### WorldSave (`arkparser.WorldSave`)
481
+
482
+ Unified parser for `.ark` world save files. Auto-detects ASE binary vs ASA SQLite.
483
+
484
+ | Property | Type | Description |
485
+ |---|---|---|
486
+ | `version` | `int` | Save format version |
487
+ | `game_time` | `float` | In-game time (seconds) |
488
+ | `save_count` | `int` | Save counter (ASE v9+) |
489
+ | `is_asa` | `bool` | Whether ASA SQLite format |
490
+ | `objects` | `list[GameObject]` | All parsed game objects |
491
+ | `object_count` | `int` | Total object count |
492
+ | `parse_error_count` / `parse_errors` | `int` / `list[str]` | Errors encountered |
493
+ | `container` | `GameObjectContainer` | Relationship-aware container |
494
+ | `actor_locations` | `dict[str, LocationData]` | GUID → location (ASA only) |
495
+ | `data_files` | `list[str]` | External data file references |
496
+ | `name_table` | `list[str] \| dict[int, str]` | Deduplicated name strings |
497
+
498
+ Key methods:
499
+
500
+ | Method | Returns | Description |
501
+ |---|---|---|
502
+ | `load(source, load_properties=True, max_objects=None)` | `WorldSave` | Load and parse a world save |
503
+ | `get_creatures()` | `list[GameObject]` | All creatures |
504
+ | `get_tamed_creatures()` / `get_wild_creatures()` | `list[GameObject]` | Filtered creature sets |
505
+ | `get_structures()` | `list[GameObject]` | Tribe-owned placed structures |
506
+ | `get_player_pawns()` | `list[GameObject]` | Player characters on the map |
507
+ | `get_terminals()` / `get_supply_drops()` / `get_artifact_crates()` | `list[GameObject]` | Map-element objects |
508
+ | `get_map_resources()` / `get_nests()` | `list[GameObject]` | Veins, charge nodes, beaver dams, nests |
509
+ | `get_items()` | `list[GameObject]` | Item objects |
510
+ | `get_objects_by_class(pattern)` | `list[GameObject]` | Substring class match |
511
+ | `get_object_by_guid(guid)` | `GameObject \| None` | Lookup by GUID (ASA) |
512
+ | `get_actor_location(guid)` | `LocationData \| None` | Actor location by GUID (ASA) |
513
+ | `iter_cryopod_creatures()` | `Iterator[(GameObject, CryopodCreature)]` | Walk filled cryopods |
514
+
515
+ ### Game Objects
516
+
517
+ #### GameObject (`arkparser.GameObject`)
518
+
519
+ | Field | Type | Description |
520
+ |---|---|---|
521
+ | `id` | `int` | Object index within the save |
522
+ | `guid` | `str` | 16-byte GUID (ASA only) |
523
+ | `class_name` | `str` | UE4 class name |
524
+ | `is_item` | `bool` | Whether this is an item / blueprint / engram |
525
+ | `names` | `list[str]` | ArkName list (1 for actors, 2+ for components) |
526
+ | `location` | `LocationData \| None` | World position and rotation |
527
+ | `properties` | `list[Property]` | Parsed property list |
528
+ | `parent` / `components` | `GameObject \| None` / `dict[str, GameObject]` | Relationships |
529
+
530
+ Lookup helpers: `get_property(name, index=None)`, `get_property_value(name, default=None, index=None)`, `get_properties_by_name(name)`, `has_property(name)`, `to_dict()`.
531
+
532
+ #### GameObjectContainer (`arkparser.GameObjectContainer`)
533
+
534
+ Relationship-aware container. Supports `len()`, iteration, indexing. Same filter methods as `WorldSave` (`get_creatures`, `get_structures`, `get_player_pawns`, …) plus `find_by_class_pattern(pattern)`.
535
+
536
+ #### LocationData (`arkparser.LocationData`)
537
+
538
+ `x`, `y`, `z`, `pitch`, `yaw`, `roll` floats. `position` / `rotation` tuple properties, `to_dict()`.
539
+
540
+ ### Cloud-Inventory Data Models
541
+
542
+ | Class | Purpose |
543
+ |---|---|
544
+ | `UploadedCreature` | Cloud creature record (class, level, stats, IDs, upload time) |
545
+ | `UploadedItem` | Cloud item record with `.is_cryopod` / `.cryopod_creature` accessors |
546
+ | `CryopodCreature` | Parsed creature snapshot from a cryopod blob (creature + status properties) |
547
+ | `DinoStats` | Current/max stat values for a creature |
548
+
549
+ All four expose `from_*` constructors and `to_dict()` for serialization.
550
+
551
+ ### Export Functions
552
+
553
+ `arkparser.export` produces dicts matching the legacy `ASVExport.exe` schema. Optional `map_config` adds `lat` / `lon` GPS keys.
554
+
555
+ | Function | Returns | Description |
556
+ |---|---|---|
557
+ | `export_tamed(save, map_config=None)` | `list[dict]` | `ASV_Tamed` records |
558
+ | `export_wild(save, map_config=None)` | `list[dict]` | `ASV_Wild` records |
559
+ | `export_players(save, map_config=None)` | `list[dict]` | `ASV_Players` records (from Profile parsers) |
560
+ | `export_tribes(save)` | `list[dict]` | `ASV_Tribes` records |
561
+ | `export_structures(save, map_config=None)` | `list[dict]` | `ASV_Structures` records |
562
+ | `export_tribe_logs(save)` | `list[dict]` | `ASV_TribeLogs` records |
563
+ | `export_map_structures(save, map_config=None)` | `list[dict]` | `ASV_MapStructures` records |
564
+ | `export_all(save, map_config=None)` | `dict[str, list[dict]]` | All seven exports keyed by ASV filename stem |
565
+ | `export_cluster_uploads(cluster_inventories, map_config=None)` | `list[dict]` | Tamed-shape records for creatures stored in cluster `CloudInventory` files (decoded from `ArkTamedDinosData[].DinoData` blobs) |
566
+ | `export_to_files(save, output_dir, map_config=None, wrap=True)` | `list[Path]` | Writes each export to `<dir>/<ASV_Name>.json`. `wrap=True` (default) emits the legacy `{map, day, time, data}` envelope; `wrap=False` writes the flat list |
567
+
568
+ Schema policy:
569
+
570
+ - **Legacy keys** (`hp-w`, `ccc`, `dinoid`, `mut-f`, `lat`, `lon`, `tribeid`, etc.) are frozen; they mirror `ASVExport.exe` exactly. They are emitted on every record regardless of value.
571
+ - **Empty parser-added fields are omitted.** Added keys whose value is `None`, `""`, `[]`, `false`, or numeric `0` are dropped from the output (ARK property absence already encodes the default). Consumers must read added fields with `.get(key, default)` rather than expecting the key to exist. Lists like `colors` / `body_colors` / `harvest_resource_levels` are also dropped when every element is zero.
572
+ - **Parser additions** sit alongside the legacy keys with descriptive snake_case names (no `extra_*` prefix). See the per-export schema tables above for the full list.
573
+ - **Stat tokens** are uniform across every export: `hp`, `stam`, `torp`, `oxy`, `food`, `water`, `temp`, `weight`, `melee`, `speed`, `fort`, `craft`. Stat blocks use the suffixes `-w` (wild base, tamed only), `-t` (tamed level-ups), `-m` (mutations); wild creatures and players use the unsuffixed form.
574
+
575
+ ### Map Config (`arkparser.common.map_config`)
576
+
577
+ | Function | Returns | Description |
578
+ |---|---|---|
579
+ | `get_map_config(filename)` | `MapConfig` | Lookup by save filename (case-insensitive) |
580
+ | `get_map_config_by_name(name)` | `MapConfig` | Lookup by display name |
581
+ | `list_maps()` | `list[MapConfig]` | All registered map configs |
582
+
583
+ `MapConfig` methods: `ue_to_lat(y)`, `ue_to_lon(x)`, `ue_to_gps(x, y)`, `ccc_string(x, y, z)`.
584
+
585
+ ### Version Detection (`arkparser.common.version_detection`)
586
+
587
+ | Function | Returns | Description |
588
+ |---|---|---|
589
+ | `detect_format(source)` | `ArkFileFormat` | `ASE`, `ASA`, or `UNKNOWN` |
590
+ | `detect_file_type(source)` | `ArkFileType` | `PROFILE`, `TRIBE`, `CLOUD_INVENTORY`, `WORLD_SAVE`, or `UNKNOWN` |
591
+ | `get_save_version(source)` | `int` | Version number (-1 if invalid) |
592
+
593
+ ### Exceptions
594
+
595
+ | Exception | Description |
596
+ |---|---|
597
+ | `ArkParseError` | Base exception for all parsing errors |
598
+ | `CorruptDataError` | File data appears corrupted or invalid |
599
+ | `UnknownPropertyError` | Unrecognized property type encountered |
600
+ | `UnknownStructError` | Unrecognized struct type encountered |
601
+ | `UnexpectedDataError` | Data doesn't match expected values |
602
+ | `EndOfDataError` | Attempted to read past end of data |
603
+
604
+ ## Format Support
605
+
606
+ | Feature | ASE (v5-12) | ASA (v13-14+) |
607
+ |---------|-------------|---------------|
608
+ | Vectors | Float (4 bytes) | Double (8 bytes) |
609
+ | Object IDs | Int32 index | 16-byte GUID |
610
+ | Booleans | Int32 | Int16 |
611
+ | World Save | Binary file | SQLite database |
612
+ | Compression | None | zlib + custom RLE |
613
+
614
+ ASA worldsave property layouts differ between **v13** (`TheIsland_WP` and older single-player saves — legacy `AsaSavegameToolkit`-style `dataSize + position + typeRef + byte` body) and **v14+** (current production ASA — marker-based body). Both are parsed by version-aware property readers; `WorldSave.version` is the source of truth.
615
+
616
+ ## Testing
617
+
618
+ ```powershell
619
+ .\.venv\Scripts\Activate.ps1
620
+ python -m pytest tests/ -v
621
+ ```
622
+
623
+ Tests live in `tests/`. Byte-level layout tests (`test_v13_property_layouts.py`, `test_binary_reader_layouts.py`) pin the canonical v13/v14 property body byte sequences with no file-fixture dependency. Integration tests (`test_world_save.py`, `test_export.py`, etc.) skip cleanly when their referenced save files are not present under `references/examples/`.
624
+
625
+ ## Credits
626
+
627
+ Built by reverse-engineering ARK save formats with heavy reference to [ASV (Ark Save Visualizer)](https://github.com/miragedmuk/ASV) by **miragedmuk**. The C# implementation in ASV is the primary reference for porting binary parsing logic to Python.