arkparser 0.4.5__tar.gz → 0.5.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 (62) hide show
  1. {arkparser-0.4.5 → arkparser-0.5.0}/PKG-INFO +15 -17
  2. {arkparser-0.4.5 → arkparser-0.5.0}/README.md +14 -16
  3. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/__init__.py +1 -1
  4. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/data_models.py +27 -3
  5. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/export.py +795 -288
  6. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/profile.py +19 -3
  7. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/world_save.py +50 -52
  8. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/compound.py +11 -2
  9. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/registry.py +18 -5
  10. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser.egg-info/PKG-INFO +15 -17
  11. {arkparser-0.4.5 → arkparser-0.5.0}/pyproject.toml +1 -1
  12. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_cloud_inventory.py +0 -1
  13. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_profile.py +0 -1
  14. arkparser-0.5.0/tests/test_review_fixes.py +271 -0
  15. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_tribe.py +0 -1
  16. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_world_save.py +1 -1
  17. arkparser-0.4.5/tests/test_review_fixes.py +0 -138
  18. {arkparser-0.4.5 → arkparser-0.5.0}/LICENSE +0 -0
  19. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/__init__.py +0 -0
  20. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/binary_reader.py +0 -0
  21. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/exceptions.py +0 -0
  22. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/map_config.py +0 -0
  23. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/normalization.py +0 -0
  24. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/types.py +0 -0
  25. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/common/version_detection.py +0 -0
  26. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/__init__.py +0 -0
  27. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/base.py +0 -0
  28. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/cloud_inventory.py +0 -0
  29. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/files/tribe.py +0 -0
  30. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/game_objects/__init__.py +0 -0
  31. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/game_objects/container.py +0 -0
  32. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/game_objects/game_object.py +0 -0
  33. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/game_objects/location.py +0 -0
  34. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/__init__.py +0 -0
  35. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/base.py +0 -0
  36. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/byte_property.py +0 -0
  37. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/properties/primitives.py +0 -0
  38. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/__init__.py +0 -0
  39. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/base.py +0 -0
  40. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/colors.py +0 -0
  41. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/misc.py +0 -0
  42. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/property_list.py +0 -0
  43. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/registry.py +0 -0
  44. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser/structs/vectors.py +0 -0
  45. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser.egg-info/SOURCES.txt +0 -0
  46. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser.egg-info/dependency_links.txt +0 -0
  47. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser.egg-info/requires.txt +0 -0
  48. {arkparser-0.4.5 → arkparser-0.5.0}/arkparser.egg-info/top_level.txt +0 -0
  49. {arkparser-0.4.5 → arkparser-0.5.0}/setup.cfg +0 -0
  50. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_asa_header_position.py +0 -0
  51. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_asa_name_table.py +0 -0
  52. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_ase_cluster_drift.py +0 -0
  53. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_binary_reader.py +0 -0
  54. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_binary_reader_layouts.py +0 -0
  55. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_cloud_export.py +0 -0
  56. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_cryopod_export.py +0 -0
  57. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_current_stats.py +0 -0
  58. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_data_models.py +0 -0
  59. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_export.py +0 -0
  60. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_game_objects.py +0 -0
  61. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_v13_property_layouts.py +0 -0
  62. {arkparser-0.4.5 → arkparser-0.5.0}/tests/test_version_detection.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.4.5
3
+ Version: 0.5.0
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,7 +56,7 @@ A pure-Python library for parsing ARK: Survival Evolved (ASE) and ARK: Survival
56
56
  - **Cloud Inventory / Obelisk**: uploaded creatures, items, cryopod contents
57
57
  - **World Saves** (`.ark`): full map state (creatures, structures, items, players)
58
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
59
+ - **Legacy-parity export**: drop-in JSON output matching `ASVExport.exe` schema, plus parser-only extras under descriptive snake_case keys (no namespace prefix; never overloading a legacy key)
60
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
61
 
62
62
  ## Installation
@@ -212,14 +212,14 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
212
212
  | `mut-f`, `mut-m` | legacy | **Ancestor-line totals.** `RandomMutationsFemale` and `RandomMutationsMale`, single integers counting the total number of mutations that occurred down the maternal and paternal ancestry lines respectively. These are *not* per-stat, they share the `-m` token with the per-stat mutation block below but mean a different thing. Kept under the legacy names for ASVExport parity. |
213
213
  | `cryo` | legacy | `True` for creatures embedded inside cryopod / soultrap / vivarium / dinoball items in the world save, `False` for actor-in-world tames. `export_tamed` walks `WorldSave.iter_cryopod_creatures()` and emits one ASV_Tamed record per embedded creature in addition to the actor-in-world tames; on busy PvE servers cryopodded tames are the majority of the roster (e.g. 10,277 of 11,054 on a live Ragnarok_WP). Cluster-uploaded tames also surface here (via `export_cluster_uploads`) with `cryo=True`. |
214
214
  | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
215
- | `dinoid` | legacy | string form of `id` |
215
+ | `dinoid` | legacy | string form of the dino id. **ASA**: decimal of the combined 64-bit `id`. **ASE**: the two halves concatenated as signed-int32 decimals (`str(DinoID1) + str(DinoID2)`), matching `ASVExport`. |
216
216
  | `isMating` | legacy | `bEnableTamedMating` |
217
217
  | `isNeutered` | legacy | `bNeutered` |
218
218
  | `isClone` | legacy | `bIsClone` or `bIsCloneDino` |
219
219
  | `tamedServer` | legacy | `TamedOnServerName` |
220
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) |
221
+ | `maturation` | legacy | `str(int(BabyAge * 100))` — integer maturation percent (a baby with no `BabyAge` is `0`, a newborn). Note: legacy emits the full float string (e.g. `"7.5131035"`); arkparser truncates to the integer percent. Semantically equal and the downstream consumer coerces to `int` either way. |
222
+ | `traits` | legacy | `CreatureTraits` as a list of `{"trait": <class>}` objects (matches `ASVExport`'s shape, not a flat string list) |
223
223
  | `inventory` | legacy | items from `MyInventoryComponent.InventoryItems`. Each entry carries `itemId`, `qty`, `blueprint`, plus a full snake_case property dump flattened in at the top level (`id`, `rating`, `durability`, `quality`, `damage`, `armor`, `durability_max`, `hypo`, `hyper`, `clip_size`, `weight`, `crafter`, `crafter_tribe`, `skill_bonus`, `loaded_ammo`, `spoils_at`, `spoiled_at`, `c0`..`c5`, `egg_*`, etc). `item_stat_values` is unpacked into the universal 8-slot ARK map (slot 0 `gen_quality`, 1 `armor`, 2 `durability_max`, 3 `damage`, 4 `clip_size`, 5 `hypo`, 6 `weight`, 7 `hyperthermal_insulation`); raw uint16s scaled by the per-blueprint multiplier (which lives in the UE blueprint, not the save). Default / unset values are filtered (no `craft_queue=0`, `skin=-1`, `color_pre_skin=[0]*6`, NaN spoil timers, etc). 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
224
  | `father_id`, `mother_id` | added | combined dino id from the first `DinoAncestors` entry (`null` when missing) |
225
225
  | `father_name`, `mother_name` | added | name strings from the first `DinoAncestors` entry |
@@ -256,7 +256,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
256
256
 
257
257
  | Field | Origin | Source / formula |
258
258
  |---|---|---|
259
- | `id`, `dinoid` | legacy | `(DinoID1 << 32) \| DinoID2` and its string form |
259
+ | `id`, `dinoid` | legacy | `id` = `(DinoID1 << 32) \| DinoID2`. `dinoid` = its string form on **ASA**, or `str(DinoID1) + str(DinoID2)` (signed int32) on **ASE**, matching `ASVExport`. |
260
260
  | `creature` | legacy | `GameObject.class_name` |
261
261
  | `sex` | legacy | `"Female"` if `bIsFemale` else `"Male"` |
262
262
  | `lvl` | legacy | `BaseCharacterLevel` (status) |
@@ -297,11 +297,8 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
297
297
  | `tribe` | legacy | tribe name |
298
298
  | `sex` | legacy | `"Female"` / `"Male"` |
299
299
  | `lvl` | legacy | `BaseCharacterLevel + ExtraCharacterLevel` |
300
- | `lat`, `lon` | legacy | `0.0` (profile/cluster files carry no in-world location) |
301
300
  | `hp` .. `craft`, `fort` | legacy | `NumberOfLevelUpPointsApplied[i]` (10 visible stats) |
302
301
  | `torp`, `temp` | added | same source, indices legacy never emitted |
303
- | `active` | legacy | last-active datetime; currently `null` (placeholder for parity) |
304
- | `ccc` | legacy | `"0 0 0"` (no in-world position from profile/cluster source) |
305
302
  | `achievements` | legacy | reserved array (currently empty for parity) |
306
303
  | `netAddress` | legacy (now populated) | Last client IP ARK persisted (`SavedNetworkAddress` in profile `MyData`). Legacy ASVExport reads the same field (ContentPlayer.cs:157 ASE / :341 ASA). `""` when the profile lacks it (e.g. never-played placeholders). ASA stores a clean IPv4/IPv6 string; some ASE saves store an engine-truncated value (e.g. `"[2001"`) reproduced verbatim, matching legacy. |
307
304
  | `steamid`, `dataFile` | legacy | platform net id and `{steamid}.arkprofile` filename |
@@ -325,12 +322,14 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
325
322
 
326
323
  #### `ASV_Tribes` schema
327
324
 
325
+ `ASV_Tribes` (and `ASV_TribeLogs` / `ASV_Players`, which iterate the same list) is a **superset** of the loaded `.arktribe` files, mirroring legacy `ContentContainer`: it seeds two sentinels (`[ASV Unclaimed]` id `2000000000`, `[ASV Abandoned]` id `-2147483648`), adds every file-backed tribe, then synthesizes a stub tribe for each player profile not already in a tribe (`Tribe of <name>`, id = `PlayerDataID`), each distinct structure `TargetingTeam` (`>= 50000`), and each distinct in-world tame `TargetingTeam` — deduped by id. Cross-server tribes that exist **only** in cluster cloud-inventory files (no map presence) are not synthesized.
326
+
328
327
  | Field | Origin | Source / formula |
329
328
  |---|---|---|
330
- | `tribeid` | legacy | `TribeID` / parser `Tribe.tribe_id` |
331
- | `tribe` | legacy | tribe name |
332
- | `players` | legacy | member count |
333
- | `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. |
329
+ | `tribeid` | legacy | `TribeID` / parser `Tribe.tribe_id`, or a synthesized stub/sentinel id (see above) |
330
+ | `tribe` | legacy | tribe name (file `TribeName`; for stubs, `OwnerName`/`TamerString`; or `Tribe of <character>` for a solo) |
331
+ | `players` | legacy | count of players **allocated** to this tribe (profiles whose explicit team / membership / solo id resolves here, plus member back-fill), matching legacy `Players.Count` — not the raw `.arktribe` member count |
332
+ | `members` | legacy | list of `{ign, lvl, playerid, playername, steamid}` built from the allocated players. `lvl` and `steamid` populate from the matching `.arkprofile`; members with no profile (back-filled from the tribe's member list) carry `lvl=0`, `steamid=""`. |
334
333
  | `tames`, `structures` | legacy | counts derived from `WorldSave` (creatures + structures whose `TargetingTeam` matches) |
335
334
  | `uploadedTames` | legacy | reserved (currently `0`) |
336
335
  | `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. |
@@ -352,9 +351,9 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
352
351
  | Field | Origin | Source / formula |
353
352
  |---|---|---|
354
353
  | `id` | added | `GameObject.id`, internal numeric identifier. Cross-references the values in other records' `linked_structures` / `saddle_structures` / `attached_dino_id` lists. |
355
- | `tribeid` | legacy | `TargetingTeam` |
356
- | `tribe` | legacy | `OwnerName` |
357
- | `struct` | legacy | `GameObject.class_name` |
354
+ | `tribeid` | legacy | `TargetingTeam` for player-owned structures (`>= 50000`). Unowned structures fall to the synthetic `[ASV Abandoned]` tribe id `-2147483648` (legacy `int.MinValue`), mirroring `ContentContainer.cs`. |
355
+ | `tribe` | legacy | resolved owning-tribe name: the loaded `.arktribe` `TribeName`, else the structure's `OwnerName` / `TamerString`; `"[ASV Abandoned]"` for unowned. (Legacy emits the resolved tribe name, not the raw `OwnerName`.) |
356
+ | `struct` | legacy | `GameObject.class_name`. Unowned map elements / crates / debug actors (`Button_*`, `*Vein_*`, `*Nest_*`, `*Beaver*`, `BeeHive_C`, `ArtifactCrate_*`, `TributeTerminal_*`, `SupplyCrate_*`) are excluded — surfaced via `ASV_MapStructures` instead, matching legacy's abandoned-structure filter. |
358
357
  | `name` | legacy | `BoxName` (empty when it matches the class name, mirroring legacy's no-rename strip) |
359
358
  | `locked` | legacy | `bIsPinLocked` or `bIsLocked` |
360
359
  | `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. |
@@ -362,7 +361,6 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
362
361
  | `lat`, `lon`, `ccc` | legacy | location via `MapConfig`, **rounded to 2 decimals** (parser-only nicety, not legacy parity) |
363
362
  | `isSwitchedOn` | legacy | `bContainerActivated`, emitted only when the structure is powered (`bIsPowered` or `bHasFuel`); omitted otherwise. Mirrors legacy `ContentStructure.cs` / `ContentPack.cs` (`IsSwitchedOn.HasValue`). |
364
363
  | `decay_reset` | added | `bHasResetDecayTime` |
365
- | `last_ally_in_range_seconds` | added | raw `LastInAllyRangeTime` / `LastInAllyRangeTimeSerialized` / `LastInAllyRangeSerialized` (in-game seconds, float) |
366
364
  | `last_ally_in_range` | added | ISO 8601 datetime with local TZ. `null` when the save lacks the anchors. |
367
365
  | `painting_id` | added | `UniquePaintingId` |
368
366
  | `feeding_inclusions` | added | `FeedingDinoList` class names when `DinoFeedingListType == 1` (ASA feeding troughs) |
@@ -22,7 +22,7 @@ A pure-Python library for parsing ARK: Survival Evolved (ASE) and ARK: Survival
22
22
  - **Cloud Inventory / Obelisk**: uploaded creatures, items, cryopod contents
23
23
  - **World Saves** (`.ark`): full map state (creatures, structures, items, players)
24
24
  - **Dual format**: automatic ASE (v5-12) / ASA (v13-14+, SQLite) detection
25
- - **Legacy-parity export**: drop-in JSON output matching `ASVExport.exe` schema, plus parser-only extras under `extra_*` keys
25
+ - **Legacy-parity export**: drop-in JSON output matching `ASVExport.exe` schema, plus parser-only extras under descriptive snake_case keys (no namespace prefix; never overloading a legacy key)
26
26
  - **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
27
27
 
28
28
  ## Installation
@@ -178,14 +178,14 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
178
178
  | `mut-f`, `mut-m` | legacy | **Ancestor-line totals.** `RandomMutationsFemale` and `RandomMutationsMale`, single integers counting the total number of mutations that occurred down the maternal and paternal ancestry lines respectively. These are *not* per-stat, they share the `-m` token with the per-stat mutation block below but mean a different thing. Kept under the legacy names for ASVExport parity. |
179
179
  | `cryo` | legacy | `True` for creatures embedded inside cryopod / soultrap / vivarium / dinoball items in the world save, `False` for actor-in-world tames. `export_tamed` walks `WorldSave.iter_cryopod_creatures()` and emits one ASV_Tamed record per embedded creature in addition to the actor-in-world tames; on busy PvE servers cryopodded tames are the majority of the roster (e.g. 10,277 of 11,054 on a live Ragnarok_WP). Cluster-uploaded tames also surface here (via `export_cluster_uploads`) with `cryo=True`. |
180
180
  | `ccc` | legacy | `"{x} {y} {z}"` from `LocationData` |
181
- | `dinoid` | legacy | string form of `id` |
181
+ | `dinoid` | legacy | string form of the dino id. **ASA**: decimal of the combined 64-bit `id`. **ASE**: the two halves concatenated as signed-int32 decimals (`str(DinoID1) + str(DinoID2)`), matching `ASVExport`. |
182
182
  | `isMating` | legacy | `bEnableTamedMating` |
183
183
  | `isNeutered` | legacy | `bNeutered` |
184
184
  | `isClone` | legacy | `bIsClone` or `bIsCloneDino` |
185
185
  | `tamedServer` | legacy | `TamedOnServerName` |
186
186
  | `uploadedServer` | legacy | `UploadedFromServerName` |
187
- | `maturation` | legacy | `str(int(BabyAge * 100))` (string for legacy parity) |
188
- | `traits` | legacy | `CreatureTraits` (full list of mutation trait class names) |
187
+ | `maturation` | legacy | `str(int(BabyAge * 100))` — integer maturation percent (a baby with no `BabyAge` is `0`, a newborn). Note: legacy emits the full float string (e.g. `"7.5131035"`); arkparser truncates to the integer percent. Semantically equal and the downstream consumer coerces to `int` either way. |
188
+ | `traits` | legacy | `CreatureTraits` as a list of `{"trait": <class>}` objects (matches `ASVExport`'s shape, not a flat string list) |
189
189
  | `inventory` | legacy | items from `MyInventoryComponent.InventoryItems`. Each entry carries `itemId`, `qty`, `blueprint`, plus a full snake_case property dump flattened in at the top level (`id`, `rating`, `durability`, `quality`, `damage`, `armor`, `durability_max`, `hypo`, `hyper`, `clip_size`, `weight`, `crafter`, `crafter_tribe`, `skill_bonus`, `loaded_ammo`, `spoils_at`, `spoiled_at`, `c0`..`c5`, `egg_*`, etc). `item_stat_values` is unpacked into the universal 8-slot ARK map (slot 0 `gen_quality`, 1 `armor`, 2 `durability_max`, 3 `damage`, 4 `clip_size`, 5 `hypo`, 6 `weight`, 7 `hyperthermal_insulation`); raw uint16s scaled by the per-blueprint multiplier (which lives in the UE blueprint, not the save). Default / unset values are filtered (no `craft_queue=0`, `skin=-1`, `color_pre_skin=[0]*6`, NaN spoil timers, etc). 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. |
190
190
  | `father_id`, `mother_id` | added | combined dino id from the first `DinoAncestors` entry (`null` when missing) |
191
191
  | `father_name`, `mother_name` | added | name strings from the first `DinoAncestors` entry |
@@ -222,7 +222,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
222
222
 
223
223
  | Field | Origin | Source / formula |
224
224
  |---|---|---|
225
- | `id`, `dinoid` | legacy | `(DinoID1 << 32) \| DinoID2` and its string form |
225
+ | `id`, `dinoid` | legacy | `id` = `(DinoID1 << 32) \| DinoID2`. `dinoid` = its string form on **ASA**, or `str(DinoID1) + str(DinoID2)` (signed int32) on **ASE**, matching `ASVExport`. |
226
226
  | `creature` | legacy | `GameObject.class_name` |
227
227
  | `sex` | legacy | `"Female"` if `bIsFemale` else `"Male"` |
228
228
  | `lvl` | legacy | `BaseCharacterLevel` (status) |
@@ -263,11 +263,8 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
263
263
  | `tribe` | legacy | tribe name |
264
264
  | `sex` | legacy | `"Female"` / `"Male"` |
265
265
  | `lvl` | legacy | `BaseCharacterLevel + ExtraCharacterLevel` |
266
- | `lat`, `lon` | legacy | `0.0` (profile/cluster files carry no in-world location) |
267
266
  | `hp` .. `craft`, `fort` | legacy | `NumberOfLevelUpPointsApplied[i]` (10 visible stats) |
268
267
  | `torp`, `temp` | added | same source, indices legacy never emitted |
269
- | `active` | legacy | last-active datetime; currently `null` (placeholder for parity) |
270
- | `ccc` | legacy | `"0 0 0"` (no in-world position from profile/cluster source) |
271
268
  | `achievements` | legacy | reserved array (currently empty for parity) |
272
269
  | `netAddress` | legacy (now populated) | Last client IP ARK persisted (`SavedNetworkAddress` in profile `MyData`). Legacy ASVExport reads the same field (ContentPlayer.cs:157 ASE / :341 ASA). `""` when the profile lacks it (e.g. never-played placeholders). ASA stores a clean IPv4/IPv6 string; some ASE saves store an engine-truncated value (e.g. `"[2001"`) reproduced verbatim, matching legacy. |
273
270
  | `steamid`, `dataFile` | legacy | platform net id and `{steamid}.arkprofile` filename |
@@ -291,12 +288,14 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
291
288
 
292
289
  #### `ASV_Tribes` schema
293
290
 
291
+ `ASV_Tribes` (and `ASV_TribeLogs` / `ASV_Players`, which iterate the same list) is a **superset** of the loaded `.arktribe` files, mirroring legacy `ContentContainer`: it seeds two sentinels (`[ASV Unclaimed]` id `2000000000`, `[ASV Abandoned]` id `-2147483648`), adds every file-backed tribe, then synthesizes a stub tribe for each player profile not already in a tribe (`Tribe of <name>`, id = `PlayerDataID`), each distinct structure `TargetingTeam` (`>= 50000`), and each distinct in-world tame `TargetingTeam` — deduped by id. Cross-server tribes that exist **only** in cluster cloud-inventory files (no map presence) are not synthesized.
292
+
294
293
  | Field | Origin | Source / formula |
295
294
  |---|---|---|
296
- | `tribeid` | legacy | `TribeID` / parser `Tribe.tribe_id` |
297
- | `tribe` | legacy | tribe name |
298
- | `players` | legacy | member count |
299
- | `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. |
295
+ | `tribeid` | legacy | `TribeID` / parser `Tribe.tribe_id`, or a synthesized stub/sentinel id (see above) |
296
+ | `tribe` | legacy | tribe name (file `TribeName`; for stubs, `OwnerName`/`TamerString`; or `Tribe of <character>` for a solo) |
297
+ | `players` | legacy | count of players **allocated** to this tribe (profiles whose explicit team / membership / solo id resolves here, plus member back-fill), matching legacy `Players.Count` — not the raw `.arktribe` member count |
298
+ | `members` | legacy | list of `{ign, lvl, playerid, playername, steamid}` built from the allocated players. `lvl` and `steamid` populate from the matching `.arkprofile`; members with no profile (back-filled from the tribe's member list) carry `lvl=0`, `steamid=""`. |
300
299
  | `tames`, `structures` | legacy | counts derived from `WorldSave` (creatures + structures whose `TargetingTeam` matches) |
301
300
  | `uploadedTames` | legacy | reserved (currently `0`) |
302
301
  | `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. |
@@ -318,9 +317,9 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
318
317
  | Field | Origin | Source / formula |
319
318
  |---|---|---|
320
319
  | `id` | added | `GameObject.id`, internal numeric identifier. Cross-references the values in other records' `linked_structures` / `saddle_structures` / `attached_dino_id` lists. |
321
- | `tribeid` | legacy | `TargetingTeam` |
322
- | `tribe` | legacy | `OwnerName` |
323
- | `struct` | legacy | `GameObject.class_name` |
320
+ | `tribeid` | legacy | `TargetingTeam` for player-owned structures (`>= 50000`). Unowned structures fall to the synthetic `[ASV Abandoned]` tribe id `-2147483648` (legacy `int.MinValue`), mirroring `ContentContainer.cs`. |
321
+ | `tribe` | legacy | resolved owning-tribe name: the loaded `.arktribe` `TribeName`, else the structure's `OwnerName` / `TamerString`; `"[ASV Abandoned]"` for unowned. (Legacy emits the resolved tribe name, not the raw `OwnerName`.) |
322
+ | `struct` | legacy | `GameObject.class_name`. Unowned map elements / crates / debug actors (`Button_*`, `*Vein_*`, `*Nest_*`, `*Beaver*`, `BeeHive_C`, `ArtifactCrate_*`, `TributeTerminal_*`, `SupplyCrate_*`) are excluded — surfaced via `ASV_MapStructures` instead, matching legacy's abandoned-structure filter. |
324
323
  | `name` | legacy | `BoxName` (empty when it matches the class name, mirroring legacy's no-rename strip) |
325
324
  | `locked` | legacy | `bIsPinLocked` or `bIsLocked` |
326
325
  | `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. |
@@ -328,7 +327,6 @@ For the richest output, hand `export_players` **both**, assemble a wrapper for e
328
327
  | `lat`, `lon`, `ccc` | legacy | location via `MapConfig`, **rounded to 2 decimals** (parser-only nicety, not legacy parity) |
329
328
  | `isSwitchedOn` | legacy | `bContainerActivated`, emitted only when the structure is powered (`bIsPowered` or `bHasFuel`); omitted otherwise. Mirrors legacy `ContentStructure.cs` / `ContentPack.cs` (`IsSwitchedOn.HasValue`). |
330
329
  | `decay_reset` | added | `bHasResetDecayTime` |
331
- | `last_ally_in_range_seconds` | added | raw `LastInAllyRangeTime` / `LastInAllyRangeTimeSerialized` / `LastInAllyRangeSerialized` (in-game seconds, float) |
332
330
  | `last_ally_in_range` | added | ISO 8601 datetime with local TZ. `null` when the save lacks the anchors. |
333
331
  | `painting_id` | added | `UniquePaintingId` |
334
332
  | `feeding_inclusions` | added | `FeedingDinoList` class names when `DinoFeedingListType == 1` (ASA feeding troughs) |
@@ -95,4 +95,4 @@ __all__ = [
95
95
  "ArkParseError",
96
96
  ]
97
97
 
98
- __version__ = "0.4.5"
98
+ __version__ = "0.5.0"
@@ -8,6 +8,7 @@ property data from ARK save files.
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
+ import math
11
12
  import typing as t
12
13
  from dataclasses import dataclass, field
13
14
 
@@ -18,6 +19,21 @@ from .properties.registry import read_properties
18
19
  logger = logging.getLogger(__name__)
19
20
 
20
21
 
22
+ def _finite(value: t.Any, default: float) -> float:
23
+ """Coerce to a finite float; NaN / inf / non-numeric collapse to ``default``.
24
+
25
+ Non-finite floats are invalid JSON tokens that crash strict serializers
26
+ (``json.dumps(allow_nan=False)``, JS ``JSON.parse``, pydantic). Legacy
27
+ ASVExport substitutes ``0.0001`` for a NaN item rating (ContentItem.cs:62);
28
+ other non-finite floats fall back to ``0.0``.
29
+ """
30
+ try:
31
+ result = float(value)
32
+ except (TypeError, ValueError):
33
+ return default
34
+ return result if math.isfinite(result) else default
35
+
36
+
21
37
  @dataclass
22
38
  class DinoStats:
23
39
  """Statistics for a creature."""
@@ -201,7 +217,7 @@ class UploadedCreature:
201
217
  dino_id1=data.get("DinoID1", 0),
202
218
  dino_id2=data.get("DinoID2", 0),
203
219
  level=level,
204
- experience=data.get("DinoExperiencePoints", 0.0),
220
+ experience=_finite(data.get("DinoExperiencePoints", 0.0), 0.0),
205
221
  stats=stats,
206
222
  upload_time=data.get("UploadTime", 0),
207
223
  version=data.get("Version", 0.0),
@@ -348,6 +364,14 @@ class CryopodCreature:
348
364
  props_dict[key] = p.value
349
365
  obj["properties"] = props_dict
350
366
  except Exception:
367
+ # Don't let one drifted object silently zero out: log it so
368
+ # cryopod decode regressions surface instead of returning
369
+ # empty stats with no diagnostic (was a blind swallow).
370
+ logger.debug(
371
+ "Failed to parse cryopod object properties at offset %s",
372
+ obj.get("props_offset"),
373
+ exc_info=True,
374
+ )
351
375
  obj["properties"] = {}
352
376
 
353
377
  # Find creature object (first one) and status component
@@ -735,8 +759,8 @@ class UploadedItem:
735
759
  item_id2=item_id.get("ItemID2", 0) if isinstance(item_id, dict) else 0,
736
760
  quantity=ark_tribute.get("ItemQuantity", 1) or 1,
737
761
  quality_index=ark_tribute.get("ItemQualityIndex", 0),
738
- durability=ark_tribute.get("ItemDurability", 0.0),
739
- rating=ark_tribute.get("ItemRating", 0.0),
762
+ durability=_finite(ark_tribute.get("ItemDurability", 0.0), 0.0),
763
+ rating=_finite(ark_tribute.get("ItemRating", 0.0), 0.0001),
740
764
  slot_index=ark_tribute.get("SlotIndex", 0),
741
765
  is_blueprint=ark_tribute.get("bIsBlueprint", False),
742
766
  is_engram=ark_tribute.get("bIsEngram", False),