Last active
April 12, 2025 00:32
-
-
Save apple1417/17c528268397d30832b1266bb39fdc2b to your computer and use it in GitHub Desktop.
Talos 2/Talos 1 Reawaked GVAS save file dumpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
# ruff: noqa: D102, D103 | |
from __future__ import annotations | |
import struct | |
from collections.abc import Mapping, Sequence | |
from dataclasses import dataclass, field | |
from datetime import datetime, timedelta | |
from enum import Enum | |
from os import SEEK_END, SEEK_SET | |
from pathlib import Path | |
from typing import Any, BinaryIO, TypeAlias | |
from uuid import UUID | |
JSON: TypeAlias = Mapping[str, "JSON"] | Sequence["JSON"] | str | int | float | bool | None | |
@dataclass | |
class BinaryReader: | |
stream: BinaryIO | |
def read(self, num: int) -> bytes: | |
data = self.stream.read(num) | |
assert len(data) == num, "reached end of stream" | |
return data | |
def unpack(self, pattern: str) -> tuple[Any, ...]: | |
return struct.unpack(pattern, self.read(struct.calcsize(pattern))) | |
def u8(self) -> int: | |
return self.unpack("<B")[0] | |
def u16(self) -> int: | |
return self.unpack("<H")[0] | |
def u32(self) -> int: | |
return self.unpack("<I")[0] | |
def u64(self) -> int: | |
return self.unpack("<Q")[0] | |
def s8(self) -> int: | |
return self.unpack("<b")[0] | |
def s16(self) -> int: | |
return self.unpack("<h")[0] | |
def s32(self) -> int: | |
return self.unpack("<i")[0] | |
def s64(self) -> int: | |
return self.unpack("<q")[0] | |
def f32(self) -> float: | |
return self.unpack("<f")[0] | |
def f64(self) -> float: | |
return self.unpack("<d")[0] | |
def string(self) -> str: | |
size = self.u32() | |
wide = size < 0 | |
if wide: | |
size *= -2 | |
return self.read(size).decode("utf-16-le" if wide else "ascii").removesuffix("\0") | |
def guid(self) -> UUID: | |
return UUID(bytes=self.read(16)) | |
class PropertyType(Enum): | |
ArrayProperty = "ArrayProperty" | |
BoolProperty = "BoolProperty" | |
ByteProperty = "ByteProperty" | |
EnumProperty = "EnumProperty" | |
FloatProperty = "FloatProperty" | |
Int64Property = "Int64Property" | |
IntProperty = "IntProperty" | |
MapProperty = "MapProperty" | |
NameProperty = "NameProperty" | |
StrProperty = "StrProperty" | |
StructProperty = "StructProperty" | |
UInt32Property = "UInt32Property" | |
@dataclass | |
class GVASHeader: | |
save_version: int | |
package_version: int | |
engine_version: tuple[int, int, int] | |
branch: str | |
custom_version_format: int | |
custom_versions: dict[UUID, int] = field(repr=False) | |
save_class: str | |
def as_json(self) -> JSON: | |
return { | |
"_save_version": self.save_version, | |
"_package_version": self.package_version, | |
"_engine_version": ".".join(str(x) for x in self.engine_version), | |
"_branch": self.branch, | |
"_custom_version_format": self.custom_version_format, | |
"_custom_versions": {str(k): v for k, v in self.custom_versions.items()}, | |
"_save_class": self.save_class, | |
} | |
@dataclass | |
class PropertyHeader: | |
name: str | |
prop_type: PropertyType | |
@dataclass | |
class ArrayHeader: | |
value_type: PropertyType | |
num_entries: int | |
struct_name: str | None | |
@dataclass | |
class MapHeader: | |
key_type: PropertyType | |
value_type: PropertyType | |
num_entries: int | |
struct_name: str | None | |
@dataclass | |
class Property: | |
name: str | |
value: Any | |
def read_gvas_header(reader: BinaryReader) -> GVASHeader: | |
assert reader.read(4) == b"GVAS" | |
save_version = reader.u32() | |
package_version = reader.u32() | |
_ = reader.u32() | |
major, minor, patch = reader.unpack("<3H") | |
_ = reader.read(4) | |
branch = reader.string() | |
custom_version_format = reader.u32() | |
custom_versions: dict[UUID, int] = {} | |
for _ in range(reader.u32()): | |
key = reader.guid() | |
version = reader.u32() | |
custom_versions[key] = version | |
save_class = reader.string() | |
return GVASHeader( | |
save_version, | |
package_version, | |
(major, minor, patch), | |
branch, | |
custom_version_format, | |
custom_versions, | |
save_class, | |
) | |
PROPS_USING_OPTIONAL_GUID: tuple[PropertyType, ...] = ( | |
PropertyType.ByteProperty, | |
PropertyType.FloatProperty, | |
PropertyType.Int64Property, | |
PropertyType.IntProperty, | |
PropertyType.NameProperty, | |
PropertyType.StrProperty, | |
PropertyType.UInt32Property, | |
) | |
PROPS_WITHOUT_EXTRA_U32: tuple[PropertyType, ...] = ( | |
PropertyType.ArrayProperty, | |
PropertyType.EnumProperty, | |
PropertyType.MapProperty, | |
PropertyType.StructProperty, | |
) | |
def read_property_header(reader: BinaryReader) -> PropertyHeader | None: | |
name = reader.string() | |
if name == "None": | |
return None | |
prop_type = PropertyType(reader.string()) | |
if prop_type not in PROPS_WITHOUT_EXTRA_U32: | |
_ = reader.u32() | |
# The file format is dumb and so we can't rely on this size, it's only a KVL sometimes | |
_size = reader.u32() | |
if prop_type in PROPS_USING_OPTIONAL_GUID: | |
has_guid = reader.u8() != 0 | |
if has_guid: | |
_ = reader.guid() | |
return PropertyHeader(name, prop_type) | |
def read_array_header(reader: BinaryReader) -> ArrayHeader: | |
value_type = PropertyType(reader.string()) | |
struct_name = None | |
match value_type: | |
case PropertyType.StructProperty: | |
_ = reader.u32() | |
struct_name = read_struct_header(reader) | |
case ( | |
PropertyType.NameProperty | |
| PropertyType.StrProperty | |
| PropertyType.IntProperty | |
| PropertyType.ByteProperty | |
): | |
_ = reader.read(9) | |
case _: | |
raise ValueError("Unknown property type") | |
num_entries = reader.u32() | |
return ArrayHeader(value_type, num_entries, struct_name) | |
def read_array(reader: BinaryReader, header: ArrayHeader) -> list[Any]: | |
match header.value_type: | |
case PropertyType.StructProperty: | |
assert header.struct_name is not None | |
return [read_struct(reader, header.struct_name) for _ in range(header.num_entries)] | |
case ( | |
PropertyType.NameProperty | |
| PropertyType.StrProperty | |
| PropertyType.IntProperty | |
| PropertyType.ByteProperty | |
): | |
return [read_value(reader, header.value_type) for _ in range(header.num_entries)] | |
case _: | |
raise ValueError("Unknown property type") | |
def read_map_header(reader: BinaryReader) -> MapHeader: | |
key_type = PropertyType(reader.string()) | |
_ = reader.u32() | |
value_type = PropertyType(reader.string()) | |
struct_name = None | |
match value_type: | |
case PropertyType.StructProperty: | |
_ = reader.u32() | |
struct_name = read_struct_header(reader) | |
_ = reader.u32() | |
case PropertyType.BoolProperty: | |
reader.read(13) | |
case _: | |
raise ValueError("Unknown property type") | |
num = reader.u32() | |
return MapHeader(key_type, value_type, num, struct_name) | |
def read_map(reader: BinaryReader, header: MapHeader) -> dict[Any, Any]: | |
entries: dict[Any, Any] = {} | |
for _ in range(header.num_entries): | |
key = read_value(reader, header.key_type) | |
match header.value_type: | |
case PropertyType.StructProperty: | |
assert header.struct_name is not None | |
value = read_struct(reader, header.struct_name) | |
case PropertyType.BoolProperty: | |
value = read_value(reader, header.value_type) | |
case _: | |
raise ValueError("Unknown property type") | |
assert key not in entries | |
entries[key] = value | |
return entries | |
def read_struct_header(reader: BinaryReader) -> str: | |
# Hacky hack | |
reader.stream.seek(reader.stream.tell() - 4) | |
_magic = reader.u32() | |
struct_name = reader.string() | |
_ = reader.u32() | |
_package = reader.string() | |
if _magic == 2: | |
_ = reader.u32() | |
_uuid = reader.string() | |
_ = reader.read(9) | |
return struct_name | |
def read_struct(reader: BinaryReader, struct_name: str | None = None) -> Any: | |
match struct_name: | |
case "DateTime": | |
ticks = reader.u64() | |
return datetime(1, 1, 1, 0, 0, 0) + timedelta(microseconds=ticks / 10) | |
case "Vector": | |
return reader.unpack("<3d") | |
case "Quat": | |
return reader.unpack("<4d") | |
case _: | |
fields: Any = {} | |
while (prop := read_full_property(reader)) is not None: | |
assert prop.name not in fields | |
fields[prop.name] = prop.value | |
return fields | |
def read_enum(reader: BinaryReader) -> str: | |
_enum_cls = reader.string() | |
_ = reader.u32() | |
_package = reader.string() | |
_ = reader.u32() | |
_inner_prop_type = PropertyType(reader.string()) | |
_ = reader.u64() | |
_as_int = read_value(reader, _inner_prop_type) | |
return reader.string() | |
def read_value(reader: BinaryReader, cls: PropertyType) -> Any: # noqa: C901 | |
match cls: | |
case PropertyType.ArrayProperty: | |
return read_array(reader, read_array_header(reader)) | |
case PropertyType.BoolProperty: | |
return reader.u8() != 0 | |
case PropertyType.ByteProperty: | |
return reader.u8() | |
case PropertyType.EnumProperty: | |
return read_enum(reader) | |
case PropertyType.FloatProperty: | |
return reader.f32() | |
case PropertyType.IntProperty: | |
return reader.s32() | |
case PropertyType.Int64Property: | |
return reader.s64() | |
case PropertyType.MapProperty: | |
return read_map(reader, read_map_header(reader)) | |
case PropertyType.NameProperty | PropertyType.StrProperty: | |
return reader.string() | |
case PropertyType.StructProperty: | |
return read_struct(reader, read_struct_header(reader)) | |
case PropertyType.UInt32Property: | |
return reader.u32() | |
case _: # pyright: ignore[reportUnnecessaryComparison] | |
raise KeyError("Unknown property type", cls) | |
def read_full_property(reader: BinaryReader) -> Property | None: | |
header = read_property_header(reader) | |
if header is None: | |
return None | |
return Property(header.name, read_value(reader, header.prop_type)) | |
if __name__ == "__main__": | |
import argparse | |
import json | |
parser = argparse.ArgumentParser( | |
description="Dumps the contents of a Talos Principle 1 Reloaded GVAS save file.", | |
) | |
parser.add_argument("file", type=Path, help="The file to dump.") | |
parser.add_argument( | |
"output", | |
type=Path, | |
nargs="?", | |
help="The file to output the contents to.", | |
) | |
parser.add_argument( | |
"--no-sort", | |
action="store_true", | |
help="If given, doesn't sort json keys, leaving them in the order encountered.", | |
) | |
args = parser.parse_args() | |
with args.file.open("rb") as file: | |
file.seek(0, SEEK_END) | |
size = file.tell() | |
file.seek(0, SEEK_SET) | |
reader = BinaryReader(file) | |
data: list[JSON] = [] | |
data.append(read_gvas_header(reader).as_json()) | |
while file.tell() < size: | |
_ = reader.u8() | |
data.append(read_struct(reader)) | |
_ = reader.read(4) | |
output_path = args.output or args.file.with_suffix(".json") | |
with output_path.open("w") as out_file: | |
json.dump(data, out_file, indent=4, default=str, sort_keys=not args.no_sort) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python3 | |
# ruff: noqa: D102, D103 | |
from __future__ import annotations | |
import struct | |
from collections.abc import Mapping, Sequence | |
from dataclasses import dataclass, field | |
from datetime import datetime, timedelta | |
from enum import Enum | |
from os import SEEK_END, SEEK_SET | |
from pathlib import Path | |
from typing import Any, BinaryIO, TypeAlias | |
from uuid import UUID | |
JSON: TypeAlias = Mapping[str, "JSON"] | Sequence["JSON"] | str | int | float | bool | None | |
@dataclass | |
class BinaryReader: | |
stream: BinaryIO | |
def read(self, num: int) -> bytes: | |
data = self.stream.read(num) | |
assert len(data) == num, "reached end of stream" | |
return data | |
def unpack(self, pattern: str) -> tuple[Any, ...]: | |
return struct.unpack(pattern, self.read(struct.calcsize(pattern))) | |
def u8(self) -> int: | |
return self.unpack("<B")[0] | |
def u16(self) -> int: | |
return self.unpack("<H")[0] | |
def u32(self) -> int: | |
return self.unpack("<I")[0] | |
def u64(self) -> int: | |
return self.unpack("<Q")[0] | |
def s8(self) -> int: | |
return self.unpack("<b")[0] | |
def s16(self) -> int: | |
return self.unpack("<h")[0] | |
def s32(self) -> int: | |
return self.unpack("<i")[0] | |
def s64(self) -> int: | |
return self.unpack("<q")[0] | |
def f32(self) -> float: | |
return self.unpack("<f")[0] | |
def f64(self) -> float: | |
return self.unpack("<d")[0] | |
def string(self) -> str: | |
size = self.u32() | |
wide = size < 0 | |
if wide: | |
size *= -2 | |
return self.read(size).decode("utf-16-le" if wide else "ascii").removesuffix("\0") | |
def guid(self) -> UUID: | |
return UUID(bytes=self.read(16)) | |
class PropertyType(Enum): | |
ArrayProperty = "ArrayProperty" | |
BoolProperty = "BoolProperty" | |
ByteProperty = "ByteProperty" | |
EnumProperty = "EnumProperty" | |
FloatProperty = "FloatProperty" | |
Int64Property = "Int64Property" | |
IntProperty = "IntProperty" | |
MapProperty = "MapProperty" | |
NameProperty = "NameProperty" | |
StrProperty = "StrProperty" | |
StructProperty = "StructProperty" | |
UInt32Property = "UInt32Property" | |
@dataclass | |
class GVASHeader: | |
save_version: int | |
package_version: int | |
engine_version: tuple[int, int, int] | |
branch: str | |
custom_version_format: int | |
custom_versions: dict[UUID, int] = field(repr=False) | |
save_class: str | |
def as_json(self) -> JSON: | |
return { | |
"_save_version": self.save_version, | |
"_package_version": self.package_version, | |
"_engine_version": ".".join(str(x) for x in self.engine_version), | |
"_branch": self.branch, | |
"_custom_version_format": self.custom_version_format, | |
"_custom_versions": {str(k): v for k, v in self.custom_versions.items()}, | |
"_save_class": self.save_class, | |
} | |
@dataclass | |
class PropertyHeader: | |
name: str | |
prop_type: PropertyType | |
@dataclass | |
class ArrayHeader: | |
value_type: PropertyType | |
num_entries: int | |
@dataclass | |
class MapHeader: | |
key_type: PropertyType | |
value_type: PropertyType | |
num_entries: int | |
@dataclass | |
class Property: | |
name: str | |
value: Any | |
def read_gvas_header(reader: BinaryReader) -> GVASHeader: | |
assert reader.read(4) == b"GVAS" | |
save_version = reader.u32() | |
package_version = reader.u32() | |
_ = reader.u32() | |
major, minor, patch = reader.unpack("<3H") | |
_ = reader.read(4) | |
branch = reader.string() | |
custom_version_format = reader.u32() | |
custom_versions: dict[UUID, int] = {} | |
for _ in range(reader.u32()): | |
key = reader.guid() | |
version = reader.u32() | |
custom_versions[key] = version | |
save_class = reader.string() | |
return GVASHeader( | |
save_version, | |
package_version, | |
(major, minor, patch), | |
branch, | |
custom_version_format, | |
custom_versions, | |
save_class, | |
) | |
PROPS_USING_OPTIONAL_GUID: tuple[PropertyType, ...] = ( | |
PropertyType.FloatProperty, | |
PropertyType.IntProperty, | |
PropertyType.Int64Property, | |
PropertyType.NameProperty, | |
PropertyType.StrProperty, | |
PropertyType.UInt32Property, | |
) | |
def read_property_header(reader: BinaryReader) -> PropertyHeader | None: | |
name = reader.string() | |
if name == "None": | |
return None | |
prop_type = PropertyType(reader.string()) | |
# The file format is dumb and so we can't rely on this size, it's only a KVL sometimes | |
_size = reader.u64() | |
if prop_type in PROPS_USING_OPTIONAL_GUID: | |
has_guid = reader.u8() != 0 | |
if has_guid: | |
_ = reader.guid() | |
return PropertyHeader(name, prop_type) | |
def read_array_header(reader: BinaryReader) -> ArrayHeader: | |
value_type = PropertyType(reader.string()) | |
_ = reader.u8() | |
num_entries = reader.u32() | |
return ArrayHeader(value_type, num_entries) | |
def read_map_header(reader: BinaryReader) -> MapHeader: | |
key_type = PropertyType(reader.string()) | |
value_type = PropertyType(reader.string()) | |
_ = reader.read(5) | |
num = reader.u32() | |
return MapHeader(key_type, value_type, num) | |
def read_map(reader: BinaryReader, header: MapHeader) -> dict[Any, Any]: | |
entries: dict[Any, Any] = {} | |
for _ in range(header.num_entries): | |
key = read_value(reader, header.key_type) | |
if header.value_type == PropertyType.StructProperty: | |
value = read_struct(reader, None) | |
else: | |
value = read_value(reader, header.value_type) | |
assert key not in entries | |
entries[key] = value | |
return entries | |
def read_struct_header(reader: BinaryReader) -> str: | |
struct_name = reader.string() | |
_ = reader.read(0x11) | |
return struct_name | |
def read_struct(reader: BinaryReader, struct_name: str | None = None) -> Any: | |
match struct_name: | |
case "DateTime": | |
ticks = reader.u64() | |
return datetime(1, 1, 1, 0, 0, 0) + timedelta(microseconds=ticks / 10) | |
case "Vector": | |
return reader.unpack("<3d") | |
case "Quat": | |
return reader.unpack("<4d") | |
case _: | |
fields: Any = {} | |
while (prop := read_full_property(reader)) is not None: | |
assert prop.name not in fields | |
fields[prop.name] = prop.value | |
return fields | |
def read_value(reader: BinaryReader, cls: PropertyType) -> Any: # noqa: C901 | |
match cls: | |
case PropertyType.ArrayProperty: | |
array_header = read_array_header(reader) | |
if array_header.value_type == PropertyType.StructProperty: | |
prop_header = read_property_header(reader) | |
assert ( | |
prop_header is not None and prop_header.prop_type == PropertyType.StructProperty | |
) | |
inner_struct_name = read_struct_header(reader) | |
return [ | |
read_struct(reader, inner_struct_name) for _ in range(array_header.num_entries) | |
] | |
return [ | |
read_value(reader, array_header.value_type) for _ in range(array_header.num_entries) | |
] | |
case PropertyType.BoolProperty: | |
val = reader.u8() != 0 | |
_ = reader.u8() | |
return val | |
case PropertyType.ByteProperty | PropertyType.EnumProperty: | |
_enum_cls = reader.string() | |
_ = reader.u8() | |
return reader.u8() if cls == PropertyType.ByteProperty else reader.string() | |
case PropertyType.FloatProperty: | |
return reader.f32() | |
case PropertyType.IntProperty: | |
return reader.s32() | |
case PropertyType.Int64Property: | |
return reader.s64() | |
case PropertyType.MapProperty: | |
return read_map(reader, read_map_header(reader)) | |
case PropertyType.NameProperty | PropertyType.StrProperty: | |
return reader.string() | |
case PropertyType.StructProperty: | |
return read_struct(reader, read_struct_header(reader)) | |
case PropertyType.UInt32Property: | |
return reader.u32() | |
case _: # pyright: ignore[reportUnnecessaryComparison] | |
raise KeyError("Unknown property type", cls) | |
def read_full_property(reader: BinaryReader) -> Property | None: | |
header = read_property_header(reader) | |
if header is None: | |
return None | |
return Property(header.name, read_value(reader, header.prop_type)) | |
if __name__ == "__main__": | |
import argparse | |
import json | |
parser = argparse.ArgumentParser( | |
description="Dumps the contents of a Talos Principle 2 GVAS save file.", | |
) | |
parser.add_argument("file", type=Path, help="The file to dump.") | |
parser.add_argument( | |
"output", | |
type=Path, | |
nargs="?", | |
help="The file to output the contents to.", | |
) | |
args = parser.parse_args() | |
with args.file.open("rb") as file: | |
file.seek(0, SEEK_END) | |
size = file.tell() | |
file.seek(0, SEEK_SET) | |
reader = BinaryReader(file) | |
data: list[JSON] = [] | |
data.append(read_gvas_header(reader).as_json()) | |
while file.tell() < size: | |
data.append(read_struct(reader)) | |
_ = reader.u32() | |
output_path = args.output or args.file.with_suffix(".json") | |
with output_path.open("w") as out_file: | |
json.dump(data, out_file, indent=4, default=str, sort_keys=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment