arkparser 0.3.1__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arkparser-0.3.1 → arkparser-0.3.3}/PKG-INFO +5 -2
- {arkparser-0.3.1 → arkparser-0.3.3}/README.md +4 -1
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/__init__.py +1 -1
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/data_models.py +45 -10
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/export.py +137 -3
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser.egg-info/PKG-INFO +5 -2
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser.egg-info/SOURCES.txt +2 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/pyproject.toml +1 -1
- arkparser-0.3.3/tests/test_cryopod_export.py +122 -0
- arkparser-0.3.3/tests/test_current_stats.py +133 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/LICENSE +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/__init__.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/binary_reader.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/exceptions.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/map_config.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/normalization.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/types.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/common/version_detection.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/__init__.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/base.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/cloud_inventory.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/profile.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/tribe.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/files/world_save.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/game_objects/__init__.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/game_objects/container.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/game_objects/game_object.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/game_objects/location.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/__init__.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/base.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/byte_property.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/compound.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/primitives.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/properties/registry.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/__init__.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/base.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/colors.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/misc.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/property_list.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/registry.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser/structs/vectors.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser.egg-info/dependency_links.txt +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser.egg-info/requires.txt +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/arkparser.egg-info/top_level.txt +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/setup.cfg +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_asa_header_position.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_binary_reader.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_binary_reader_layouts.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_cloud_inventory.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_data_models.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_export.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_game_objects.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_profile.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_tribe.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_v13_property_layouts.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/tests/test_version_detection.py +0 -0
- {arkparser-0.3.1 → arkparser-0.3.3}/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.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -210,7 +210,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
|
|
|
210
210
|
| `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
|
|
211
211
|
| `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
|
|
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
|
-
| `cryo` | legacy | `
|
|
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
215
|
| `dinoid` | legacy | string form of `id` |
|
|
216
216
|
| `isMating` | legacy | `bEnableTamedMating` |
|
|
@@ -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
|
|
|
@@ -176,7 +176,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
|
|
|
176
176
|
| `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
|
|
177
177
|
| `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
|
|
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
|
-
| `cryo` | legacy | `
|
|
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
181
|
| `dinoid` | legacy | string form of `id` |
|
|
182
182
|
| `isMating` | legacy | `bEnableTamedMating` |
|
|
@@ -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
|
|
|
@@ -547,20 +547,29 @@ class CryopodCreature:
|
|
|
547
547
|
]
|
|
548
548
|
|
|
549
549
|
if len(floats) >= 22:
|
|
550
|
-
#
|
|
551
|
-
#
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
#
|
|
555
|
-
|
|
550
|
+
# ASA cryopod blob: 11 current stats + 11 max stats + extras (36 floats).
|
|
551
|
+
# ASE cryopod blob: 12 current stats + 12 max stats + 1 extra (25 floats).
|
|
552
|
+
# Without the per-format count, the ASA loop walks 12 names against an
|
|
553
|
+
# 11-wide current block, leaking max[0] (which equals current[0] at full
|
|
554
|
+
# HP) into current_stats["CraftingSkill"] and surfacing hp=craft on every
|
|
555
|
+
# cryopod tame in exports.
|
|
556
|
+
if len(floats) >= 36:
|
|
557
|
+
current_count = 11
|
|
558
|
+
max_offset = 11
|
|
559
|
+
else:
|
|
560
|
+
current_count = 12
|
|
561
|
+
max_offset = 12
|
|
562
|
+
|
|
563
|
+
# Current stats: only consume the per-format slot count
|
|
564
|
+
for i in range(min(current_count, len(stat_names))):
|
|
556
565
|
if i < len(floats):
|
|
557
|
-
cryo.current_stats[
|
|
566
|
+
cryo.current_stats[stat_names[i]] = floats[i]
|
|
558
567
|
|
|
559
|
-
# Max stats
|
|
560
|
-
for i
|
|
568
|
+
# Max stats: same count, starting at offset
|
|
569
|
+
for i in range(min(current_count, len(stat_names))):
|
|
561
570
|
max_idx = i + max_offset
|
|
562
571
|
if max_idx < len(floats):
|
|
563
|
-
cryo.max_stats[
|
|
572
|
+
cryo.max_stats[stat_names[i]] = floats[max_idx]
|
|
564
573
|
|
|
565
574
|
# Parse soft class for blueprint reference
|
|
566
575
|
soft_classes = normalize_indexed_list(custom_data.get("CustomDataSoftClasses"))
|
|
@@ -569,6 +578,32 @@ class CryopodCreature:
|
|
|
569
578
|
if isinstance(first_class, dict):
|
|
570
579
|
cryo.class_name = first_class.get("name", cryo.class_name)
|
|
571
580
|
|
|
581
|
+
# Populate raw creature_props / status_props so the export
|
|
582
|
+
# pipeline's _SyntheticGameObject adapter (which calls
|
|
583
|
+
# ``get_property_value``) sees the fields it expects. ARK
|
|
584
|
+
# cryopod CustomItemDatas blobs only carry a small subset of
|
|
585
|
+
# the original creature properties (no DinoID, no TribeID, no
|
|
586
|
+
# tamer string, etc.) — surface what we have, leave the rest
|
|
587
|
+
# absent so consumers can detect the gap.
|
|
588
|
+
_stat_to_idx = {
|
|
589
|
+
"Health": 0, "Stamina": 1, "Torpidity": 2, "Oxygen": 3,
|
|
590
|
+
"Food": 4, "Water": 5, "Temperature": 6, "Weight": 7,
|
|
591
|
+
"MeleeDamage": 8, "MovementSpeed": 9, "Fortitude": 10,
|
|
592
|
+
"CraftingSkill": 11,
|
|
593
|
+
}
|
|
594
|
+
if cryo.name:
|
|
595
|
+
cryo.creature_props["TamedName"] = cryo.name
|
|
596
|
+
for i, color in enumerate(cryo.colors[:6]):
|
|
597
|
+
cryo.creature_props[f"ColorSetIndices_{i}" if i > 0 else "ColorSetIndices"] = color
|
|
598
|
+
if cryo.level:
|
|
599
|
+
cryo.status_props["BaseCharacterLevel"] = cryo.level
|
|
600
|
+
for stat_name, value in cryo.current_stats.items():
|
|
601
|
+
idx = _stat_to_idx.get(stat_name)
|
|
602
|
+
if idx is None:
|
|
603
|
+
continue
|
|
604
|
+
key = "CurrentStatusValues" if idx == 0 else f"CurrentStatusValues_{idx}"
|
|
605
|
+
cryo.status_props[key] = value
|
|
606
|
+
|
|
572
607
|
return cryo
|
|
573
608
|
|
|
574
609
|
except Exception:
|
|
@@ -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")),
|
|
@@ -719,12 +771,55 @@ def _tamed_dict(
|
|
|
719
771
|
|
|
720
772
|
|
|
721
773
|
def export_tamed(save: t.Any, map_config: MapConfig | None = None) -> list[dict[str, t.Any]]:
|
|
774
|
+
"""Emit ASV_Tamed records.
|
|
775
|
+
|
|
776
|
+
Pre-conditions: ``save`` exposes ``get_tamed_creatures()`` (in-world
|
|
777
|
+
tames) and ideally ``iter_cryopod_creatures()`` (creatures whose actor
|
|
778
|
+
has been removed from the world and re-embedded inside a cryopod /
|
|
779
|
+
soultrap / vivarium / dinoball item).
|
|
780
|
+
|
|
781
|
+
Post-conditions: returned list combines (a) every in-world tame and
|
|
782
|
+
(b) every cryopod-embedded tame, with the latter carrying ``cryo=True``
|
|
783
|
+
and inheriting the cryopod item's world location for GPS fields.
|
|
784
|
+
"""
|
|
722
785
|
objects = _world_objects(save, "get_tamed_creatures", "tamed_objects")
|
|
723
786
|
lookup = _save_lookup(save)
|
|
724
|
-
|
|
787
|
+
results: list[dict[str, t.Any]] = [
|
|
725
788
|
_tamed_dict(obj, _status_for(obj, lookup), lookup, map_config, save)
|
|
726
789
|
for obj in objects
|
|
727
790
|
]
|
|
791
|
+
results.extend(_export_world_cryopods(save, map_config))
|
|
792
|
+
return results
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def _export_world_cryopods(
|
|
796
|
+
save: t.Any,
|
|
797
|
+
map_config: MapConfig | None,
|
|
798
|
+
) -> list[dict[str, t.Any]]:
|
|
799
|
+
"""Build ASV_Tamed records for cryopod-embedded creatures on the map.
|
|
800
|
+
|
|
801
|
+
ARK strips the actor for any creature stuffed into a cryopod / soultrap
|
|
802
|
+
/ vivarium / dinoball and serialises a snapshot into the item's
|
|
803
|
+
``CustomItemDatas``. ``get_tamed_creatures()`` therefore misses them
|
|
804
|
+
entirely. We walk ``iter_cryopod_creatures()`` (when available),
|
|
805
|
+
decode each embedded blob, and produce ``ASV_Tamed`` entries with
|
|
806
|
+
``cryo=True`` and the cryopod item's location.
|
|
807
|
+
"""
|
|
808
|
+
iter_cryos = getattr(save, "iter_cryopod_creatures", None)
|
|
809
|
+
if not callable(iter_cryos):
|
|
810
|
+
return []
|
|
811
|
+
out: list[dict[str, t.Any]] = []
|
|
812
|
+
empty_lookup: dict[t.Any, t.Any] = {}
|
|
813
|
+
for item_obj, cryo in iter_cryos():
|
|
814
|
+
actor, status = _cryo_props_to_synthetic(cryo)
|
|
815
|
+
# Inherit the cryopod's world location so GPS fields populate.
|
|
816
|
+
actor.location = getattr(item_obj, "location", None)
|
|
817
|
+
record = _tamed_dict(actor, status, empty_lookup, map_config, save)
|
|
818
|
+
# The synthetic actor carries no IsInCryo property; force the legacy
|
|
819
|
+
# flag so consumers can distinguish in-world tames from stored ones.
|
|
820
|
+
record["cryo"] = True
|
|
821
|
+
out.append(record)
|
|
822
|
+
return out
|
|
728
823
|
|
|
729
824
|
|
|
730
825
|
class _SyntheticGameObject:
|
|
@@ -870,6 +965,7 @@ def _wild_dict(
|
|
|
870
965
|
# CreatureTraits list is exposed alongside as ``traits``.
|
|
871
966
|
"trait": traits[0] if traits else "",
|
|
872
967
|
"traits": traits,
|
|
968
|
+
"current_stats": _current_stats_dict(status),
|
|
873
969
|
"wild_spawn_region": _str(_prop(obj, "OriginalNPCVolumeName")),
|
|
874
970
|
}
|
|
875
971
|
data.update(_gps_payload(obj, map_config))
|
|
@@ -882,13 +978,24 @@ def export_wild(save: t.Any, map_config: MapConfig | None = None) -> list[dict[s
|
|
|
882
978
|
return [_wild_dict(obj, _status_for(obj, lookup), map_config) for obj in objects]
|
|
883
979
|
|
|
884
980
|
|
|
885
|
-
def _player_from_profile(
|
|
981
|
+
def _player_from_profile(
|
|
982
|
+
profile: Profile,
|
|
983
|
+
save: t.Any = None,
|
|
984
|
+
pawn_status_by_id: dict[int, t.Any] | None = None,
|
|
985
|
+
) -> dict[str, t.Any]:
|
|
886
986
|
stat_points = [_int(profile.get_stat(i)["added"]) for i in range(12)]
|
|
887
987
|
gamertag = profile.player_name or ""
|
|
888
988
|
character = profile.character_name or gamertag
|
|
889
989
|
steam_id = profile.unique_id or ""
|
|
890
990
|
active_dt = _approx_real_datetime(profile.last_login_time, save)
|
|
891
991
|
|
|
992
|
+
# Live HP/stam/etc live on the player's in-world pawn's status component,
|
|
993
|
+
# not in the profile. Resolve via PlayerDataID -> pawn -> status. Absent
|
|
994
|
+
# when the player has no spawned body (never spawned / corpse cleared).
|
|
995
|
+
status = None
|
|
996
|
+
if pawn_status_by_id and profile.player_id:
|
|
997
|
+
status = pawn_status_by_id.get(int(profile.player_id))
|
|
998
|
+
|
|
892
999
|
out: dict[str, t.Any] = {
|
|
893
1000
|
"playerid": profile.player_id or 0,
|
|
894
1001
|
"steam": _str(gamertag),
|
|
@@ -907,6 +1014,7 @@ def _player_from_profile(profile: Profile, save: t.Any = None) -> dict[str, t.An
|
|
|
907
1014
|
"netAddress": "",
|
|
908
1015
|
"engram_points": profile.total_engram_points,
|
|
909
1016
|
"experience": _int(profile.experience),
|
|
1017
|
+
"current_stats": _current_stats_dict(status),
|
|
910
1018
|
}
|
|
911
1019
|
if steam_id:
|
|
912
1020
|
out["steamid"] = steam_id
|
|
@@ -981,17 +1089,43 @@ def _player_from_object(
|
|
|
981
1089
|
"body_colors": body_colors,
|
|
982
1090
|
"died_at": died_iso,
|
|
983
1091
|
"corpse_destruction": corpse_iso,
|
|
1092
|
+
"current_stats": _current_stats_dict(status),
|
|
984
1093
|
}
|
|
985
1094
|
return _compact(data, LEGACY_PLAYER_KEYS)
|
|
986
1095
|
|
|
987
1096
|
|
|
1097
|
+
def _player_status_by_data_id(save: t.Any, lookup: dict[t.Any, t.Any]) -> dict[int, t.Any]:
|
|
1098
|
+
"""Index ``MyCharacterStatusComponent`` per player by ``LinkedPlayerDataID``.
|
|
1099
|
+
|
|
1100
|
+
Walks every ``PlayerPawnTest_*_C`` / ``PlayerCharacter_*`` in the world
|
|
1101
|
+
save, reads its ``LinkedPlayerDataID``, follows ``MyCharacterStatusComponent``
|
|
1102
|
+
via ``lookup``, and stores the resolved status object. Lets profile-based
|
|
1103
|
+
player exports surface live HP/stamina/food/etc. without legacy ASVPack
|
|
1104
|
+
having to do the same join.
|
|
1105
|
+
"""
|
|
1106
|
+
out: dict[int, t.Any] = {}
|
|
1107
|
+
objects = getattr(save, "objects", None) or []
|
|
1108
|
+
for obj in objects:
|
|
1109
|
+
cn = str(getattr(obj, "class_name", "") or "")
|
|
1110
|
+
if "PlayerPawn" not in cn and "PlayerCharacter" not in cn:
|
|
1111
|
+
continue
|
|
1112
|
+
pid = _int(_prop(obj, "LinkedPlayerDataID"))
|
|
1113
|
+
if not pid:
|
|
1114
|
+
continue
|
|
1115
|
+
status = _status_for(obj, lookup)
|
|
1116
|
+
if status is not None:
|
|
1117
|
+
out[pid] = status
|
|
1118
|
+
return out
|
|
1119
|
+
|
|
1120
|
+
|
|
988
1121
|
def export_players(save: t.Any, map_config: MapConfig | None = None) -> list[dict[str, t.Any]]:
|
|
989
1122
|
profiles = _collection(save, "profiles", Profile)
|
|
990
1123
|
lookup = _save_lookup(save)
|
|
1124
|
+
pawn_status_by_id = _player_status_by_data_id(save, lookup)
|
|
991
1125
|
results: list[dict[str, t.Any]] = []
|
|
992
1126
|
for entry in profiles:
|
|
993
1127
|
if isinstance(entry, Profile):
|
|
994
|
-
results.append(_player_from_profile(entry, save))
|
|
1128
|
+
results.append(_player_from_profile(entry, save, pawn_status_by_id))
|
|
995
1129
|
continue
|
|
996
1130
|
profile_obj = getattr(entry, "profile", None)
|
|
997
1131
|
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.
|
|
3
|
+
Version: 0.3.3
|
|
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
|
|
@@ -210,7 +210,7 @@ The legacy ASVExport.exe emitted only the visible 8 stats (`hp`, `stam`, `melee`
|
|
|
210
210
|
| `hp-m` .. `fort-m` (all 12) | added | `NumberOfMutationsAppliedTamed[i]` (mutation point counts per stat) |
|
|
211
211
|
| `c0` .. `c5` | legacy | `ColorSetIndices[i]` |
|
|
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
|
-
| `cryo` | legacy | `
|
|
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
215
|
| `dinoid` | legacy | string form of `id` |
|
|
216
216
|
| `isMating` | legacy | `bEnableTamedMating` |
|
|
@@ -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,8 @@ 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_cryopod_export.py
|
|
47
|
+
tests/test_current_stats.py
|
|
46
48
|
tests/test_data_models.py
|
|
47
49
|
tests/test_export.py
|
|
48
50
|
tests/test_game_objects.py
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests pinning ASA cryopod creature decode + export integration.
|
|
2
|
+
|
|
3
|
+
ARK strips the actor for every cryopodded / soultrapped / vivariumed /
|
|
4
|
+
dinoballed creature and embeds a serialised snapshot into the item's
|
|
5
|
+
``CustomItemDatas``. ``WorldSave.iter_cryopod_creatures`` walks these
|
|
6
|
+
items, and ``export_tamed`` then surfaces them as ``ASV_Tamed`` entries
|
|
7
|
+
with ``cryo=True``. Two regressions tested here:
|
|
8
|
+
|
|
9
|
+
- ASA cryopod blobs carry 11 current stats (no CraftingSkill in the
|
|
10
|
+
current block), 11 max stats, and 14 extras (36 floats total). The
|
|
11
|
+
pre-0.3.3 parser walked 12 names against an 11-wide block, leaking
|
|
12
|
+
``max[0]`` (Health) into ``current_stats["CraftingSkill"]`` and
|
|
13
|
+
surfacing ``hp == craft`` on every cryopod tame.
|
|
14
|
+
- ``from_asa_cryopod_data`` did not populate ``creature_props`` /
|
|
15
|
+
``status_props``; the ``_SyntheticGameObject`` adapter the exporter
|
|
16
|
+
uses then saw empty dicts and emitted records with ``lvl=1``,
|
|
17
|
+
``name=""``, all-zero colors, and ``current_stats: null``.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from arkparser.data_models import CryopodCreature
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _asa_blob(current: list[float], max_: list[float]) -> dict[str, object]:
|
|
26
|
+
"""Synthesise the smallest ASA cryopod CustomItemDatas entry we can.
|
|
27
|
+
|
|
28
|
+
36 floats total: 11 current + 11 max + 14 extras (zeros).
|
|
29
|
+
"""
|
|
30
|
+
assert len(current) == 11
|
|
31
|
+
assert len(max_) == 11
|
|
32
|
+
floats = current + max_ + [0.0] * 14
|
|
33
|
+
return {
|
|
34
|
+
"CustomDataName": "Dino",
|
|
35
|
+
"CustomDataStrings": [
|
|
36
|
+
"Argent_Character_BP_C_2094073921", # [0] class_name
|
|
37
|
+
"dada - Lvl 259 (Argentavis)", # [1] display
|
|
38
|
+
"37,0,37,0,0,0,", # [2] colors
|
|
39
|
+
"", "", "", "", "", "", "Argentavis", # [9] species
|
|
40
|
+
],
|
|
41
|
+
"CustomDataFloats": floats,
|
|
42
|
+
"CustomDataNames": [],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_asa_cryopod_eleven_current_stats_does_not_leak_max_into_craft() -> None:
|
|
47
|
+
"""ASA cryopod blob: current[0..10] then max[0..10]. craft must come
|
|
48
|
+
from current[11] — which does not exist, so it must stay 0.0, NOT pick
|
|
49
|
+
up max[0]."""
|
|
50
|
+
current = [6206.46, 2411.2, 0.0, 750.0, 13153.03, 100.0, 0.0, 63.6, 6.69, 0.09, 0.0]
|
|
51
|
+
max_ = [6206.46, 2411.2, 0.0, 750.0, 13153.03, 100.0, 0.0, 63.6, 6.69, 0.09, 0.0]
|
|
52
|
+
cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob(current, max_))
|
|
53
|
+
assert cryo is not None
|
|
54
|
+
assert "Health" in cryo.current_stats
|
|
55
|
+
assert cryo.current_stats["Health"] == 6206.46
|
|
56
|
+
# CraftingSkill must NOT be in current_stats on ASA cryopods (only
|
|
57
|
+
# 11 slots are populated). Pre-0.3.3 set it equal to max[0]=Health.
|
|
58
|
+
assert "CraftingSkill" not in cryo.current_stats
|
|
59
|
+
# Max block round-trips properly
|
|
60
|
+
assert cryo.max_stats["Health"] == 6206.46
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_asa_cryopod_populates_creature_and_status_props() -> None:
|
|
64
|
+
"""The exporter adapter (``_SyntheticGameObject``) reads via
|
|
65
|
+
``get_property_value`` keyed by ARK property name + index suffix. The
|
|
66
|
+
ASA cryopod decoder must mirror those keys into ``creature_props`` /
|
|
67
|
+
``status_props`` so the synthetic adapter surfaces tamed-name, level,
|
|
68
|
+
colors, and CurrentStatusValues per stat index."""
|
|
69
|
+
current = [3000.0, 800.0, 0.0, 500.0, 5000.0, 100.0, 0.0, 200.0, 2.5, 0.05, 0.0]
|
|
70
|
+
max_ = [3000.0] * 11
|
|
71
|
+
cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob(current, max_))
|
|
72
|
+
assert cryo is not None
|
|
73
|
+
|
|
74
|
+
# TamedName from display-name parse
|
|
75
|
+
assert cryo.creature_props.get("TamedName") == "dada"
|
|
76
|
+
# Level from display-name parse
|
|
77
|
+
assert cryo.status_props.get("BaseCharacterLevel") == 259
|
|
78
|
+
# Colors at expected indices (first non-zero is c0=37, then c2=37)
|
|
79
|
+
assert cryo.creature_props.get("ColorSetIndices") == 37
|
|
80
|
+
assert cryo.creature_props.get("ColorSetIndices_2") == 37
|
|
81
|
+
# CurrentStatusValues indexed by EPrimalCharacterStatusValue
|
|
82
|
+
# 0=Health, 1=Stamina, 3=Oxygen, 4=Food, 7=Weight, 8=MeleeDamage
|
|
83
|
+
assert cryo.status_props.get("CurrentStatusValues") == 3000.0 # health
|
|
84
|
+
assert cryo.status_props.get("CurrentStatusValues_1") == 800.0 # stam
|
|
85
|
+
assert cryo.status_props.get("CurrentStatusValues_4") == 5000.0 # food
|
|
86
|
+
assert cryo.status_props.get("CurrentStatusValues_7") == 200.0 # weight
|
|
87
|
+
assert cryo.status_props.get("CurrentStatusValues_8") == 2.5 # melee
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_asa_cryopod_display_name_parses_level_and_species() -> None:
|
|
91
|
+
"""Display-name format ``"Name - Lvl N (Species)"`` must yield all three."""
|
|
92
|
+
cryo = CryopodCreature.from_asa_cryopod_data(_asa_blob([0.0] * 11, [0.0] * 11))
|
|
93
|
+
assert cryo is not None
|
|
94
|
+
assert cryo.name == "dada"
|
|
95
|
+
assert cryo.level == 259
|
|
96
|
+
# Species comes from strings[9] when present, falling back to display parse
|
|
97
|
+
assert cryo.species == "Argentavis"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_ase_cryopod_keeps_twelve_stat_layout() -> None:
|
|
101
|
+
"""ASE cryopod blobs carry 12 current stats + 12 max stats (25 floats).
|
|
102
|
+
The ASA fix must not break the ASE path: walking all 12 names is the
|
|
103
|
+
correct behaviour when ``len(floats) < 36``."""
|
|
104
|
+
# 25 floats = 12 current + 12 max + 1 extra
|
|
105
|
+
floats = [float(i + 1) for i in range(12)] + [float(i + 100) for i in range(12)] + [0.0]
|
|
106
|
+
blob = {
|
|
107
|
+
"CustomDataName": "Dino",
|
|
108
|
+
"CustomDataStrings": [
|
|
109
|
+
"Raptor_Character_BP_C", "Bluey - Lvl 30 (Raptor)", "0,0,0,0,0,0,",
|
|
110
|
+
],
|
|
111
|
+
"CustomDataFloats": floats,
|
|
112
|
+
"CustomDataNames": [],
|
|
113
|
+
}
|
|
114
|
+
cryo = CryopodCreature.from_asa_cryopod_data(blob)
|
|
115
|
+
assert cryo is not None
|
|
116
|
+
# All 12 current and 12 max should be populated on ASE blobs
|
|
117
|
+
assert len(cryo.current_stats) == 12
|
|
118
|
+
assert len(cryo.max_stats) == 12
|
|
119
|
+
assert cryo.current_stats["Health"] == 1.0
|
|
120
|
+
assert cryo.current_stats["CraftingSkill"] == 12.0
|
|
121
|
+
assert cryo.max_stats["Health"] == 100.0
|
|
122
|
+
assert cryo.max_stats["CraftingSkill"] == 111.0
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|