Skip to content

Instantly share code, notes, and snippets.

@apple1417
Last active April 12, 2025 00:32
Show Gist options
  • Save apple1417/17c528268397d30832b1266bb39fdc2b to your computer and use it in GitHub Desktop.
Save apple1417/17c528268397d30832b1266bb39fdc2b to your computer and use it in GitHub Desktop.
Talos 2/Talos 1 Reawaked GVAS save file dumpers
#! /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)
#! /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