arkparser 0.1.0__py3-none-any.whl

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 (46) hide show
  1. arkparser/__init__.py +117 -0
  2. arkparser/common/__init__.py +72 -0
  3. arkparser/common/binary_reader.py +402 -0
  4. arkparser/common/exceptions.py +99 -0
  5. arkparser/common/map_config.py +166 -0
  6. arkparser/common/types.py +249 -0
  7. arkparser/common/version_detection.py +195 -0
  8. arkparser/data_models.py +801 -0
  9. arkparser/export.py +485 -0
  10. arkparser/files/__init__.py +25 -0
  11. arkparser/files/base.py +309 -0
  12. arkparser/files/cloud_inventory.py +259 -0
  13. arkparser/files/profile.py +205 -0
  14. arkparser/files/tribe.py +155 -0
  15. arkparser/files/world_save.py +699 -0
  16. arkparser/game_objects/__init__.py +32 -0
  17. arkparser/game_objects/container.py +180 -0
  18. arkparser/game_objects/game_object.py +273 -0
  19. arkparser/game_objects/location.py +87 -0
  20. arkparser/models/__init__.py +29 -0
  21. arkparser/models/character.py +227 -0
  22. arkparser/models/creature.py +642 -0
  23. arkparser/models/item.py +207 -0
  24. arkparser/models/player.py +263 -0
  25. arkparser/models/stats.py +226 -0
  26. arkparser/models/structure.py +176 -0
  27. arkparser/models/tribe.py +291 -0
  28. arkparser/properties/__init__.py +77 -0
  29. arkparser/properties/base.py +329 -0
  30. arkparser/properties/byte_property.py +230 -0
  31. arkparser/properties/compound.py +1125 -0
  32. arkparser/properties/primitives.py +803 -0
  33. arkparser/properties/registry.py +236 -0
  34. arkparser/py.typed +0 -0
  35. arkparser/structs/__init__.py +60 -0
  36. arkparser/structs/base.py +63 -0
  37. arkparser/structs/colors.py +108 -0
  38. arkparser/structs/misc.py +133 -0
  39. arkparser/structs/property_list.py +101 -0
  40. arkparser/structs/registry.py +140 -0
  41. arkparser/structs/vectors.py +221 -0
  42. arkparser-0.1.0.dist-info/METADATA +833 -0
  43. arkparser-0.1.0.dist-info/RECORD +46 -0
  44. arkparser-0.1.0.dist-info/WHEEL +5 -0
  45. arkparser-0.1.0.dist-info/licenses/LICENSE +21 -0
  46. arkparser-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,801 @@
1
+ """
2
+ Data Models for extracted ARK data.
3
+
4
+ These dataclasses provide clean, typed access to the nested
5
+ property data from ARK save files.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import typing as t
12
+ from dataclasses import dataclass, field
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ @dataclass
18
+ class DinoStats:
19
+ """Statistics for a creature."""
20
+
21
+ health: float = 0.0
22
+ max_health: float = 0.0
23
+ stamina: float = 0.0
24
+ max_stamina: float = 0.0
25
+ torpidity: float = 0.0
26
+ max_torpidity: float = 0.0
27
+ oxygen: float = 0.0
28
+ max_oxygen: float = 0.0
29
+ food: float = 0.0
30
+ max_food: float = 0.0
31
+ water: float = 0.0
32
+ max_water: float = 0.0
33
+ weight: float = 0.0
34
+ max_weight: float = 0.0
35
+ melee_damage: float = 100.0 # Percentage
36
+ movement_speed: float = 100.0 # Percentage
37
+ crafting_skill: float = 100.0 # Percentage
38
+
39
+ @classmethod
40
+ def from_stat_strings(cls, stat_strings: list[str] | None) -> DinoStats:
41
+ """
42
+ Parse stats from the DinoStats string array format.
43
+
44
+ Format examples:
45
+ - "Health: 365.0 / 404.0"
46
+ - "Melee Damage: 369.6 %"
47
+ """
48
+ stats = cls()
49
+ if not stat_strings:
50
+ return stats
51
+
52
+ for stat_str in stat_strings:
53
+ if ": " not in stat_str:
54
+ continue
55
+
56
+ name, value_part = stat_str.split(": ", 1)
57
+ name = name.lower().replace(" ", "_")
58
+
59
+ try:
60
+ if " / " in value_part:
61
+ # Current / Max format
62
+ current, maximum = value_part.split(" / ")
63
+ current = float(current)
64
+ maximum = float(maximum)
65
+
66
+ if name == "health":
67
+ stats.health = current
68
+ stats.max_health = maximum
69
+ elif name == "stamina":
70
+ stats.stamina = current
71
+ stats.max_stamina = maximum
72
+ elif name == "torpidity":
73
+ stats.torpidity = current
74
+ stats.max_torpidity = maximum
75
+ elif name == "oxygen":
76
+ stats.oxygen = current
77
+ stats.max_oxygen = maximum
78
+ elif name == "food":
79
+ stats.food = current
80
+ stats.max_food = maximum
81
+ elif name == "water":
82
+ stats.water = current
83
+ stats.max_water = maximum
84
+ elif name == "weight":
85
+ stats.weight = current
86
+ stats.max_weight = maximum
87
+
88
+ elif value_part.endswith(" %"):
89
+ # Percentage format
90
+ pct = float(value_part.replace(" %", ""))
91
+ if name == "melee_damage":
92
+ stats.melee_damage = pct
93
+ elif name == "movement_speed":
94
+ stats.movement_speed = pct
95
+ elif name == "crafting_skill":
96
+ stats.crafting_skill = pct
97
+
98
+ except (ValueError, IndexError):
99
+ continue
100
+
101
+ return stats
102
+
103
+ def to_dict(self) -> dict[str, float]:
104
+ """Convert to dictionary."""
105
+ return {
106
+ "health": self.health,
107
+ "max_health": self.max_health,
108
+ "stamina": self.stamina,
109
+ "max_stamina": self.max_stamina,
110
+ "torpidity": self.torpidity,
111
+ "max_torpidity": self.max_torpidity,
112
+ "oxygen": self.oxygen,
113
+ "max_oxygen": self.max_oxygen,
114
+ "food": self.food,
115
+ "max_food": self.max_food,
116
+ "water": self.water,
117
+ "max_water": self.max_water,
118
+ "weight": self.weight,
119
+ "max_weight": self.max_weight,
120
+ "melee_damage": self.melee_damage,
121
+ "movement_speed": self.movement_speed,
122
+ "crafting_skill": self.crafting_skill,
123
+ }
124
+
125
+
126
+ @dataclass
127
+ class UploadedCreature:
128
+ """
129
+ An uploaded creature from cloud inventory.
130
+
131
+ Provides clean access to creature properties.
132
+ """
133
+
134
+ # Core identification
135
+ class_name: str = ""
136
+ blueprint: str = ""
137
+ name: str = ""
138
+ species: str = ""
139
+
140
+ # IDs
141
+ dino_id1: int = 0
142
+ dino_id2: int = 0
143
+
144
+ # Stats
145
+ level: int = 1
146
+ experience: float = 0.0
147
+ stats: DinoStats = field(default_factory=DinoStats)
148
+
149
+ # Upload info
150
+ upload_time: int = 0
151
+ version: float = 0.0
152
+
153
+ # Raw data for advanced access
154
+ raw_data: dict[str, t.Any] = field(default_factory=dict, repr=False)
155
+
156
+ @classmethod
157
+ def from_ark_data(cls, data: dict[str, t.Any]) -> UploadedCreature:
158
+ """
159
+ Create from ArkTamedDinosData struct.
160
+
161
+ Args:
162
+ data: The raw dino data dictionary from parsing.
163
+ """
164
+ # Parse species from name like "Rex - Lvl 226 (Dodo)"
165
+ dino_name = data.get("DinoName", "")
166
+ species = ""
167
+ tame_name = ""
168
+ level = 1
169
+
170
+ if dino_name:
171
+ # Format: "TameName - Lvl N (Species)"
172
+ if " - Lvl " in dino_name and "(" in dino_name:
173
+ parts = dino_name.split(" - Lvl ")
174
+ tame_name = parts[0] if parts else ""
175
+ if len(parts) > 1:
176
+ lvl_species = parts[1]
177
+ if " (" in lvl_species:
178
+ lvl_str, species_part = lvl_species.split(" (", 1)
179
+ try:
180
+ level = int(lvl_str)
181
+ except ValueError:
182
+ pass
183
+ species = species_part.rstrip(")")
184
+ else:
185
+ tame_name = dino_name
186
+
187
+ # Parse stats
188
+ stats = DinoStats.from_stat_strings(data.get("DinoStats"))
189
+
190
+ return cls(
191
+ class_name=data.get("DinoClass", ""),
192
+ blueprint=data.get("DinoClassName", ""),
193
+ name=tame_name,
194
+ species=species,
195
+ dino_id1=data.get("DinoID1", 0),
196
+ dino_id2=data.get("DinoID2", 0),
197
+ level=level,
198
+ experience=data.get("DinoExperiencePoints", 0.0),
199
+ stats=stats,
200
+ upload_time=data.get("UploadTime", 0),
201
+ version=data.get("Version", 0.0),
202
+ raw_data=data,
203
+ )
204
+
205
+ @property
206
+ def unique_id(self) -> str:
207
+ """Get unique ID as combined string."""
208
+ return f"{self.dino_id1}_{self.dino_id2}"
209
+
210
+ def to_dict(self) -> dict[str, t.Any]:
211
+ """Convert to dictionary."""
212
+ return {
213
+ "class_name": self.class_name,
214
+ "blueprint": self.blueprint,
215
+ "name": self.name,
216
+ "species": self.species,
217
+ "dino_id1": self.dino_id1,
218
+ "dino_id2": self.dino_id2,
219
+ "unique_id": self.unique_id,
220
+ "level": self.level,
221
+ "experience": self.experience,
222
+ "stats": self.stats.to_dict(),
223
+ "upload_time": self.upload_time,
224
+ "version": self.version,
225
+ }
226
+
227
+
228
+ @dataclass
229
+ class CryopodCreature:
230
+ """
231
+ A creature stored inside a cryopod.
232
+
233
+ Contains parsed creature data from the cryopod's CustomItemDatas byte array.
234
+ """
235
+
236
+ # Core identification
237
+ class_name: str = ""
238
+ name: str = ""
239
+ species: str = ""
240
+
241
+ # IDs
242
+ dino_id1: int = 0
243
+ dino_id2: int = 0
244
+
245
+ # Stats
246
+ level: int = 1
247
+ experience: float = 0.0
248
+
249
+ # Owner info
250
+ tamer_name: str = ""
251
+ owner_name: str = ""
252
+ taming_team_id: int = 0
253
+ owning_player_id: int = 0
254
+
255
+ # Server info
256
+ tamed_on_server: str = ""
257
+ uploaded_from_server: str = ""
258
+
259
+ # Colors (6 color regions)
260
+ colors: list[int] = field(default_factory=list)
261
+ color_names: list[str] = field(default_factory=list)
262
+
263
+ # Stats - current and max values
264
+ current_stats: dict[str, float] = field(default_factory=dict)
265
+ max_stats: dict[str, float] = field(default_factory=dict)
266
+ base_stats: dict[str, float] = field(default_factory=dict)
267
+ level_ups_wild: dict[str, int] = field(default_factory=dict)
268
+ level_ups_tamed: dict[str, int] = field(default_factory=dict)
269
+
270
+ # Raw parsed properties for advanced access
271
+ creature_props: dict[str, t.Any] = field(default_factory=dict, repr=False)
272
+ status_props: dict[str, t.Any] = field(default_factory=dict, repr=False)
273
+
274
+ @classmethod
275
+ def from_cryopod_bytes(cls, byte_data: list[int]) -> CryopodCreature | None:
276
+ """
277
+ Parse creature data from cryopod CustomDataBytes.
278
+
279
+ Args:
280
+ byte_data: The raw bytes from CustomDataBytes.ByteArrays[0].Bytes
281
+
282
+ Returns:
283
+ CryopodCreature with parsed data, or None if parsing fails.
284
+ """
285
+ from .common.binary_reader import BinaryReader
286
+ from .properties.registry import read_properties
287
+
288
+ try:
289
+ reader = BinaryReader.from_bytes(bytes(byte_data))
290
+
291
+ # First int32 is object count
292
+ obj_count = reader.read_int32()
293
+ if obj_count < 1:
294
+ return None
295
+
296
+ # Read object headers
297
+ # Format: GUID(16) + ClassName(string) + IsItem(int32) + NamesCount(int32)
298
+ # + Names(strings, no instance index) + FromDataFile(int32)
299
+ # + DataFileIndex(int32) + HasLocation(int32) + [LocationData(24)]
300
+ # + PropsOffset(int32) + Unknown(int32)
301
+ objects: list[dict[str, t.Any]] = []
302
+ for _ in range(obj_count):
303
+ obj: dict[str, t.Any] = {}
304
+
305
+ # Read GUID (16 bytes of zeros for ASE)
306
+ obj["guid"] = reader.read_bytes(16)
307
+
308
+ # Read class name
309
+ obj["class_name"] = reader.read_string()
310
+
311
+ # Read flag (whether this is an item)
312
+ obj["is_item"] = reader.read_int32() != 0
313
+
314
+ # Read names count and names (NO instance indices in cryopod format)
315
+ names_count = reader.read_int32()
316
+ obj["names"] = [reader.read_string() for _ in range(names_count)]
317
+
318
+ # Read more header fields
319
+ obj["from_data_file"] = reader.read_int32() != 0
320
+ obj["data_file_index"] = reader.read_int32()
321
+
322
+ # Read has_location flag
323
+ has_location = reader.read_int32() != 0
324
+ if has_location:
325
+ # Skip location data (6 floats: x, y, z, pitch, yaw, roll)
326
+ reader.skip(24)
327
+
328
+ # Read properties offset (where this object's properties start)
329
+ obj["props_offset"] = reader.read_int32()
330
+
331
+ # Read unknown int (always 0)
332
+ reader.read_int32()
333
+
334
+ objects.append(obj)
335
+
336
+ # Now read properties for each object by seeking to props_offset
337
+ for obj in objects:
338
+ try:
339
+ reader.position = obj["props_offset"]
340
+ props = read_properties(reader, is_asa=False)
341
+ # Convert to dict, handling duplicate property names (indexed props)
342
+ props_dict: dict[str, t.Any] = {}
343
+ for p in props:
344
+ key = f"{p.name}_{p.index}" if p.index > 0 else p.name
345
+ props_dict[key] = p.value
346
+ obj["properties"] = props_dict
347
+ except Exception:
348
+ obj["properties"] = {}
349
+
350
+ # Find creature object (first one) and status component
351
+ creature_obj = objects[0] if objects else None
352
+ status_obj = None
353
+ for obj in objects:
354
+ class_name = obj.get("class_name", "")
355
+ if "DinoCharacterStatus" in class_name:
356
+ status_obj = obj
357
+ break
358
+
359
+ if not creature_obj:
360
+ return None
361
+
362
+ creature_props = creature_obj.get("properties", {})
363
+ status_props = status_obj.get("properties", {}) if status_obj else {}
364
+
365
+ # Extract creature data
366
+ cryo = cls()
367
+ cryo.class_name = creature_obj.get("class_name", "")
368
+
369
+ # Extract species from class name
370
+ # e.g., "Raptor_Character_BP_C" -> "Raptor"
371
+ if cryo.class_name:
372
+ species = cryo.class_name.replace("_Character_BP_C", "").replace("_C", "")
373
+ cryo.species = species.replace("_", " ")
374
+
375
+ # Basic creature properties
376
+ cryo.name = creature_props.get("TamedName", "")
377
+ cryo.tamer_name = creature_props.get("TamerString", "")
378
+ cryo.owner_name = creature_props.get("OwningPlayerName", "")
379
+ cryo.taming_team_id = creature_props.get("TamingTeamID", 0)
380
+ cryo.owning_player_id = creature_props.get("OwningPlayerID", 0)
381
+ cryo.dino_id1 = creature_props.get("DinoID1", 0)
382
+ cryo.dino_id2 = creature_props.get("DinoID2", 0)
383
+ cryo.tamed_on_server = creature_props.get("TamedOnServerName", "")
384
+ cryo.uploaded_from_server = creature_props.get("UploadedFromServerName", "")
385
+
386
+ # Color data (indexed properties)
387
+ cryo.colors = []
388
+ cryo.color_names = []
389
+ for i in range(6):
390
+ # Check both indexed and non-indexed keys
391
+ color_key = f"ColorSetIndices_{i}" if i > 0 else "ColorSetIndices"
392
+ color = creature_props.get(color_key, 0)
393
+ if isinstance(color, (int, float)):
394
+ cryo.colors.append(int(color))
395
+
396
+ name_key = f"ColorSetNames_{i}" if i > 0 else "ColorSetNames"
397
+ color_name = creature_props.get(name_key, "")
398
+ if color_name:
399
+ cryo.color_names.append(str(color_name))
400
+
401
+ # Status component stats
402
+ stat_names = [
403
+ "Health",
404
+ "Stamina",
405
+ "Torpidity",
406
+ "Oxygen",
407
+ "Food",
408
+ "Water",
409
+ "Temperature",
410
+ "Weight",
411
+ "MeleeDamage",
412
+ "MovementSpeed",
413
+ "Fortitude",
414
+ "CraftingSkill",
415
+ ]
416
+
417
+ for i, stat_name in enumerate(stat_names):
418
+ # Current values (indexed properties)
419
+ current_key = f"CurrentStatusValues_{i}" if i > 0 else "CurrentStatusValues"
420
+ current = status_props.get(current_key, None)
421
+ if current is not None:
422
+ cryo.current_stats[stat_name] = float(current)
423
+
424
+ # Max values
425
+ max_key = f"MaxStatusValues_{i}" if i > 0 else "MaxStatusValues"
426
+ max_val = status_props.get(max_key, None)
427
+ if max_val is not None:
428
+ cryo.max_stats[stat_name] = float(max_val)
429
+
430
+ # Base level max values
431
+ base_key = f"BaseLevelMaxStatusValues_{i}" if i > 0 else "BaseLevelMaxStatusValues"
432
+ base_val = status_props.get(base_key, None)
433
+ if base_val is not None:
434
+ cryo.base_stats[stat_name] = float(base_val)
435
+
436
+ # Level ups (wild)
437
+ wild_key = f"NumberOfLevelUpPointsApplied_{i}" if i > 0 else "NumberOfLevelUpPointsApplied"
438
+ wild_ups = status_props.get(wild_key, None)
439
+ if wild_ups is not None:
440
+ cryo.level_ups_wild[stat_name] = int(wild_ups)
441
+
442
+ # Level ups (tamed)
443
+ tamed_key = f"NumberOfLevelUpPointsAppliedTamed_{i}" if i > 0 else "NumberOfLevelUpPointsAppliedTamed"
444
+ tamed_ups = status_props.get(tamed_key, None)
445
+ if tamed_ups is not None:
446
+ cryo.level_ups_tamed[stat_name] = int(tamed_ups)
447
+
448
+ # Calculate level from status component
449
+ base_level = status_props.get("BaseCharacterLevel", 1)
450
+ extra_level = status_props.get("ExtraCharacterLevel", 0)
451
+ cryo.level = int(base_level) + int(extra_level)
452
+ cryo.experience = float(status_props.get("ExperiencePoints", 0.0))
453
+
454
+ # Store raw props for advanced access
455
+ cryo.creature_props = creature_props
456
+ cryo.status_props = status_props
457
+
458
+ return cryo
459
+
460
+ except Exception:
461
+ logger.debug("Failed to parse cryopod creature from bytes", exc_info=True)
462
+ return None
463
+
464
+ @classmethod
465
+ def from_asa_cryopod_data(cls, custom_data: dict[str, t.Any]) -> CryopodCreature | None:
466
+ """
467
+ Parse creature data from ASA/ASE cryopod CustomItemDatas entry.
468
+
469
+ Both ASA and ASE store cryopod creature data using:
470
+ - CustomDataStrings: [class_name, display_name, colors_str, ?, gender, ?, ?, ...]
471
+ - ASE has 7 strings, ASA has 10+ strings (with species at index 9)
472
+ - CustomDataFloats: [current_stats x 12, max_stats x 12, ...]
473
+ - CustomDataNames: Color names for the 6 color regions
474
+
475
+ Args:
476
+ custom_data: The CustomItemDatas entry with CustomDataName == "Dino"
477
+
478
+ Returns:
479
+ CryopodCreature with parsed data, or None if parsing fails.
480
+ """
481
+ try:
482
+ cryo = cls()
483
+
484
+ # Parse strings - need at least 3 for basic info
485
+ strings = custom_data.get("CustomDataStrings", [])
486
+ if len(strings) >= 3:
487
+ cryo.class_name = strings[0] # e.g., "Raptor_Character_BP_C_2145673735"
488
+ display_name = strings[1] # e.g., "bluey - Lvl 228 (Raptor)"
489
+ colors_str = strings[2] # e.g., "2,2,2,2,2,2,"
490
+
491
+ # Parse display name for tame name, level, and species
492
+ # Format: "Bluey - Lvl 226 (Raptor)"
493
+ if " - Lvl " in display_name:
494
+ parts = display_name.split(" - Lvl ")
495
+ cryo.name = parts[0]
496
+ if len(parts) > 1:
497
+ lvl_species = parts[1]
498
+ if " (" in lvl_species:
499
+ lvl_part, species_part = lvl_species.split(" (", 1)
500
+ try:
501
+ cryo.level = int(lvl_part)
502
+ except ValueError:
503
+ pass
504
+ # Extract species from "(Raptor)"
505
+ cryo.species = species_part.rstrip(")")
506
+
507
+ # If we have index 9 with species name (ASA format), use it
508
+ if len(strings) > 9 and strings[9]:
509
+ cryo.species = strings[9]
510
+ elif not cryo.species and cryo.class_name:
511
+ # Fall back to class name parsing
512
+ species = cryo.class_name.split("_Character_BP")[0]
513
+ cryo.species = species.replace("_", " ")
514
+
515
+ # Parse colors from string "2,2,2,2,2,2,"
516
+ if colors_str:
517
+ color_parts = colors_str.strip(",").split(",")
518
+ cryo.colors = [int(c) for c in color_parts if c.strip().isdigit()]
519
+
520
+ # Parse color names
521
+ color_names = custom_data.get("CustomDataNames", [])
522
+ if color_names:
523
+ cryo.color_names = list(color_names)
524
+
525
+ # Parse stats from floats
526
+ # Format varies by version:
527
+ # - ASE: 25 floats - current[0-11], max[12-23], extra[24]
528
+ # - ASA: 36 floats - current[0-10], max[11-21], extra[22-35]
529
+ floats = custom_data.get("CustomDataFloats", [])
530
+ stat_names = [
531
+ "Health",
532
+ "Stamina",
533
+ "Torpidity",
534
+ "Oxygen",
535
+ "Food",
536
+ "Water",
537
+ "Temperature",
538
+ "Weight",
539
+ "MeleeDamage",
540
+ "MovementSpeed",
541
+ "Fortitude",
542
+ "CraftingSkill",
543
+ ]
544
+
545
+ if len(floats) >= 22:
546
+ # Determine offset based on array length
547
+ # ASA has 36 floats with offset 11, ASE has 25 floats with offset 12
548
+ max_offset = 11 if len(floats) >= 36 else 12
549
+
550
+ # Current stats start at 0
551
+ for i, stat_name in enumerate(stat_names):
552
+ if i < len(floats):
553
+ cryo.current_stats[stat_name] = floats[i]
554
+
555
+ # Max stats start at offset
556
+ for i, stat_name in enumerate(stat_names):
557
+ max_idx = i + max_offset
558
+ if max_idx < len(floats):
559
+ cryo.max_stats[stat_name] = floats[max_idx]
560
+
561
+ # Parse soft class for blueprint reference
562
+ soft_classes = custom_data.get("CustomDataSoftClasses", [])
563
+ if soft_classes:
564
+ first_class = soft_classes[0]
565
+ if isinstance(first_class, dict):
566
+ cryo.class_name = first_class.get("name", cryo.class_name)
567
+
568
+ return cryo
569
+
570
+ except Exception:
571
+ logger.debug("Failed to parse ASA cryopod creature data", exc_info=True)
572
+ return None
573
+
574
+ @property
575
+ def unique_id(self) -> str:
576
+ """Get unique ID as combined string."""
577
+ return f"{self.dino_id1}_{self.dino_id2}"
578
+
579
+ @property
580
+ def stats(self) -> DinoStats:
581
+ """Get stats in DinoStats format for compatibility."""
582
+ return DinoStats(
583
+ health=self.current_stats.get("Health", 0.0),
584
+ max_health=self.max_stats.get("Health", 0.0),
585
+ stamina=self.current_stats.get("Stamina", 0.0),
586
+ max_stamina=self.max_stats.get("Stamina", 0.0),
587
+ torpidity=self.max_stats.get("Torpidity", 0.0),
588
+ max_torpidity=self.max_stats.get("Torpidity", 0.0),
589
+ oxygen=self.current_stats.get("Oxygen", 0.0),
590
+ max_oxygen=self.max_stats.get("Oxygen", 0.0),
591
+ food=self.current_stats.get("Food", 0.0),
592
+ max_food=self.max_stats.get("Food", 0.0),
593
+ water=self.current_stats.get("Water", 0.0),
594
+ max_water=self.max_stats.get("Water", 0.0),
595
+ weight=self.current_stats.get("Weight", 0.0),
596
+ max_weight=self.max_stats.get("Weight", 0.0),
597
+ melee_damage=self.current_stats.get("MeleeDamage", 1.0) * 100 + 100,
598
+ movement_speed=self.current_stats.get("MovementSpeed", 1.0) * 100 + 100,
599
+ crafting_skill=self.current_stats.get("CraftingSkill", 1.0) * 100,
600
+ )
601
+
602
+ def to_dict(self) -> dict[str, t.Any]:
603
+ """Convert to dictionary."""
604
+ return {
605
+ "class_name": self.class_name,
606
+ "name": self.name,
607
+ "species": self.species,
608
+ "level": self.level,
609
+ "experience": self.experience,
610
+ "unique_id": self.unique_id,
611
+ "dino_id1": self.dino_id1,
612
+ "dino_id2": self.dino_id2,
613
+ "tamer_name": self.tamer_name,
614
+ "owner_name": self.owner_name,
615
+ "taming_team_id": self.taming_team_id,
616
+ "owning_player_id": self.owning_player_id,
617
+ "tamed_on_server": self.tamed_on_server,
618
+ "uploaded_from_server": self.uploaded_from_server,
619
+ "colors": self.colors,
620
+ "color_names": self.color_names,
621
+ "current_stats": self.current_stats,
622
+ "max_stats": self.max_stats,
623
+ "base_stats": self.base_stats,
624
+ "level_ups_wild": self.level_ups_wild,
625
+ "level_ups_tamed": self.level_ups_tamed,
626
+ }
627
+
628
+
629
+ @dataclass
630
+ class UploadedItem:
631
+ """
632
+ An uploaded item from cloud inventory.
633
+
634
+ Provides clean access to item properties.
635
+ """
636
+
637
+ # Core identification
638
+ blueprint: str = ""
639
+ name: str = ""
640
+ custom_name: str = ""
641
+
642
+ # IDs
643
+ item_id1: int = 0
644
+ item_id2: int = 0
645
+
646
+ # Item properties
647
+ quantity: int = 1
648
+ quality_index: int = 0
649
+ durability: float = 0.0
650
+ rating: float = 0.0
651
+ slot_index: int = 0
652
+
653
+ # Flags
654
+ is_blueprint: bool = False
655
+ is_engram: bool = False
656
+
657
+ # Upload info
658
+ upload_time: float = 0.0
659
+
660
+ # Raw data for advanced access
661
+ raw_data: dict[str, t.Any] = field(default_factory=dict, repr=False)
662
+
663
+ # Cached cryopod creature
664
+ _cryopod_creature: CryopodCreature | None = field(default=None, repr=False, init=False)
665
+
666
+ @classmethod
667
+ def from_ark_data(cls, data: dict[str, t.Any]) -> UploadedItem:
668
+ """
669
+ Create from ArkItems struct.
670
+
671
+ Args:
672
+ data: The raw item data dictionary from parsing.
673
+ """
674
+ ark_tribute = data.get("ArkTributeItem", {})
675
+ item_id = ark_tribute.get("ItemId", {})
676
+
677
+ # Extract item name from blueprint path
678
+ blueprint = ark_tribute.get("ItemArchetype", "")
679
+ name = ""
680
+ if blueprint:
681
+ # Extract class name from path like "BlueprintGeneratedClass /Game/.../WeaponTek.WeaponTek_C"
682
+ if "." in blueprint:
683
+ name = blueprint.rsplit(".", 1)[-1].replace("_C", "")
684
+
685
+ return cls(
686
+ blueprint=blueprint,
687
+ name=name,
688
+ custom_name=ark_tribute.get("CustomItemName", ""),
689
+ item_id1=item_id.get("ItemID1", 0) if isinstance(item_id, dict) else 0,
690
+ item_id2=item_id.get("ItemID2", 0) if isinstance(item_id, dict) else 0,
691
+ quantity=ark_tribute.get("ItemQuantity", 1) or 1,
692
+ quality_index=ark_tribute.get("ItemQualityIndex", 0),
693
+ durability=ark_tribute.get("ItemDurability", 0.0),
694
+ rating=ark_tribute.get("ItemRating", 0.0),
695
+ slot_index=ark_tribute.get("SlotIndex", 0),
696
+ is_blueprint=ark_tribute.get("bIsBlueprint", False),
697
+ is_engram=ark_tribute.get("bIsEngram", False),
698
+ upload_time=data.get("UploadTime", 0.0),
699
+ raw_data=data,
700
+ )
701
+
702
+ @property
703
+ def unique_id(self) -> str:
704
+ """Get unique ID as combined string."""
705
+ return f"{self.item_id1}_{self.item_id2}"
706
+
707
+ @property
708
+ def quality_name(self) -> str:
709
+ """Get quality tier name."""
710
+ qualities = [
711
+ "Primitive",
712
+ "Ramshackle",
713
+ "Apprentice",
714
+ "Journeyman",
715
+ "Mastercraft",
716
+ "Ascendant",
717
+ ]
718
+ if 0 <= self.quality_index < len(qualities):
719
+ return qualities[self.quality_index]
720
+ return "Unknown"
721
+
722
+ @property
723
+ def display_name(self) -> str:
724
+ """Get display name (custom name or extracted name)."""
725
+ return self.custom_name or self.name
726
+
727
+ @property
728
+ def is_cryopod(self) -> bool:
729
+ """Check if this item is a cryopod (or similar creature storage item)."""
730
+ bp_lower = self.blueprint.lower()
731
+ return any(x in bp_lower for x in ["cryopod", "soultrap", "vivarium", "dinoball"])
732
+
733
+ @property
734
+ def cryopod_creature(self) -> CryopodCreature | None:
735
+ """
736
+ Get the creature stored in this cryopod.
737
+
738
+ Returns:
739
+ CryopodCreature with parsed creature data, or None if not a cryopod
740
+ or if parsing fails.
741
+ """
742
+ if not self.is_cryopod:
743
+ return None
744
+
745
+ # Return cached if already parsed
746
+ if self._cryopod_creature is not None:
747
+ return self._cryopod_creature
748
+
749
+ # Try to parse from CustomItemDatas
750
+ ark_tribute = self.raw_data.get("ArkTributeItem", {})
751
+ custom_datas = ark_tribute.get("CustomItemDatas", [])
752
+
753
+ for entry in custom_datas:
754
+ # Look for the "Dino" data entry
755
+ if entry.get("CustomDataName") == "Dino":
756
+ # Prefer byte blob parsing (gives full creature/status properties)
757
+ cryo_bytes = entry.get("CustomDataBytes", {})
758
+ byte_arrays = cryo_bytes.get("ByteArrays", [])
759
+
760
+ if byte_arrays and "Bytes" in byte_arrays[0]:
761
+ byte_data = byte_arrays[0]["Bytes"]
762
+ self._cryopod_creature = CryopodCreature.from_cryopod_bytes(byte_data)
763
+ if self._cryopod_creature:
764
+ # Supplement with CustomDataStrings/Names if available
765
+ # (species name at index 9, color names from CustomDataNames)
766
+ strings = entry.get("CustomDataStrings", [])
767
+ if len(strings) > 9 and strings[9]:
768
+ self._cryopod_creature.species = strings[9]
769
+ color_names = entry.get("CustomDataNames", [])
770
+ if color_names:
771
+ self._cryopod_creature.color_names = list(color_names)
772
+ return self._cryopod_creature
773
+
774
+ # Fall back to CustomDataStrings/Floats parsing
775
+ if entry.get("CustomDataStrings"):
776
+ self._cryopod_creature = CryopodCreature.from_asa_cryopod_data(entry)
777
+ if self._cryopod_creature:
778
+ return self._cryopod_creature
779
+
780
+ return None
781
+
782
+ def to_dict(self) -> dict[str, t.Any]:
783
+ """Convert to dictionary."""
784
+ return {
785
+ "blueprint": self.blueprint,
786
+ "name": self.name,
787
+ "custom_name": self.custom_name,
788
+ "display_name": self.display_name,
789
+ "item_id1": self.item_id1,
790
+ "item_id2": self.item_id2,
791
+ "unique_id": self.unique_id,
792
+ "quantity": self.quantity,
793
+ "quality_index": self.quality_index,
794
+ "quality_name": self.quality_name,
795
+ "durability": self.durability,
796
+ "rating": self.rating,
797
+ "slot_index": self.slot_index,
798
+ "is_blueprint": self.is_blueprint,
799
+ "is_engram": self.is_engram,
800
+ "upload_time": self.upload_time,
801
+ }