arkparser 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {arkparser-0.3.1 → arkparser-0.3.2}/PKG-INFO +4 -1
  2. {arkparser-0.3.1 → arkparser-0.3.2}/README.md +3 -0
  3. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/__init__.py +1 -1
  4. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/export.py +93 -2
  5. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser.egg-info/PKG-INFO +4 -1
  6. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser.egg-info/SOURCES.txt +1 -0
  7. {arkparser-0.3.1 → arkparser-0.3.2}/pyproject.toml +1 -1
  8. arkparser-0.3.2/tests/test_current_stats.py +133 -0
  9. {arkparser-0.3.1 → arkparser-0.3.2}/LICENSE +0 -0
  10. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/__init__.py +0 -0
  11. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/binary_reader.py +0 -0
  12. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/exceptions.py +0 -0
  13. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/map_config.py +0 -0
  14. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/normalization.py +0 -0
  15. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/types.py +0 -0
  16. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/common/version_detection.py +0 -0
  17. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/data_models.py +0 -0
  18. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/__init__.py +0 -0
  19. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/base.py +0 -0
  20. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/cloud_inventory.py +0 -0
  21. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/profile.py +0 -0
  22. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/tribe.py +0 -0
  23. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/files/world_save.py +0 -0
  24. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/game_objects/__init__.py +0 -0
  25. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/game_objects/container.py +0 -0
  26. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/game_objects/game_object.py +0 -0
  27. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/game_objects/location.py +0 -0
  28. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/__init__.py +0 -0
  29. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/base.py +0 -0
  30. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/byte_property.py +0 -0
  31. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/compound.py +0 -0
  32. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/primitives.py +0 -0
  33. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/properties/registry.py +0 -0
  34. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/__init__.py +0 -0
  35. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/base.py +0 -0
  36. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/colors.py +0 -0
  37. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/misc.py +0 -0
  38. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/property_list.py +0 -0
  39. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/registry.py +0 -0
  40. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser/structs/vectors.py +0 -0
  41. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser.egg-info/dependency_links.txt +0 -0
  42. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser.egg-info/requires.txt +0 -0
  43. {arkparser-0.3.1 → arkparser-0.3.2}/arkparser.egg-info/top_level.txt +0 -0
  44. {arkparser-0.3.1 → arkparser-0.3.2}/setup.cfg +0 -0
  45. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_asa_header_position.py +0 -0
  46. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_binary_reader.py +0 -0
  47. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_binary_reader_layouts.py +0 -0
  48. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_cloud_inventory.py +0 -0
  49. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_data_models.py +0 -0
  50. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_export.py +0 -0
  51. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_game_objects.py +0 -0
  52. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_profile.py +0 -0
  53. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_tribe.py +0 -0
  54. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_v13_property_layouts.py +0 -0
  55. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_version_detection.py +0 -0
  56. {arkparser-0.3.1 → arkparser-0.3.2}/tests/test_world_save.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.3.1
3
+ Version: 0.3.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
@@ -250,6 +250,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
250
250
  | `last_baby_age_update` | added | ISO 8601 datetime of `LastUpdatedBabyAgeAtTime`. |
251
251
  | `last_gestation_update` | added | ISO 8601 datetime of `LastUpdatedGestationAtTime`. |
252
252
  | `next_cuddle` | added | ISO 8601 datetime of `BabyNextCuddleTime`. |
253
+ | `current_stats` | added | Live in-world stat values from the dino's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. These are the *current* values (e.g. `hp: 11013.62` = current HP, drops as the dino takes damage). Max values are NOT persisted by ARK — compute downstream from species stat tables + `*-w`/`*-t` points + `imprint` + server multipliers if you need them. `null` when the status component carries no `CurrentStatusValues` entries (e.g. uninitialised baby actor). |
253
254
 
254
255
  #### `ASV_Wild` schema
255
256
 
@@ -267,6 +268,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
267
268
  | `trait` | legacy | first entry of `CreatureTraits` (or empty string) |
268
269
  | `traits` | added | full `CreatureTraits` list |
269
270
  | `wild_spawn_region` | added | `OriginalNPCVolumeName` — `NPCZoneVolume` the creature spawned in. |
271
+ | `current_stats` | added | Live in-world stat values from the creature's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. Max values are NOT in the save (would need species stat tables). `null` when uninitialised. |
270
272
 
271
273
  #### Player data: `.arkprofile` vs in-world pawn
272
274
 
@@ -318,6 +320,7 @@ For the richest output, hand `export_players` **both** — assemble a wrapper fo
318
320
  | `corpse_destruction` | added | ISO 8601 datetime of `CorpseDestructionTime`. |
319
321
  | `chibi_levels` | added | `NumChibiLevelUps` — bonus levels from chibi pets. |
320
322
  | `ascensions_scorched` | added | `NumAscensionsScorched` — ASE ascension counter (legacy `ContentPlayer` parses the ASA ascension block differently; this is the ASE-specific field). |
323
+ | `current_stats` | added | Live in-world stat values for the player from the pawn's `MyCharacterStatusComponent` (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. For profile-sourced records the parser joins on `PlayerDataID == LinkedPlayerDataID` to find the spawned pawn in the world save. `null` when the player has no in-world pawn (never spawned this server / corpse cleared) or the status component has no values — only currently / recently spawned characters have live stats. Max values are NOT persisted by ARK. |
321
324
 
322
325
  #### `ASV_Tribes` schema
323
326
 
@@ -216,6 +216,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
216
216
  | `last_baby_age_update` | added | ISO 8601 datetime of `LastUpdatedBabyAgeAtTime`. |
217
217
  | `last_gestation_update` | added | ISO 8601 datetime of `LastUpdatedGestationAtTime`. |
218
218
  | `next_cuddle` | added | ISO 8601 datetime of `BabyNextCuddleTime`. |
219
+ | `current_stats` | added | Live in-world stat values from the dino's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. These are the *current* values (e.g. `hp: 11013.62` = current HP, drops as the dino takes damage). Max values are NOT persisted by ARK — compute downstream from species stat tables + `*-w`/`*-t` points + `imprint` + server multipliers if you need them. `null` when the status component carries no `CurrentStatusValues` entries (e.g. uninitialised baby actor). |
219
220
 
220
221
  #### `ASV_Wild` schema
221
222
 
@@ -233,6 +234,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
233
234
  | `trait` | legacy | first entry of `CreatureTraits` (or empty string) |
234
235
  | `traits` | added | full `CreatureTraits` list |
235
236
  | `wild_spawn_region` | added | `OriginalNPCVolumeName` — `NPCZoneVolume` the creature spawned in. |
237
+ | `current_stats` | added | Live in-world stat values from the creature's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. Max values are NOT in the save (would need species stat tables). `null` when uninitialised. |
236
238
 
237
239
  #### Player data: `.arkprofile` vs in-world pawn
238
240
 
@@ -284,6 +286,7 @@ For the richest output, hand `export_players` **both** — assemble a wrapper fo
284
286
  | `corpse_destruction` | added | ISO 8601 datetime of `CorpseDestructionTime`. |
285
287
  | `chibi_levels` | added | `NumChibiLevelUps` — bonus levels from chibi pets. |
286
288
  | `ascensions_scorched` | added | `NumAscensionsScorched` — ASE ascension counter (legacy `ContentPlayer` parses the ASA ascension block differently; this is the ASE-specific field). |
289
+ | `current_stats` | added | Live in-world stat values for the player from the pawn's `MyCharacterStatusComponent` (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. For profile-sourced records the parser joins on `PlayerDataID == LinkedPlayerDataID` to find the spawned pawn in the world save. `null` when the player has no in-world pawn (never spawned this server / corpse cleared) or the status component has no values — only currently / recently spawned characters have live stats. Max values are NOT persisted by ARK. |
287
290
 
288
291
  #### `ASV_Tribes` schema
289
292
 
@@ -91,4 +91,4 @@ __all__ = [
91
91
  "ArkParseError",
92
92
  ]
93
93
 
94
- __version__ = "0.3.1"
94
+ __version__ = "0.3.2"
@@ -203,6 +203,57 @@ def _stat_array(status: t.Any, prop_name: str) -> list[int]:
203
203
  return out
204
204
 
205
205
 
206
+ def _current_stat_floats(status: t.Any) -> list[float] | None:
207
+ """Read ``CurrentStatusValues[0..11]`` from a character status component.
208
+
209
+ Status components persist the live in-world values of all 12 stats. The
210
+ array is indexed by ARK's ``EPrimalCharacterStatusValue`` enum:
211
+ 0=hp, 1=stam, 2=torpor, 3=oxy, 4=food, 5=water, 6=temp, 7=weight,
212
+ 8=melee, 9=speed, 10=temp-fortitude, 11=crafting.
213
+
214
+ Returns ``None`` when the status component is missing or carries no
215
+ CurrentStatusValues entries (e.g. uninitialised baby actor) so callers
216
+ can distinguish "no data" from "all zeros".
217
+ """
218
+ if status is None:
219
+ return None
220
+ out: list[float] = [0.0] * 12
221
+ seen = False
222
+ getter = getattr(status, "get_properties_by_name", None)
223
+ if callable(getter):
224
+ # Real GameObject: get_properties_by_name surfaces every indexed
225
+ # entry. Empty result == component carries no CurrentStatusValues
226
+ # at all (uninitialised); return None so callers can distinguish
227
+ # "no data" from "all zeros".
228
+ for prop in getter("CurrentStatusValues"):
229
+ idx = getattr(prop, "index", 0)
230
+ if 0 <= idx < 12:
231
+ out[idx] = _float(getattr(prop, "value", 0.0))
232
+ seen = True
233
+ return out if seen else None
234
+ # Synthetic / cryopod stand-in: per-index get_property_value lookups.
235
+ for i in range(12):
236
+ val = _prop(status, "CurrentStatusValues", default=None, index=i)
237
+ if val is not None:
238
+ out[i] = _float(val)
239
+ seen = True
240
+ return out if seen else None
241
+
242
+
243
+ def _current_stats_dict(status: t.Any) -> dict[str, float] | None:
244
+ """Return current stat values keyed by short stat name.
245
+
246
+ Pre-conditions: ``status`` is the character status component (creature or
247
+ player). May be ``None`` (e.g. offline player with no spawned pawn).
248
+ Post-conditions: returns a dict ``{hp, stam, ..., craft}`` of floats, or
249
+ ``None`` when no CurrentStatusValues are persisted on the component.
250
+ """
251
+ floats = _current_stat_floats(status)
252
+ if floats is None:
253
+ return None
254
+ return {_STAT_NAMES[i]: floats[i] for i in range(12)}
255
+
256
+
206
257
  def _flat_stats(points: list[int], suffix: str = "") -> dict[str, int]:
207
258
  """Emit all 12 stats as a flat dict.
208
259
 
@@ -674,6 +725,7 @@ def _tamed_dict(
674
725
  )) is not None
675
726
  else None
676
727
  ),
728
+ "current_stats": _current_stats_dict(status),
677
729
  "imprinter_player_id": _int(_prop(obj, "ImprinterPlayerDataID")),
678
730
  "imprinter_net_id": _str(_prop(obj, "ImprinterPlayerUniqueNetId")),
679
731
  "taming_team_id": _int(_prop(obj, "TamingTeamID")),
@@ -870,6 +922,7 @@ def _wild_dict(
870
922
  # CreatureTraits list is exposed alongside as ``traits``.
871
923
  "trait": traits[0] if traits else "",
872
924
  "traits": traits,
925
+ "current_stats": _current_stats_dict(status),
873
926
  "wild_spawn_region": _str(_prop(obj, "OriginalNPCVolumeName")),
874
927
  }
875
928
  data.update(_gps_payload(obj, map_config))
@@ -882,13 +935,24 @@ def export_wild(save: t.Any, map_config: MapConfig | None = None) -> list[dict[s
882
935
  return [_wild_dict(obj, _status_for(obj, lookup), map_config) for obj in objects]
883
936
 
884
937
 
885
- def _player_from_profile(profile: Profile, save: t.Any = None) -> dict[str, t.Any]:
938
+ def _player_from_profile(
939
+ profile: Profile,
940
+ save: t.Any = None,
941
+ pawn_status_by_id: dict[int, t.Any] | None = None,
942
+ ) -> dict[str, t.Any]:
886
943
  stat_points = [_int(profile.get_stat(i)["added"]) for i in range(12)]
887
944
  gamertag = profile.player_name or ""
888
945
  character = profile.character_name or gamertag
889
946
  steam_id = profile.unique_id or ""
890
947
  active_dt = _approx_real_datetime(profile.last_login_time, save)
891
948
 
949
+ # Live HP/stam/etc live on the player's in-world pawn's status component,
950
+ # not in the profile. Resolve via PlayerDataID -> pawn -> status. Absent
951
+ # when the player has no spawned body (never spawned / corpse cleared).
952
+ status = None
953
+ if pawn_status_by_id and profile.player_id:
954
+ status = pawn_status_by_id.get(int(profile.player_id))
955
+
892
956
  out: dict[str, t.Any] = {
893
957
  "playerid": profile.player_id or 0,
894
958
  "steam": _str(gamertag),
@@ -907,6 +971,7 @@ def _player_from_profile(profile: Profile, save: t.Any = None) -> dict[str, t.An
907
971
  "netAddress": "",
908
972
  "engram_points": profile.total_engram_points,
909
973
  "experience": _int(profile.experience),
974
+ "current_stats": _current_stats_dict(status),
910
975
  }
911
976
  if steam_id:
912
977
  out["steamid"] = steam_id
@@ -981,17 +1046,43 @@ def _player_from_object(
981
1046
  "body_colors": body_colors,
982
1047
  "died_at": died_iso,
983
1048
  "corpse_destruction": corpse_iso,
1049
+ "current_stats": _current_stats_dict(status),
984
1050
  }
985
1051
  return _compact(data, LEGACY_PLAYER_KEYS)
986
1052
 
987
1053
 
1054
+ def _player_status_by_data_id(save: t.Any, lookup: dict[t.Any, t.Any]) -> dict[int, t.Any]:
1055
+ """Index ``MyCharacterStatusComponent`` per player by ``LinkedPlayerDataID``.
1056
+
1057
+ Walks every ``PlayerPawnTest_*_C`` / ``PlayerCharacter_*`` in the world
1058
+ save, reads its ``LinkedPlayerDataID``, follows ``MyCharacterStatusComponent``
1059
+ via ``lookup``, and stores the resolved status object. Lets profile-based
1060
+ player exports surface live HP/stamina/food/etc. without legacy ASVPack
1061
+ having to do the same join.
1062
+ """
1063
+ out: dict[int, t.Any] = {}
1064
+ objects = getattr(save, "objects", None) or []
1065
+ for obj in objects:
1066
+ cn = str(getattr(obj, "class_name", "") or "")
1067
+ if "PlayerPawn" not in cn and "PlayerCharacter" not in cn:
1068
+ continue
1069
+ pid = _int(_prop(obj, "LinkedPlayerDataID"))
1070
+ if not pid:
1071
+ continue
1072
+ status = _status_for(obj, lookup)
1073
+ if status is not None:
1074
+ out[pid] = status
1075
+ return out
1076
+
1077
+
988
1078
  def export_players(save: t.Any, map_config: MapConfig | None = None) -> list[dict[str, t.Any]]:
989
1079
  profiles = _collection(save, "profiles", Profile)
990
1080
  lookup = _save_lookup(save)
1081
+ pawn_status_by_id = _player_status_by_data_id(save, lookup)
991
1082
  results: list[dict[str, t.Any]] = []
992
1083
  for entry in profiles:
993
1084
  if isinstance(entry, Profile):
994
- results.append(_player_from_profile(entry, save))
1085
+ results.append(_player_from_profile(entry, save, pawn_status_by_id))
995
1086
  continue
996
1087
  profile_obj = getattr(entry, "profile", None)
997
1088
  if profile_obj is None and getattr(entry, "objects", None):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arkparser
3
- Version: 0.3.1
3
+ Version: 0.3.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
@@ -250,6 +250,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
250
250
  | `last_baby_age_update` | added | ISO 8601 datetime of `LastUpdatedBabyAgeAtTime`. |
251
251
  | `last_gestation_update` | added | ISO 8601 datetime of `LastUpdatedGestationAtTime`. |
252
252
  | `next_cuddle` | added | ISO 8601 datetime of `BabyNextCuddleTime`. |
253
+ | `current_stats` | added | Live in-world stat values from the dino's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. These are the *current* values (e.g. `hp: 11013.62` = current HP, drops as the dino takes damage). Max values are NOT persisted by ARK — compute downstream from species stat tables + `*-w`/`*-t` points + `imprint` + server multipliers if you need them. `null` when the status component carries no `CurrentStatusValues` entries (e.g. uninitialised baby actor). |
253
254
 
254
255
  #### `ASV_Wild` schema
255
256
 
@@ -267,6 +268,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
267
268
  | `trait` | legacy | first entry of `CreatureTraits` (or empty string) |
268
269
  | `traits` | added | full `CreatureTraits` list |
269
270
  | `wild_spawn_region` | added | `OriginalNPCVolumeName` — `NPCZoneVolume` the creature spawned in. |
271
+ | `current_stats` | added | Live in-world stat values from the creature's status component (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. Max values are NOT in the save (would need species stat tables). `null` when uninitialised. |
270
272
 
271
273
  #### Player data: `.arkprofile` vs in-world pawn
272
274
 
@@ -318,6 +320,7 @@ For the richest output, hand `export_players` **both** — assemble a wrapper fo
318
320
  | `corpse_destruction` | added | ISO 8601 datetime of `CorpseDestructionTime`. |
319
321
  | `chibi_levels` | added | `NumChibiLevelUps` — bonus levels from chibi pets. |
320
322
  | `ascensions_scorched` | added | `NumAscensionsScorched` — ASE ascension counter (legacy `ContentPlayer` parses the ASA ascension block differently; this is the ASE-specific field). |
323
+ | `current_stats` | added | Live in-world stat values for the player from the pawn's `MyCharacterStatusComponent` (`CurrentStatusValues[0..11]`) as a `{hp, stam, torp, oxy, food, water, temp, weight, melee, speed, fort, craft}` dict of floats. For profile-sourced records the parser joins on `PlayerDataID == LinkedPlayerDataID` to find the spawned pawn in the world save. `null` when the player has no in-world pawn (never spawned this server / corpse cleared) or the status component has no values — only currently / recently spawned characters have live stats. Max values are NOT persisted by ARK. |
321
324
 
322
325
  #### `ASV_Tribes` schema
323
326
 
@@ -43,6 +43,7 @@ tests/test_asa_header_position.py
43
43
  tests/test_binary_reader.py
44
44
  tests/test_binary_reader_layouts.py
45
45
  tests/test_cloud_inventory.py
46
+ tests/test_current_stats.py
46
47
  tests/test_data_models.py
47
48
  tests/test_export.py
48
49
  tests/test_game_objects.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "arkparser"
7
- version = "0.3.1"
7
+ version = "0.3.2"
8
8
  description = "Pure Python parser for ARK: Survival Evolved and ARK: Survival Ascended save files"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,133 @@
1
+ """Unit tests for live CurrentStatusValues extraction.
2
+
3
+ ARK persists every character's instantaneous stat values on the status
4
+ component as ``CurrentStatusValues[0..11]``. ``_current_stats_dict`` reads
5
+ those entries (using either ``get_properties_by_name`` for objects that
6
+ expose it or per-index ``get_property_value`` lookups for synthetic /
7
+ cryopod objects) and returns ``{hp, stam, torp, oxy, food, water, temp,
8
+ weight, melee, speed, fort, craft}``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import typing as t
14
+ from dataclasses import dataclass
15
+
16
+ from arkparser.export import _current_stat_floats, _current_stats_dict
17
+
18
+ _STAT_ORDER = (
19
+ "hp", "stam", "torp", "oxy", "food", "water",
20
+ "temp", "weight", "melee", "speed", "fort", "craft",
21
+ )
22
+
23
+
24
+ @dataclass
25
+ class _IndexedProp:
26
+ """Stand-in for an arkparser property with an array index."""
27
+
28
+ name: str
29
+ index: int
30
+ value: float
31
+
32
+
33
+ class _FakeStatus:
34
+ """Minimal stand-in for a character status component.
35
+
36
+ Supports the ``get_properties_by_name`` interface used by
37
+ ``_current_stat_floats``.
38
+ """
39
+
40
+ def __init__(self, current: list[tuple[int, float]]) -> None:
41
+ self._props = [_IndexedProp("CurrentStatusValues", idx, val) for idx, val in current]
42
+
43
+ def get_properties_by_name(self, name: str) -> list[_IndexedProp]:
44
+ return [p for p in self._props if p.name == name]
45
+
46
+
47
+ class _PerIndexStatus:
48
+ """Synthetic stand-in (matches ``_SyntheticGameObject`` interface).
49
+
50
+ Stores values as ``CurrentStatusValues_{idx}`` keys, and
51
+ ``get_property_value`` returns the indexed value.
52
+ """
53
+
54
+ def __init__(self, current: dict[int, float]) -> None:
55
+ self._values = current
56
+
57
+ def get_property_value(
58
+ self,
59
+ name: str,
60
+ default: t.Any = None,
61
+ index: int | None = None,
62
+ ) -> t.Any:
63
+ if name != "CurrentStatusValues":
64
+ return default
65
+ if index is None:
66
+ return default
67
+ return self._values.get(index, default)
68
+
69
+
70
+ def test_none_status_returns_none() -> None:
71
+ assert _current_stat_floats(None) is None
72
+ assert _current_stats_dict(None) is None
73
+
74
+
75
+ def test_empty_status_returns_none() -> None:
76
+ """Status component carrying no CurrentStatusValues at all -> ``None``."""
77
+ status = _FakeStatus(current=[])
78
+ assert _current_stat_floats(status) is None
79
+ assert _current_stats_dict(status) is None
80
+
81
+
82
+ def test_sparse_indices_fill_with_zeros() -> None:
83
+ """Real saves only persist the stats that diverge from defaults; the
84
+ rest get zero. Wyvern test: hp + stam + oxy set, the rest zero."""
85
+ status = _FakeStatus(current=[
86
+ (0, 25238.14), # hp
87
+ (1, 806.40), # stam
88
+ (3, 660.0), # oxy
89
+ ])
90
+ result = _current_stats_dict(status)
91
+ assert result is not None
92
+ assert result["hp"] == 25238.14
93
+ assert result["stam"] == 806.40
94
+ assert result["oxy"] == 660.0
95
+ assert result["torp"] == 0.0
96
+ assert result["food"] == 0.0
97
+ assert result["water"] == 0.0
98
+ assert result["weight"] == 0.0
99
+ assert result["melee"] == 0.0
100
+ assert result["speed"] == 0.0
101
+ assert result["temp"] == 0.0
102
+ assert result["fort"] == 0.0
103
+ assert result["craft"] == 0.0
104
+
105
+
106
+ def test_all_twelve_indices_round_trip() -> None:
107
+ """Verify every stat slot maps to the correct name (catches off-by-one)."""
108
+ status = _FakeStatus(current=[(i, float(i + 1) * 10) for i in range(12)])
109
+ result = _current_stats_dict(status)
110
+ assert result is not None
111
+ for i, name in enumerate(_STAT_ORDER):
112
+ assert result[name] == float(i + 1) * 10, f"slot {i} ({name}) mismatched"
113
+
114
+
115
+ def test_synthetic_object_uses_per_index_getter() -> None:
116
+ """Cryopod-decoded creatures expose status via ``get_property_value``
117
+ with an explicit ``index`` arg, not ``get_properties_by_name``."""
118
+ status = _PerIndexStatus(current={0: 1000.0, 1: 500.0, 4: 3000.0})
119
+ result = _current_stats_dict(status)
120
+ assert result is not None
121
+ assert result["hp"] == 1000.0
122
+ assert result["stam"] == 500.0
123
+ assert result["food"] == 3000.0
124
+ assert result["torp"] == 0.0
125
+
126
+
127
+ def test_out_of_range_indices_are_ignored() -> None:
128
+ """Defensive: stray index 12+ entries must not blow past the 12-slot array."""
129
+ status = _FakeStatus(current=[(0, 100.0), (12, 999.0), (15, 999.0)])
130
+ result = _current_stats_dict(status)
131
+ assert result is not None
132
+ assert result["hp"] == 100.0
133
+ assert len(result) == 12
File without changes
File without changes
File without changes