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/files/base.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base class for ARK save file formats.
|
|
3
|
+
|
|
4
|
+
All file types (Profile, Tribe, CloudInventory) share the same basic structure:
|
|
5
|
+
1. Version header (Int32)
|
|
6
|
+
2. Object list (Int32 count + GameObject headers)
|
|
7
|
+
3. Properties block (properties for each object)
|
|
8
|
+
|
|
9
|
+
This base class handles the common parsing logic.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import typing as t
|
|
15
|
+
from abc import ABC
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from arkparser.common.binary_reader import BinaryReader
|
|
20
|
+
from arkparser.common.exceptions import ArkParseError
|
|
21
|
+
from arkparser.game_objects.container import GameObjectContainer
|
|
22
|
+
from arkparser.game_objects.game_object import GameObject
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ArkFile(ABC):
|
|
27
|
+
"""
|
|
28
|
+
Abstract base class for ARK save file formats.
|
|
29
|
+
|
|
30
|
+
All ARK save files share this structure:
|
|
31
|
+
- Int32 version number
|
|
32
|
+
- Int32 object count
|
|
33
|
+
- Object headers (repeated object_count times)
|
|
34
|
+
- Properties block (properties for each object)
|
|
35
|
+
|
|
36
|
+
Subclasses define which version numbers are valid and which
|
|
37
|
+
class name identifies the "main" object in the file.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
version: int
|
|
41
|
+
objects: list[GameObject] = field(default_factory=list)
|
|
42
|
+
container: GameObjectContainer = field(default_factory=GameObjectContainer)
|
|
43
|
+
is_asa: bool = False
|
|
44
|
+
|
|
45
|
+
# Subclasses must define these
|
|
46
|
+
VALID_VERSIONS: t.ClassVar[tuple[int, ...]] = ()
|
|
47
|
+
MAIN_CLASS_NAME: t.ClassVar[str] = ""
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def main_object(self) -> GameObject | None:
|
|
51
|
+
"""
|
|
52
|
+
Get the main object for this file type.
|
|
53
|
+
|
|
54
|
+
For profiles, this is the PrimalPlayerData object.
|
|
55
|
+
For tribes, this is the PrimalTribeData object.
|
|
56
|
+
For cloud inventory, this is the ArkCloudInventoryData object.
|
|
57
|
+
"""
|
|
58
|
+
for obj in self.objects:
|
|
59
|
+
# ASA uses full path like "/Script/ShooterGame.ArkCloudInventoryData"
|
|
60
|
+
# ASE uses just "ArkCloudInventoryData"
|
|
61
|
+
if self.MAIN_CLASS_NAME in obj.class_name:
|
|
62
|
+
return obj
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def load(cls, source: str | Path | bytes) -> t.Self:
|
|
67
|
+
"""
|
|
68
|
+
Load a file from path or bytes.
|
|
69
|
+
|
|
70
|
+
Automatically detects ASE vs ASA format based on file structure.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
source: File path (str or Path) or raw bytes
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Parsed file instance
|
|
77
|
+
|
|
78
|
+
Raises:
|
|
79
|
+
ArkParseError: If the file cannot be parsed
|
|
80
|
+
FileNotFoundError: If the file path doesn't exist
|
|
81
|
+
"""
|
|
82
|
+
if isinstance(source, bytes):
|
|
83
|
+
reader = BinaryReader.from_bytes(source)
|
|
84
|
+
else:
|
|
85
|
+
path = Path(source)
|
|
86
|
+
if not path.exists():
|
|
87
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
88
|
+
reader = BinaryReader.from_file(path)
|
|
89
|
+
|
|
90
|
+
return cls._parse(reader)
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def _parse(cls, reader: BinaryReader) -> t.Self:
|
|
94
|
+
"""
|
|
95
|
+
Parse the file from a binary reader.
|
|
96
|
+
|
|
97
|
+
ASE File structure (profiles/tribes version 1, world saves version 5-6):
|
|
98
|
+
1. Int32 version
|
|
99
|
+
2. Int32 object_count
|
|
100
|
+
3. (16 zero bytes padding for profiles/tribes)
|
|
101
|
+
4. Object headers (object_count times)
|
|
102
|
+
5. Properties for each object
|
|
103
|
+
|
|
104
|
+
ASA File structure (profiles/tribes version 6, cloud inventory version 7+):
|
|
105
|
+
For version 6 (profiles/tribes):
|
|
106
|
+
1. Int32 version
|
|
107
|
+
2. Int32 object_count
|
|
108
|
+
3. No extra header - object headers with GUIDs follow immediately
|
|
109
|
+
4. Object headers with embedded GUIDs
|
|
110
|
+
5. Properties for each object
|
|
111
|
+
|
|
112
|
+
For version 7+ (cloud inventory):
|
|
113
|
+
1. Int32 version
|
|
114
|
+
2. Int32 unknown1 (extra field)
|
|
115
|
+
3. Int32 unknown2 (extra field)
|
|
116
|
+
4. Int32 object_count
|
|
117
|
+
5. Object headers with GUIDs
|
|
118
|
+
6. Properties for each object
|
|
119
|
+
"""
|
|
120
|
+
# Read version
|
|
121
|
+
version = reader.read_int32()
|
|
122
|
+
|
|
123
|
+
if cls.VALID_VERSIONS and version not in cls.VALID_VERSIONS:
|
|
124
|
+
raise ArkParseError(f"Unsupported {cls.__name__} version {version}. Expected one of: {cls.VALID_VERSIONS}")
|
|
125
|
+
|
|
126
|
+
# Detect ASE vs ASA
|
|
127
|
+
is_asa = cls._detect_asa(reader, version)
|
|
128
|
+
|
|
129
|
+
if is_asa and version >= 7:
|
|
130
|
+
# Only cloud inventory (version 7+) has extra header fields before object count
|
|
131
|
+
_unknown1 = reader.read_int32()
|
|
132
|
+
_unknown2 = reader.read_int32()
|
|
133
|
+
|
|
134
|
+
# Read object count
|
|
135
|
+
object_count = reader.read_int32()
|
|
136
|
+
|
|
137
|
+
if object_count < 0 or object_count > 1000000:
|
|
138
|
+
raise ArkParseError(f"Invalid object count: {object_count}")
|
|
139
|
+
|
|
140
|
+
# Read object headers
|
|
141
|
+
objects: list[GameObject] = []
|
|
142
|
+
for i in range(object_count):
|
|
143
|
+
obj = cls._read_object_header(reader, obj_id=i, is_asa=is_asa, version=version)
|
|
144
|
+
objects.append(obj)
|
|
145
|
+
|
|
146
|
+
# For profile/tribe/obelisk files (version 1), propertiesOffset is absolute from file start
|
|
147
|
+
# For world save files (version 5-7+), propertiesOffset is relative to a base offset in the header
|
|
148
|
+
# Since we're parsing simple files here, we use absolute offsets (properties_block_offset = 0)
|
|
149
|
+
properties_block_offset = 0
|
|
150
|
+
|
|
151
|
+
# Load properties for each object
|
|
152
|
+
# Properties are read in order, with each object's properties
|
|
153
|
+
# starting at its propertiesOffset (absolute from file start for these file types)
|
|
154
|
+
for i, obj in enumerate(objects):
|
|
155
|
+
# Get next object for boundary checking (optional)
|
|
156
|
+
next_obj = objects[i + 1] if i + 1 < len(objects) else None
|
|
157
|
+
obj.load_properties(
|
|
158
|
+
reader, properties_block_offset=properties_block_offset, is_asa=is_asa, next_object=next_obj
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Build container with lookups
|
|
162
|
+
container = GameObjectContainer(objects=objects)
|
|
163
|
+
container.build_relationships()
|
|
164
|
+
|
|
165
|
+
return cls(
|
|
166
|
+
version=version,
|
|
167
|
+
objects=objects,
|
|
168
|
+
container=container,
|
|
169
|
+
is_asa=is_asa,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def _read_object_header(cls, reader: BinaryReader, obj_id: int, is_asa: bool, version: int = 7) -> GameObject:
|
|
174
|
+
"""
|
|
175
|
+
Read an object header in ASE or ASA format.
|
|
176
|
+
|
|
177
|
+
ASE object header:
|
|
178
|
+
- Class name (string)
|
|
179
|
+
- Item1 flag (int32)
|
|
180
|
+
- Item2 flag (int32)
|
|
181
|
+
- Names (string array)
|
|
182
|
+
- IsItem flag (int32, 0 or 1)
|
|
183
|
+
- More fields...
|
|
184
|
+
|
|
185
|
+
ASA object header:
|
|
186
|
+
- GUID (16 bytes)
|
|
187
|
+
- Class name (string)
|
|
188
|
+
- Field1 (int32) - unknown purpose
|
|
189
|
+
- names_count (int32) - number of name strings to read
|
|
190
|
+
- Names array (names_count strings)
|
|
191
|
+
- Padding (20 bytes): 12 zeros + Int32 properties_offset + 4 zeros
|
|
192
|
+
"""
|
|
193
|
+
if is_asa:
|
|
194
|
+
obj = GameObject(id=obj_id)
|
|
195
|
+
|
|
196
|
+
# Read GUID
|
|
197
|
+
guid = reader.read_guid()
|
|
198
|
+
obj.guid = str(guid)
|
|
199
|
+
|
|
200
|
+
# Class name
|
|
201
|
+
obj.class_name = reader.read_string()
|
|
202
|
+
|
|
203
|
+
# Unknown field and names count
|
|
204
|
+
_field1 = reader.read_int32()
|
|
205
|
+
names_count = reader.read_int32()
|
|
206
|
+
|
|
207
|
+
# Read all names (first is typically instance name, rest are world context)
|
|
208
|
+
names = []
|
|
209
|
+
for _ in range(names_count):
|
|
210
|
+
name = reader.read_string()
|
|
211
|
+
names.append(name)
|
|
212
|
+
obj.names = names
|
|
213
|
+
|
|
214
|
+
# Read padding/metadata after names:
|
|
215
|
+
# Layout: 12 zero bytes + int32 properties_offset + 4 zero bytes = 20 bytes
|
|
216
|
+
# The stored properties_offset for v7 points to one byte before the actual
|
|
217
|
+
# properties start. That byte is a 0x00 terminator in most single-object
|
|
218
|
+
# files; in multi-object files, that byte is the first byte of the next
|
|
219
|
+
# object's GUID (non-zero). Either way, actual properties = stored + 1 for v7.
|
|
220
|
+
# For v6, the stored offset IS the exact properties start (no +1 needed).
|
|
221
|
+
reader.skip(12) # 12 padding zeros
|
|
222
|
+
stored_props_offset = reader.read_int32() # stored absolute file offset
|
|
223
|
+
reader.skip(4) # 4 trailing zeros
|
|
224
|
+
# Consume the optional 0x00 terminator byte if present (most v7 single-obj files)
|
|
225
|
+
if version >= 7 and reader.remaining > 0 and reader.peek_bytes(1)[0] == 0x00:
|
|
226
|
+
reader.skip(1)
|
|
227
|
+
|
|
228
|
+
obj.properties_offset = stored_props_offset + (1 if version >= 7 else 0)
|
|
229
|
+
|
|
230
|
+
return obj
|
|
231
|
+
else:
|
|
232
|
+
return GameObject.read_header(reader, obj_id=obj_id, is_asa=False)
|
|
233
|
+
|
|
234
|
+
@classmethod
|
|
235
|
+
def _detect_asa(cls, reader: BinaryReader, version: int) -> bool:
|
|
236
|
+
"""
|
|
237
|
+
Detect if this is an ASA format file.
|
|
238
|
+
|
|
239
|
+
Detection heuristics:
|
|
240
|
+
- Version 7+ is typically ASA (cloud inventory/obelisk)
|
|
241
|
+
- Version 6 with non-zero GUID at offset 8 is ASA (profiles/tribes)
|
|
242
|
+
- Version 6 with zeros at offset 8 is ASE
|
|
243
|
+
|
|
244
|
+
File structure at this point (after reading version):
|
|
245
|
+
- ASE: object_count (4) + zeros (16) + object headers
|
|
246
|
+
- ASA: object_count (4) + GUID (16) + object headers
|
|
247
|
+
|
|
248
|
+
The GUID check distinguishes ASE v6 world saves from ASA profiles.
|
|
249
|
+
"""
|
|
250
|
+
# Version 7+ is always ASA
|
|
251
|
+
if version >= 7:
|
|
252
|
+
return True
|
|
253
|
+
|
|
254
|
+
# Version 6 could be either ASE world save or ASA profile/tribe
|
|
255
|
+
# Check for non-zero GUID at offset 8 (after version + object_count)
|
|
256
|
+
if version == 6:
|
|
257
|
+
# Save position after version read
|
|
258
|
+
current_pos = reader.position
|
|
259
|
+
# Skip object_count (4 bytes) to reach potential GUID position
|
|
260
|
+
reader.skip(4)
|
|
261
|
+
# Read 16 bytes that would be GUID in ASA or zeros in ASE
|
|
262
|
+
guid_bytes = reader.read_bytes(16)
|
|
263
|
+
# Restore position
|
|
264
|
+
reader.position = current_pos
|
|
265
|
+
# If any byte is non-zero, it's ASA (has a GUID)
|
|
266
|
+
if any(b != 0 for b in guid_bytes):
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
def get_property_value(self, name: str, default: t.Any = None, from_main: bool = True) -> t.Any:
|
|
272
|
+
"""
|
|
273
|
+
Get a property value from the main object.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
name: Property name to look up
|
|
277
|
+
default: Default value if not found
|
|
278
|
+
from_main: If True, get from main object. If False, search all objects.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Property value or default
|
|
282
|
+
"""
|
|
283
|
+
if from_main and self.main_object:
|
|
284
|
+
return self.main_object.get_property_value(name, default)
|
|
285
|
+
|
|
286
|
+
# Search all objects
|
|
287
|
+
for obj in self.objects:
|
|
288
|
+
value = obj.get_property_value(name)
|
|
289
|
+
if value is not None:
|
|
290
|
+
return value
|
|
291
|
+
return default
|
|
292
|
+
|
|
293
|
+
def to_dict(self) -> dict[str, t.Any]:
|
|
294
|
+
"""
|
|
295
|
+
Convert to a dictionary representation.
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Dictionary with file data
|
|
299
|
+
"""
|
|
300
|
+
return {
|
|
301
|
+
"version": self.version,
|
|
302
|
+
"is_asa": self.is_asa,
|
|
303
|
+
"object_count": len(self.objects),
|
|
304
|
+
"main_object": self.main_object.to_dict() if self.main_object else None,
|
|
305
|
+
"objects": [obj.to_dict() for obj in self.objects],
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
def __repr__(self) -> str:
|
|
309
|
+
return f"{self.__class__.__name__}(version={self.version}, objects={len(self.objects)}, is_asa={self.is_asa})"
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cloud inventory parser for obelisk/ARK data files.
|
|
3
|
+
|
|
4
|
+
Cloud inventory files contain uploaded data including:
|
|
5
|
+
- Uploaded creatures (dinos)
|
|
6
|
+
- Uploaded items (including cryopods with dinos inside)
|
|
7
|
+
- Uploaded characters
|
|
8
|
+
- Transfer timers
|
|
9
|
+
|
|
10
|
+
These files have no extension and are typically found in obelisk directories.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import typing as t
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
|
|
18
|
+
from arkparser.common.binary_reader import BinaryReader
|
|
19
|
+
from arkparser.common.exceptions import ArkParseError
|
|
20
|
+
from arkparser.data_models import UploadedCreature, UploadedItem
|
|
21
|
+
from arkparser.game_objects.container import GameObjectContainer
|
|
22
|
+
from arkparser.game_objects.game_object import GameObject
|
|
23
|
+
|
|
24
|
+
from .base import ArkFile
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class CloudInventory(ArkFile):
|
|
29
|
+
"""
|
|
30
|
+
Parser for obelisk/cloud inventory data files.
|
|
31
|
+
|
|
32
|
+
The main object has class name "ArkCloudInventoryData".
|
|
33
|
+
|
|
34
|
+
Supports both ASE (versions 1-6) and ASA (version 7+) formats.
|
|
35
|
+
|
|
36
|
+
Example usage:
|
|
37
|
+
>>> inv = CloudInventory.load("examples/ase/obelisk/2533274922942310")
|
|
38
|
+
>>> print(f"Creatures: {len(inv.uploaded_creatures)}")
|
|
39
|
+
>>> for creature in inv.uploaded_creatures:
|
|
40
|
+
... print(f" {creature.name} ({creature.species}) Lvl {creature.level}")
|
|
41
|
+
>>> for item in inv.uploaded_items:
|
|
42
|
+
... print(f" {item.display_name} ({item.quality_name})")
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
VALID_VERSIONS: t.ClassVar[tuple[int, ...]] = (1, 2, 3, 4, 5, 6, 7)
|
|
46
|
+
MAIN_CLASS_NAME: t.ClassVar[str] = "ArkCloudInventoryData"
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def _parse(cls, reader: BinaryReader) -> t.Self:
|
|
50
|
+
"""
|
|
51
|
+
Parse the cloud inventory file.
|
|
52
|
+
|
|
53
|
+
ASE format:
|
|
54
|
+
- Int32 version
|
|
55
|
+
- Int32 object_count
|
|
56
|
+
- Object headers (ASE format)
|
|
57
|
+
- Properties
|
|
58
|
+
|
|
59
|
+
ASA format:
|
|
60
|
+
- Int32 version
|
|
61
|
+
- Int32 unknown1 (extra field)
|
|
62
|
+
- Int32 unknown2 (extra field)
|
|
63
|
+
- Int32 object_count
|
|
64
|
+
- Object headers (ASA format with GUIDs)
|
|
65
|
+
- Properties
|
|
66
|
+
"""
|
|
67
|
+
# Read version
|
|
68
|
+
version = reader.read_int32()
|
|
69
|
+
|
|
70
|
+
if cls.VALID_VERSIONS and version not in cls.VALID_VERSIONS:
|
|
71
|
+
raise ArkParseError(f"Unsupported {cls.__name__} version {version}. Expected one of: {cls.VALID_VERSIONS}")
|
|
72
|
+
|
|
73
|
+
# Detect ASE vs ASA
|
|
74
|
+
is_asa = cls._detect_asa(reader, version)
|
|
75
|
+
|
|
76
|
+
if is_asa:
|
|
77
|
+
# ASA has extra header fields
|
|
78
|
+
_unknown1 = reader.read_int32()
|
|
79
|
+
_unknown2 = reader.read_int32()
|
|
80
|
+
|
|
81
|
+
# Read object count
|
|
82
|
+
object_count = reader.read_int32()
|
|
83
|
+
|
|
84
|
+
if object_count < 0 or object_count > 1000000:
|
|
85
|
+
raise ArkParseError(f"Invalid object count: {object_count}")
|
|
86
|
+
|
|
87
|
+
# Read object headers
|
|
88
|
+
objects: list[GameObject] = []
|
|
89
|
+
for i in range(object_count):
|
|
90
|
+
if is_asa:
|
|
91
|
+
obj = cls._read_asa_object_header(reader, obj_id=i)
|
|
92
|
+
else:
|
|
93
|
+
obj = GameObject.read_header(reader, obj_id=i, is_asa=False)
|
|
94
|
+
objects.append(obj)
|
|
95
|
+
|
|
96
|
+
# Load properties for each object
|
|
97
|
+
properties_block_offset = 0
|
|
98
|
+
for i, obj in enumerate(objects):
|
|
99
|
+
next_obj = objects[i + 1] if i + 1 < len(objects) else None
|
|
100
|
+
obj.load_properties(
|
|
101
|
+
reader, properties_block_offset=properties_block_offset, is_asa=is_asa, next_object=next_obj
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Build container with lookups
|
|
105
|
+
container = GameObjectContainer(objects=objects)
|
|
106
|
+
container.build_relationships()
|
|
107
|
+
|
|
108
|
+
return cls(
|
|
109
|
+
version=version,
|
|
110
|
+
objects=objects,
|
|
111
|
+
container=container,
|
|
112
|
+
is_asa=is_asa,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def _read_asa_object_header(cls, reader: BinaryReader, obj_id: int) -> GameObject:
|
|
117
|
+
"""
|
|
118
|
+
Read an ASA object header.
|
|
119
|
+
|
|
120
|
+
ASA obelisk object header format:
|
|
121
|
+
- GUID (16 bytes)
|
|
122
|
+
- Class name (string)
|
|
123
|
+
- Field1 (int32)
|
|
124
|
+
- Field2 (int32)
|
|
125
|
+
- Instance name (string)
|
|
126
|
+
- Padding (21 bytes)
|
|
127
|
+
"""
|
|
128
|
+
obj = GameObject(id=obj_id)
|
|
129
|
+
|
|
130
|
+
# Read GUID
|
|
131
|
+
guid = reader.read_guid()
|
|
132
|
+
obj.guid = str(guid)
|
|
133
|
+
|
|
134
|
+
# Class name
|
|
135
|
+
obj.class_name = reader.read_string()
|
|
136
|
+
|
|
137
|
+
# Fields (not sure what these represent)
|
|
138
|
+
_field1 = reader.read_int32()
|
|
139
|
+
_field2 = reader.read_int32()
|
|
140
|
+
|
|
141
|
+
# Instance name
|
|
142
|
+
instance_name = reader.read_string()
|
|
143
|
+
obj.names = [instance_name] if instance_name else []
|
|
144
|
+
|
|
145
|
+
# Skip 21 bytes of padding
|
|
146
|
+
reader.skip(21)
|
|
147
|
+
|
|
148
|
+
# Properties offset will be set by sequential reading
|
|
149
|
+
obj.properties_offset = reader.position
|
|
150
|
+
|
|
151
|
+
return obj
|
|
152
|
+
|
|
153
|
+
# =========================================================================
|
|
154
|
+
# Data Extraction - Primary API
|
|
155
|
+
# =========================================================================
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def uploaded_creatures(self) -> list[UploadedCreature]:
|
|
159
|
+
"""
|
|
160
|
+
Get all uploaded creatures as structured data.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of UploadedCreature objects with typed fields.
|
|
164
|
+
"""
|
|
165
|
+
my_ark_data = self.get_property_value("MyArkData")
|
|
166
|
+
if not my_ark_data:
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
dino_data_list = my_ark_data.get("ArkTamedDinosData", [])
|
|
170
|
+
return [UploadedCreature.from_ark_data(d) for d in dino_data_list]
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def uploaded_items(self) -> list[UploadedItem]:
|
|
174
|
+
"""
|
|
175
|
+
Get all uploaded items as structured data.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of UploadedItem objects with typed fields.
|
|
179
|
+
"""
|
|
180
|
+
my_ark_data = self.get_property_value("MyArkData")
|
|
181
|
+
if not my_ark_data:
|
|
182
|
+
return []
|
|
183
|
+
|
|
184
|
+
item_data_list = my_ark_data.get("ArkItems", [])
|
|
185
|
+
return [UploadedItem.from_ark_data(d) for d in item_data_list]
|
|
186
|
+
|
|
187
|
+
# =========================================================================
|
|
188
|
+
# Convenience Properties
|
|
189
|
+
# =========================================================================
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def creature_count(self) -> int:
|
|
193
|
+
"""Get number of uploaded creatures."""
|
|
194
|
+
return len(self.uploaded_creatures)
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def item_count(self) -> int:
|
|
198
|
+
"""Get number of uploaded items."""
|
|
199
|
+
return len(self.uploaded_items)
|
|
200
|
+
|
|
201
|
+
# =========================================================================
|
|
202
|
+
# Legacy API (for backward compatibility)
|
|
203
|
+
# =========================================================================
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def creatures(self) -> list[GameObject]:
|
|
207
|
+
"""
|
|
208
|
+
Get creatures as raw GameObjects.
|
|
209
|
+
|
|
210
|
+
Note: Use `uploaded_creatures` for structured data access.
|
|
211
|
+
"""
|
|
212
|
+
return self.container.get_creatures()
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def items(self) -> list[GameObject]:
|
|
216
|
+
"""
|
|
217
|
+
Get items as raw GameObjects.
|
|
218
|
+
|
|
219
|
+
Note: Use `uploaded_items` for structured data access.
|
|
220
|
+
"""
|
|
221
|
+
result = []
|
|
222
|
+
for obj in self.objects:
|
|
223
|
+
class_name = obj.class_name or ""
|
|
224
|
+
if "PrimalItem" in class_name or "Item" in class_name:
|
|
225
|
+
result.append(obj)
|
|
226
|
+
return result
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def characters(self) -> list[GameObject]:
|
|
230
|
+
"""
|
|
231
|
+
Get all uploaded player characters.
|
|
232
|
+
|
|
233
|
+
Characters are identified by having PlayerPawnTest class name.
|
|
234
|
+
"""
|
|
235
|
+
result = []
|
|
236
|
+
for obj in self.objects:
|
|
237
|
+
class_name = obj.class_name or ""
|
|
238
|
+
if "PlayerPawnTest" in class_name:
|
|
239
|
+
result.append(obj)
|
|
240
|
+
return result
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def character_count(self) -> int:
|
|
244
|
+
"""Get number of uploaded characters."""
|
|
245
|
+
return len(self.characters)
|
|
246
|
+
|
|
247
|
+
def to_dict(self) -> dict[str, t.Any]:
|
|
248
|
+
"""Convert to dictionary with cloud inventory-specific fields."""
|
|
249
|
+
base_dict = super().to_dict()
|
|
250
|
+
base_dict.update(
|
|
251
|
+
{
|
|
252
|
+
"creature_count": self.creature_count,
|
|
253
|
+
"item_count": self.item_count,
|
|
254
|
+
"character_count": self.character_count,
|
|
255
|
+
"uploaded_creatures": [c.to_dict() for c in self.uploaded_creatures],
|
|
256
|
+
"uploaded_items": [i.to_dict() for i in self.uploaded_items],
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
return base_dict
|