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.
- arkparser/__init__.py +117 -0
- arkparser/common/__init__.py +72 -0
- arkparser/common/binary_reader.py +402 -0
- arkparser/common/exceptions.py +99 -0
- arkparser/common/map_config.py +166 -0
- arkparser/common/types.py +249 -0
- arkparser/common/version_detection.py +195 -0
- arkparser/data_models.py +801 -0
- arkparser/export.py +485 -0
- arkparser/files/__init__.py +25 -0
- arkparser/files/base.py +309 -0
- arkparser/files/cloud_inventory.py +259 -0
- arkparser/files/profile.py +205 -0
- arkparser/files/tribe.py +155 -0
- arkparser/files/world_save.py +699 -0
- arkparser/game_objects/__init__.py +32 -0
- arkparser/game_objects/container.py +180 -0
- arkparser/game_objects/game_object.py +273 -0
- arkparser/game_objects/location.py +87 -0
- arkparser/models/__init__.py +29 -0
- arkparser/models/character.py +227 -0
- arkparser/models/creature.py +642 -0
- arkparser/models/item.py +207 -0
- arkparser/models/player.py +263 -0
- arkparser/models/stats.py +226 -0
- arkparser/models/structure.py +176 -0
- arkparser/models/tribe.py +291 -0
- arkparser/properties/__init__.py +77 -0
- arkparser/properties/base.py +329 -0
- arkparser/properties/byte_property.py +230 -0
- arkparser/properties/compound.py +1125 -0
- arkparser/properties/primitives.py +803 -0
- arkparser/properties/registry.py +236 -0
- arkparser/py.typed +0 -0
- arkparser/structs/__init__.py +60 -0
- arkparser/structs/base.py +63 -0
- arkparser/structs/colors.py +108 -0
- arkparser/structs/misc.py +133 -0
- arkparser/structs/property_list.py +101 -0
- arkparser/structs/registry.py +140 -0
- arkparser/structs/vectors.py +221 -0
- arkparser-0.1.0.dist-info/METADATA +833 -0
- arkparser-0.1.0.dist-info/RECORD +46 -0
- arkparser-0.1.0.dist-info/WHEEL +5 -0
- arkparser-0.1.0.dist-info/licenses/LICENSE +21 -0
- arkparser-0.1.0.dist-info/top_level.txt +1 -0
arkparser/data_models.py
ADDED
|
@@ -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
|
+
}
|