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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map configuration for UE coordinate to GPS lat/lon conversion.
|
|
3
|
+
|
|
4
|
+
Formula (from C# reference):
|
|
5
|
+
latitude = lat_shift + (y / lat_div)
|
|
6
|
+
longitude = lon_shift + (x / lon_div)
|
|
7
|
+
|
|
8
|
+
Where x, y are Unreal Engine world coordinates.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import typing as t
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class MapConfig:
|
|
19
|
+
"""
|
|
20
|
+
Configuration for a specific ARK map.
|
|
21
|
+
|
|
22
|
+
Used to convert Unreal Engine coordinates to GPS lat/lon.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
name: Human-readable map name.
|
|
26
|
+
filename: Save file name (e.g., "theisland.ark").
|
|
27
|
+
lat_shift: Latitude origin offset.
|
|
28
|
+
lat_div: Latitude divisor (scale).
|
|
29
|
+
lon_shift: Longitude origin offset.
|
|
30
|
+
lon_div: Longitude divisor (scale).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
filename: str
|
|
35
|
+
lat_shift: float = 50.0
|
|
36
|
+
lat_div: float = 8000.0
|
|
37
|
+
lon_shift: float = 50.0
|
|
38
|
+
lon_div: float = 8000.0
|
|
39
|
+
|
|
40
|
+
def ue_to_lat(self, y: float) -> float:
|
|
41
|
+
"""Convert UE Y coordinate to latitude."""
|
|
42
|
+
return self.lat_shift + (y / self.lat_div)
|
|
43
|
+
|
|
44
|
+
def ue_to_lon(self, x: float) -> float:
|
|
45
|
+
"""Convert UE X coordinate to longitude."""
|
|
46
|
+
return self.lon_shift + (x / self.lon_div)
|
|
47
|
+
|
|
48
|
+
def ue_to_gps(self, x: float, y: float) -> tuple[float, float]:
|
|
49
|
+
"""
|
|
50
|
+
Convert UE coordinates to GPS (latitude, longitude).
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tuple of (latitude, longitude).
|
|
54
|
+
"""
|
|
55
|
+
return (self.ue_to_lat(y), self.ue_to_lon(x))
|
|
56
|
+
|
|
57
|
+
def ccc_string(self, x: float, y: float, z: float) -> str:
|
|
58
|
+
"""Format coordinates as a cheat setplayerpos string."""
|
|
59
|
+
return f"{x} {y} {z}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ============================================================================
|
|
63
|
+
# Built-in map configurations from C# ArkViewer maps.json
|
|
64
|
+
# ============================================================================
|
|
65
|
+
|
|
66
|
+
# fmt: off
|
|
67
|
+
_MAP_CONFIGS: list[MapConfig] = [
|
|
68
|
+
# Official ASE Maps
|
|
69
|
+
MapConfig("The Island (Evolved)", "theisland.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
70
|
+
MapConfig("Scorched Earth", "scorchedearth_p.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
71
|
+
MapConfig("Aberration (Evolved)", "aberration_p.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
72
|
+
MapConfig("Extinction (Evolved)", "extinction.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
73
|
+
MapConfig("The Center (Evolved)", "thecenter.ark", 30.34223747253418, 9584.0, 55.10416793823242, 9600.0),
|
|
74
|
+
MapConfig("Ragnarok", "ragnarok.ark", 50.009388, 13100.0, 50.009388, 13100.0),
|
|
75
|
+
MapConfig("Valguero", "valguero_p.ark", 50.0, 8161.0, 50.0, 8161.0),
|
|
76
|
+
MapConfig("Crystal Isles", "crystalisles.ark", 48.687, 15882.02, 49.9481, 16988.76),
|
|
77
|
+
MapConfig("Genesis", "genesis.ark", 50.0, 10500.0, 50.0, 10500.0),
|
|
78
|
+
MapConfig("Genesis 2", "gen2.ark", 49.6, 14500.0, 49.6, 14500.0),
|
|
79
|
+
MapConfig("Lost Island", "lostisland.ark", 51.6, 15300.0, 49.0, 15300.0),
|
|
80
|
+
MapConfig("Fjordur", "fjordur.ark", 50.0, 7140.0, 50.0, 7140.0),
|
|
81
|
+
|
|
82
|
+
# Official ASA Maps
|
|
83
|
+
MapConfig("The Island (Ascended)", "theisland_wp.ark", 50.0, 6850.0, 50.0, 6850.0),
|
|
84
|
+
MapConfig("The Center (Ascended)", "thecenter_wp.ark", 32.5, 10380.52, 50.5, 10374.29),
|
|
85
|
+
MapConfig("Scorched Earth (Ascended)", "scorchedearth_wp.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
86
|
+
MapConfig("Aberration (Ascended)", "aberration_wp.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
87
|
+
MapConfig("Extinction (Ascended)", "extinction_wp.ark", 50.0, 6850.0, 50.0, 6850.0),
|
|
88
|
+
MapConfig("Ragnarok (Ascended)", "ragnarok_wp.ark", 50.009388, 13100.0, 50.009388, 13100.0),
|
|
89
|
+
MapConfig("Valguero (Ascended)", "valguero_wp.ark", 50.0, 8161.0, 50.0, 8161.0),
|
|
90
|
+
|
|
91
|
+
# Community / Modded Maps
|
|
92
|
+
MapConfig("Astral ARK", "astralark.ark", 50.0, 2000.0, 50.0, 2000.0),
|
|
93
|
+
MapConfig("Hope", "hope.ark", 50.0, 6850.0, 50.0, 6850.0),
|
|
94
|
+
MapConfig("Tunguska", "tunguska_p.ark", 46.8, 14000.0, 49.29, 13300.0),
|
|
95
|
+
MapConfig("Caballus", "caballus_p.ark", 50.0, 8125.0, 50.0, 8125.0),
|
|
96
|
+
MapConfig("Tiamat Prime", "tiamatprime.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
97
|
+
MapConfig("Glacius", "glacius_p.ark", 50.0, 16250.0, 50.0, 16250.0),
|
|
98
|
+
MapConfig("Antartika", "antartika.ark", 50.0, 8000.0, 50.0, 8000.0),
|
|
99
|
+
MapConfig("Amissa (Evolved)", "amissa.ark", 49.9, 10900.0, 49.9, 10850.0),
|
|
100
|
+
MapConfig("Amissa (Ascended)", "amissa_wp.ark", 46.9, 11375.0, 48.1, 11400.0),
|
|
101
|
+
MapConfig("Olympus", "olympus.ark", 0.0, 8130.0, 0.0, 8130.0),
|
|
102
|
+
MapConfig("Ebenus Astrum", "ebenusastrum.ark", 52.9, 8650.0, 25.0, 18500.0),
|
|
103
|
+
MapConfig("ARKForum Event Map", "arkforum_eventmap.ark", 50.0, 1500.0, 50.0, 1500.0),
|
|
104
|
+
MapConfig("The Volcano", "thevolcano.ark", 50.0, 9200.0, 50.0, 9200.0),
|
|
105
|
+
MapConfig("The Earrion", "earrion_p.ark", 50.0, 6250.0, 50.0, 6250.0),
|
|
106
|
+
MapConfig("Alemia", "Alemia_P.ark", 50.0, 8150.0, 50.0, 8150.0),
|
|
107
|
+
MapConfig("Velius", "Velius_P.ark", 50.0, 8575.0, 50.0, 8575.0),
|
|
108
|
+
MapConfig("Svartalfheim (Evolved)", "Svartalfheim.ark", 50.0, 4065.0, 50.0, 4065.0),
|
|
109
|
+
MapConfig("Svartalfheim (Ascended)", "Svartalfheim_WP.ark", 50.0, 4055.0, 50.0, 4055.0),
|
|
110
|
+
MapConfig("TaeniaStella", "TaeniaStella.ark", 48.9, 15500.0, 48.9, 15500.0),
|
|
111
|
+
MapConfig("Forglar (Ascended)", "forglar_wp.ark", 61.4, 7150.0, 69.8, 7945.0),
|
|
112
|
+
MapConfig("Insaluna (Ascended)", "insaluna_wp.ark", 50.0, 9400.0, 50.0, 9400.0),
|
|
113
|
+
MapConfig("Temptress Lagoon (Ascended)", "temptress_wp.ark", 50.0, 8150.0, 50.0, 8150.0),
|
|
114
|
+
MapConfig("Reverence (Ascended)", "reverence_wp.ark", 50.0, 8125.0, 50.0, 8125.0),
|
|
115
|
+
MapConfig("Nyrandil (Ascended)", "nyrandil.ark", 50.0, 8175.0, 50.0, 8175.0),
|
|
116
|
+
MapConfig("Astraeos (Ascended)", "astraeos_wp.ark", 50.0, 16000.0, 50.0, 16000.0),
|
|
117
|
+
MapConfig("Gun Smoke", "gunsmoke.ark", 12.1, 7900.0, 10.8, 7850.0),
|
|
118
|
+
MapConfig("Fjell", "viking_p.ark", 50.0, 7140.0, 50.0, 7140.0),
|
|
119
|
+
]
|
|
120
|
+
# fmt: on
|
|
121
|
+
|
|
122
|
+
# Build lookup by filename (case-insensitive)
|
|
123
|
+
_MAP_BY_FILENAME: dict[str, MapConfig] = {cfg.filename.lower(): cfg for cfg in _MAP_CONFIGS}
|
|
124
|
+
|
|
125
|
+
# Default config for unknown maps
|
|
126
|
+
DEFAULT_MAP_CONFIG = MapConfig("Unknown", "unknown.ark", 50.0, 8000.0, 50.0, 8000.0)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_map_config(filename: str) -> MapConfig:
|
|
130
|
+
"""
|
|
131
|
+
Get map configuration by save filename.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
filename: The save file name (e.g., "ragnarok.ark").
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
MapConfig for the map, or DEFAULT_MAP_CONFIG if not found.
|
|
138
|
+
"""
|
|
139
|
+
return _MAP_BY_FILENAME.get(filename.lower(), DEFAULT_MAP_CONFIG)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_map_config_by_name(name: str) -> MapConfig:
|
|
143
|
+
"""
|
|
144
|
+
Get map configuration by display name (case-insensitive partial match).
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Map display name or partial match (e.g., "Ragnarok", "The Island").
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
MapConfig for the map, or DEFAULT_MAP_CONFIG if not found.
|
|
151
|
+
"""
|
|
152
|
+
name_lower = name.lower()
|
|
153
|
+
for cfg in _MAP_CONFIGS:
|
|
154
|
+
if name_lower in cfg.name.lower():
|
|
155
|
+
return cfg
|
|
156
|
+
return DEFAULT_MAP_CONFIG
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def list_maps() -> list[MapConfig]:
|
|
160
|
+
"""
|
|
161
|
+
Get all available map configurations.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of all registered MapConfig instances.
|
|
165
|
+
"""
|
|
166
|
+
return list(_MAP_CONFIGS)
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core ARK Types.
|
|
3
|
+
|
|
4
|
+
This module defines the fundamental types used throughout ARK save files:
|
|
5
|
+
- ArkName: Unreal Engine's FName type (name + instance index)
|
|
6
|
+
- ObjectReference: References to other game objects
|
|
7
|
+
|
|
8
|
+
These types mirror Unreal Engine 4's serialization format.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import typing as t
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
# =============================================================================
|
|
18
|
+
# Constants
|
|
19
|
+
# =============================================================================
|
|
20
|
+
|
|
21
|
+
# The "None" name is used as a terminator in property lists
|
|
22
|
+
NAME_NONE = "None"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =============================================================================
|
|
26
|
+
# ArkName (FName)
|
|
27
|
+
# =============================================================================
|
|
28
|
+
|
|
29
|
+
# Pattern to extract instance index from name strings like "MyName_5"
|
|
30
|
+
_NAME_INDEX_PATTERN = re.compile(r"^(.+)_(\d+)$")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True, slots=True)
|
|
34
|
+
class ArkName:
|
|
35
|
+
"""
|
|
36
|
+
Unreal Engine FName type.
|
|
37
|
+
|
|
38
|
+
FNames consist of a base name and an instance index. They're used
|
|
39
|
+
extensively for property names, class names, and identifiers.
|
|
40
|
+
|
|
41
|
+
Format in files:
|
|
42
|
+
- Without name table: String with optional "_N" suffix
|
|
43
|
+
- With name table: Int32 index + Int32 instance
|
|
44
|
+
|
|
45
|
+
Instance numbering:
|
|
46
|
+
- Instance 0 = no suffix (or first occurrence)
|
|
47
|
+
- Instance 1 = "_0" suffix
|
|
48
|
+
- Instance 2 = "_1" suffix
|
|
49
|
+
- etc.
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
>>> ArkName.from_string("Health")
|
|
53
|
+
ArkName(name='Health', instance=0)
|
|
54
|
+
|
|
55
|
+
>>> ArkName.from_string("MyDino_5")
|
|
56
|
+
ArkName(name='MyDino', instance=6) # instance = parsed + 1
|
|
57
|
+
|
|
58
|
+
>>> str(ArkName("MyDino", 6))
|
|
59
|
+
'MyDino_5'
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
name: The base name string.
|
|
63
|
+
instance: The instance index (0 = no suffix).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name: str
|
|
67
|
+
instance: int = 0
|
|
68
|
+
|
|
69
|
+
def __str__(self) -> str:
|
|
70
|
+
"""Convert to string representation."""
|
|
71
|
+
if self.instance == 0:
|
|
72
|
+
return self.name
|
|
73
|
+
return f"{self.name}_{self.instance - 1}"
|
|
74
|
+
|
|
75
|
+
def __repr__(self) -> str:
|
|
76
|
+
"""Detailed representation for debugging."""
|
|
77
|
+
if self.instance == 0:
|
|
78
|
+
return f"ArkName({self.name!r})"
|
|
79
|
+
return f"ArkName({self.name!r}, instance={self.instance})"
|
|
80
|
+
|
|
81
|
+
def __eq__(self, other: object) -> bool:
|
|
82
|
+
"""Check equality with another ArkName."""
|
|
83
|
+
if isinstance(other, ArkName):
|
|
84
|
+
return self.name == other.name and self.instance == other.instance
|
|
85
|
+
return NotImplemented
|
|
86
|
+
|
|
87
|
+
def __hash__(self) -> int:
|
|
88
|
+
"""Hash for use in dicts/sets."""
|
|
89
|
+
return hash((self.name, self.instance))
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_none(self) -> bool:
|
|
93
|
+
"""Check if this is the 'None' terminator name."""
|
|
94
|
+
return self.name == NAME_NONE and self.instance == 0
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_string(cls, value: str) -> ArkName:
|
|
98
|
+
"""
|
|
99
|
+
Parse an ArkName from a string.
|
|
100
|
+
|
|
101
|
+
Handles the "_N" suffix convention:
|
|
102
|
+
- "Health" -> ArkName("Health", 0)
|
|
103
|
+
- "MyDino_0" -> ArkName("MyDino", 1)
|
|
104
|
+
- "Item_5" -> ArkName("Item", 6)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
value: The string to parse.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
An ArkName instance.
|
|
111
|
+
"""
|
|
112
|
+
if not value:
|
|
113
|
+
return cls("", 0)
|
|
114
|
+
|
|
115
|
+
match = _NAME_INDEX_PATTERN.match(value)
|
|
116
|
+
if match:
|
|
117
|
+
name = match.group(1)
|
|
118
|
+
index = int(match.group(2))
|
|
119
|
+
return cls(name, index + 1)
|
|
120
|
+
|
|
121
|
+
return cls(value, 0)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def from_parts(cls, name: str, instance: int) -> ArkName:
|
|
125
|
+
"""
|
|
126
|
+
Create an ArkName from separate name and instance.
|
|
127
|
+
|
|
128
|
+
Used when reading from a name table where name and instance
|
|
129
|
+
are stored separately.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
name: The base name.
|
|
133
|
+
instance: The instance index.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
An ArkName instance.
|
|
137
|
+
"""
|
|
138
|
+
return cls(name, instance)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def none(cls) -> ArkName:
|
|
142
|
+
"""Return the 'None' terminator name."""
|
|
143
|
+
return cls(NAME_NONE, 0)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
# =============================================================================
|
|
147
|
+
# ObjectReference
|
|
148
|
+
# =============================================================================
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class ObjectReference:
|
|
153
|
+
"""
|
|
154
|
+
Reference to another game object.
|
|
155
|
+
|
|
156
|
+
Objects in ARK save files can reference each other. The format differs
|
|
157
|
+
between ASE and ASA:
|
|
158
|
+
|
|
159
|
+
ASE format:
|
|
160
|
+
- Int32 type (0 = index, 1 = name)
|
|
161
|
+
- If type 0: Int32 object index
|
|
162
|
+
- If type 1: ArkName
|
|
163
|
+
|
|
164
|
+
ASA format:
|
|
165
|
+
- Int16 isName flag
|
|
166
|
+
- If isName = 0: 16-byte GUID
|
|
167
|
+
- If isName = 1: ArkName
|
|
168
|
+
|
|
169
|
+
Attributes:
|
|
170
|
+
object_id: The object index (ASE) or None.
|
|
171
|
+
object_guid: The object GUID as hex string (ASA) or None.
|
|
172
|
+
object_name: The object name (when referencing by name) or None.
|
|
173
|
+
is_null: True if this is a null reference.
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
object_id: int | None = None
|
|
177
|
+
object_guid: str | None = None
|
|
178
|
+
object_name: ArkName | None = None
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def is_null(self) -> bool:
|
|
182
|
+
"""Check if this is a null/empty reference."""
|
|
183
|
+
return self.object_id is None and self.object_guid is None and self.object_name is None
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def is_id_reference(self) -> bool:
|
|
187
|
+
"""Check if this references by ID (ASE style)."""
|
|
188
|
+
return self.object_id is not None
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def is_guid_reference(self) -> bool:
|
|
192
|
+
"""Check if this references by GUID (ASA style)."""
|
|
193
|
+
return self.object_guid is not None
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_name_reference(self) -> bool:
|
|
197
|
+
"""Check if this references by name."""
|
|
198
|
+
return self.object_name is not None
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def null(cls) -> ObjectReference:
|
|
202
|
+
"""Create a null reference."""
|
|
203
|
+
return cls()
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def from_id(cls, object_id: int) -> ObjectReference:
|
|
207
|
+
"""Create a reference from an object ID (ASE)."""
|
|
208
|
+
return cls(object_id=object_id)
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def from_guid(cls, guid: str) -> ObjectReference:
|
|
212
|
+
"""Create a reference from a GUID string (ASA)."""
|
|
213
|
+
return cls(object_guid=guid)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def from_name(cls, name: ArkName) -> ObjectReference:
|
|
217
|
+
"""Create a reference from an object name."""
|
|
218
|
+
return cls(object_name=name)
|
|
219
|
+
|
|
220
|
+
def __str__(self) -> str:
|
|
221
|
+
"""String representation."""
|
|
222
|
+
if self.is_null:
|
|
223
|
+
return "ObjectReference(null)"
|
|
224
|
+
if self.object_id is not None:
|
|
225
|
+
return f"ObjectReference(id={self.object_id})"
|
|
226
|
+
if self.object_guid is not None:
|
|
227
|
+
return f"ObjectReference(guid={self.object_guid})"
|
|
228
|
+
if self.object_name is not None:
|
|
229
|
+
return f"ObjectReference(name={self.object_name})"
|
|
230
|
+
return "ObjectReference()"
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# =============================================================================
|
|
234
|
+
# Type Aliases
|
|
235
|
+
# =============================================================================
|
|
236
|
+
|
|
237
|
+
# For type hints in generic contexts
|
|
238
|
+
PropertyValue = t.Union[
|
|
239
|
+
int,
|
|
240
|
+
float,
|
|
241
|
+
bool,
|
|
242
|
+
str,
|
|
243
|
+
bytes,
|
|
244
|
+
ArkName,
|
|
245
|
+
ObjectReference,
|
|
246
|
+
list[t.Any],
|
|
247
|
+
dict[str, t.Any],
|
|
248
|
+
None,
|
|
249
|
+
]
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ARK File Format Detection.
|
|
3
|
+
|
|
4
|
+
Detects whether an ARK save file is in ASE or ASA format based on
|
|
5
|
+
header information. This allows the parser to automatically choose
|
|
6
|
+
the correct parsing strategy.
|
|
7
|
+
|
|
8
|
+
Detection Strategy for profiles/tribes/cloud data:
|
|
9
|
+
1. Read the version number from the file header (Int32)
|
|
10
|
+
2. Version 7+ is always ASA
|
|
11
|
+
3. For versions 1-6, check for a GUID at bytes 8-24:
|
|
12
|
+
- All zeros = ASE
|
|
13
|
+
- Non-zero = ASA (uses GUIDs for object identification)
|
|
14
|
+
|
|
15
|
+
World saves (.ark files) have different headers:
|
|
16
|
+
- ASE: Int16 version at offset 0 (typically 5-7)
|
|
17
|
+
- ASA: SQLite database format
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import typing as t
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ArkFileFormat(Enum):
|
|
28
|
+
"""
|
|
29
|
+
ARK save file format types.
|
|
30
|
+
|
|
31
|
+
ASE (ARK: Survival Evolved):
|
|
32
|
+
- Save format versions 5-6
|
|
33
|
+
- Uses floats for vectors
|
|
34
|
+
- Object references by index
|
|
35
|
+
|
|
36
|
+
ASA (ARK: Survival Ascended):
|
|
37
|
+
- Save format version 7+
|
|
38
|
+
- Uses doubles for vectors
|
|
39
|
+
- Object references by GUID
|
|
40
|
+
- World saves use SQLite database format
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
ASE = "ASE"
|
|
44
|
+
ASA = "ASA"
|
|
45
|
+
UNKNOWN = "UNKNOWN"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ArkFileType(Enum):
|
|
49
|
+
"""
|
|
50
|
+
ARK save file types based on extension and content.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
PROFILE = "profile" # .arkprofile
|
|
54
|
+
TRIBE = "tribe" # .arktribe
|
|
55
|
+
CLOUD_INVENTORY = "cloud_inventory" # No extension (obelisk data)
|
|
56
|
+
WORLD_SAVE = "world_save" # .ark
|
|
57
|
+
UNKNOWN = "unknown"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def detect_file_type(source: bytes | str | Path) -> ArkFileType:
|
|
61
|
+
"""
|
|
62
|
+
Detect the type of ARK save file based on extension.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
source: File path string, Path object, or bytes (for bytes, returns UNKNOWN).
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The detected file type.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
>>> detect_file_type("player.arkprofile")
|
|
72
|
+
ArkFileType.PROFILE
|
|
73
|
+
"""
|
|
74
|
+
if isinstance(source, bytes):
|
|
75
|
+
return ArkFileType.UNKNOWN
|
|
76
|
+
|
|
77
|
+
path = Path(source)
|
|
78
|
+
suffix = path.suffix.lower()
|
|
79
|
+
|
|
80
|
+
if suffix == ".arkprofile":
|
|
81
|
+
return ArkFileType.PROFILE
|
|
82
|
+
elif suffix == ".arktribe":
|
|
83
|
+
return ArkFileType.TRIBE
|
|
84
|
+
elif suffix == ".ark":
|
|
85
|
+
return ArkFileType.WORLD_SAVE
|
|
86
|
+
elif suffix == "":
|
|
87
|
+
# No extension - likely cloud inventory / obelisk data
|
|
88
|
+
return ArkFileType.CLOUD_INVENTORY
|
|
89
|
+
|
|
90
|
+
return ArkFileType.UNKNOWN
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def detect_format(source: bytes | str | Path) -> ArkFileFormat:
|
|
94
|
+
"""
|
|
95
|
+
Detect the format of an ARK save file.
|
|
96
|
+
|
|
97
|
+
Automatically determines whether a file uses ASE or ASA format
|
|
98
|
+
by examining the file header. Works for:
|
|
99
|
+
- .arkprofile (player profiles)
|
|
100
|
+
- .arktribe (tribe data)
|
|
101
|
+
- Cloud/obelisk data (no extension)
|
|
102
|
+
- .ark world saves
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
source: Raw bytes, file path string, or Path object.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
ArkFileFormat.ASE, ArkFileFormat.ASA, or ArkFileFormat.UNKNOWN.
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
>>> format = detect_format("examples/ase/map_save/1446520645.arktribe")
|
|
112
|
+
>>> print(format)
|
|
113
|
+
ArkFileFormat.ASE
|
|
114
|
+
"""
|
|
115
|
+
# Load data if given a path
|
|
116
|
+
path: Path | None = None
|
|
117
|
+
if isinstance(source, (str, Path)):
|
|
118
|
+
path = Path(source)
|
|
119
|
+
if not path.exists() or path.stat().st_size == 0:
|
|
120
|
+
return ArkFileFormat.UNKNOWN
|
|
121
|
+
data = path.read_bytes()
|
|
122
|
+
else:
|
|
123
|
+
data = source
|
|
124
|
+
|
|
125
|
+
# Need at least some bytes to detect
|
|
126
|
+
if len(data) < 24:
|
|
127
|
+
return ArkFileFormat.UNKNOWN
|
|
128
|
+
|
|
129
|
+
# Check for SQLite header (ASA world saves)
|
|
130
|
+
if data[:16] == b"SQLite format 3\x00":
|
|
131
|
+
return ArkFileFormat.ASA
|
|
132
|
+
|
|
133
|
+
# Check if this is a world save by file extension
|
|
134
|
+
is_world_save = path is not None and path.suffix.lower() == ".ark"
|
|
135
|
+
|
|
136
|
+
if is_world_save:
|
|
137
|
+
# World saves use Int16 version at offset 0
|
|
138
|
+
int16_version = int.from_bytes(data[0:2], "little", signed=True)
|
|
139
|
+
# ASE world saves have versions 5-12
|
|
140
|
+
if 5 <= int16_version <= 12:
|
|
141
|
+
return ArkFileFormat.ASE
|
|
142
|
+
return ArkFileFormat.UNKNOWN
|
|
143
|
+
|
|
144
|
+
# For profiles/tribes/cloud data, version is Int32
|
|
145
|
+
version = int.from_bytes(data[0:4], "little", signed=True)
|
|
146
|
+
|
|
147
|
+
# Version 7+ is ASA for profiles/tribes/cloud
|
|
148
|
+
if version >= 7:
|
|
149
|
+
return ArkFileFormat.ASA
|
|
150
|
+
|
|
151
|
+
# For versions 1-6, check for GUID at bytes 8-24
|
|
152
|
+
# ASE files have all zeros here; ASA files have a non-zero GUID
|
|
153
|
+
if 1 <= version <= 6:
|
|
154
|
+
guid_bytes = data[8:24]
|
|
155
|
+
has_guid = any(b != 0 for b in guid_bytes)
|
|
156
|
+
return ArkFileFormat.ASA if has_guid else ArkFileFormat.ASE
|
|
157
|
+
|
|
158
|
+
return ArkFileFormat.UNKNOWN
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def get_save_version(source: bytes | str | Path) -> int:
|
|
162
|
+
"""
|
|
163
|
+
Read the save version number from a file header.
|
|
164
|
+
|
|
165
|
+
For profiles/tribes/cloud data, this reads an Int32.
|
|
166
|
+
For world saves, this reads an Int16.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
source: Raw bytes, file path string, or Path object.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The version number, or -1 if the file is invalid.
|
|
173
|
+
"""
|
|
174
|
+
if isinstance(source, (str, Path)):
|
|
175
|
+
path = Path(source)
|
|
176
|
+
if not path.exists() or path.stat().st_size == 0:
|
|
177
|
+
return -1
|
|
178
|
+
data = path.read_bytes()
|
|
179
|
+
else:
|
|
180
|
+
data = source
|
|
181
|
+
|
|
182
|
+
if len(data) < 4:
|
|
183
|
+
return -1
|
|
184
|
+
|
|
185
|
+
# Check for SQLite (ASA world save)
|
|
186
|
+
if data[:16] == b"SQLite format 3\x00":
|
|
187
|
+
return -1 # SQLite doesn't have a simple version
|
|
188
|
+
|
|
189
|
+
# Check if it looks like a world save (Int16 version 5-9)
|
|
190
|
+
int16_version = int.from_bytes(data[0:2], "little", signed=True)
|
|
191
|
+
if 5 <= int16_version <= 9:
|
|
192
|
+
return int16_version
|
|
193
|
+
|
|
194
|
+
# Otherwise read as Int32
|
|
195
|
+
return int.from_bytes(data[0:4], "little", signed=True)
|