156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
import base64
|
|
from typing import Any, Callable
|
|
|
|
from lib.archive import FArchiveReader, FArchiveWriter
|
|
|
|
|
|
def custom_version_reader(reader: FArchiveReader):
|
|
return (reader.guid(), reader.i32())
|
|
|
|
|
|
def custom_version_writer(writer: FArchiveWriter, value: tuple[str, int]):
|
|
writer.guid(value[0])
|
|
writer.i32(value[1])
|
|
|
|
|
|
class GvasHeader:
|
|
magic: int
|
|
save_game_version: int
|
|
package_file_version_ue4: int
|
|
package_file_version_ue5: int
|
|
engine_version_major: int
|
|
engine_version_minor: int
|
|
engine_version_patch: int
|
|
engine_version_changelist: int
|
|
engine_version_branch: str
|
|
custom_version_format: int
|
|
custom_versions: list[tuple[str, int]]
|
|
save_game_class_name: str
|
|
|
|
@staticmethod
|
|
def read(reader: FArchiveReader) -> "GvasHeader":
|
|
header = GvasHeader()
|
|
# FileTypeTag
|
|
header.magic = reader.i32()
|
|
if header.magic != 0x53415647:
|
|
raise Exception("invalid magic")
|
|
# SaveGameFileVersion
|
|
header.save_game_version = reader.i32()
|
|
if header.save_game_version != 3:
|
|
raise Exception(
|
|
f"expected save game version 3, got {header.save_game_version}"
|
|
)
|
|
# PackageFileUEVersion
|
|
header.package_file_version_ue4 = reader.i32()
|
|
header.package_file_version_ue5 = reader.i32()
|
|
# SavedEngineVersion
|
|
header.engine_version_major = reader.u16()
|
|
header.engine_version_minor = reader.u16()
|
|
header.engine_version_patch = reader.u16()
|
|
header.engine_version_changelist = reader.u32()
|
|
header.engine_version_branch = reader.fstring()
|
|
# CustomVersionFormat
|
|
header.custom_version_format = reader.i32()
|
|
if header.custom_version_format != 3:
|
|
raise Exception(
|
|
f"expected custom version format 3, got {header.custom_version_format}"
|
|
)
|
|
# CustomVersions
|
|
header.custom_versions = reader.tarray(custom_version_reader)
|
|
header.save_game_class_name = reader.fstring()
|
|
return header
|
|
|
|
@staticmethod
|
|
def load(dict: dict[str, Any]) -> "GvasHeader":
|
|
header = GvasHeader()
|
|
header.magic = dict["magic"]
|
|
header.save_game_version = dict["save_game_version"]
|
|
header.package_file_version_ue4 = dict["package_file_version_ue4"]
|
|
header.package_file_version_ue5 = dict["package_file_version_ue5"]
|
|
header.engine_version_major = dict["engine_version_major"]
|
|
header.engine_version_minor = dict["engine_version_minor"]
|
|
header.engine_version_patch = dict["engine_version_patch"]
|
|
header.engine_version_changelist = dict["engine_version_changelist"]
|
|
header.engine_version_branch = dict["engine_version_branch"]
|
|
header.custom_version_format = dict["custom_version_format"]
|
|
header.custom_versions = dict["custom_versions"]
|
|
header.save_game_class_name = dict["save_game_class_name"]
|
|
return header
|
|
|
|
def dump(self) -> dict[str, Any]:
|
|
return {
|
|
"magic": self.magic,
|
|
"save_game_version": self.save_game_version,
|
|
"package_file_version_ue4": self.package_file_version_ue4,
|
|
"package_file_version_ue5": self.package_file_version_ue5,
|
|
"engine_version_major": self.engine_version_major,
|
|
"engine_version_minor": self.engine_version_minor,
|
|
"engine_version_patch": self.engine_version_patch,
|
|
"engine_version_changelist": self.engine_version_changelist,
|
|
"engine_version_branch": self.engine_version_branch,
|
|
"custom_version_format": self.custom_version_format,
|
|
"custom_versions": self.custom_versions,
|
|
"save_game_class_name": self.save_game_class_name,
|
|
}
|
|
|
|
def write(self, writer: FArchiveWriter):
|
|
writer.i32(self.magic)
|
|
writer.i32(self.save_game_version)
|
|
writer.i32(self.package_file_version_ue4)
|
|
writer.i32(self.package_file_version_ue5)
|
|
writer.u16(self.engine_version_major)
|
|
writer.u16(self.engine_version_minor)
|
|
writer.u16(self.engine_version_patch)
|
|
writer.u32(self.engine_version_changelist)
|
|
writer.fstring(self.engine_version_branch)
|
|
writer.i32(self.custom_version_format)
|
|
writer.tarray(custom_version_writer, self.custom_versions)
|
|
writer.fstring(self.save_game_class_name)
|
|
|
|
|
|
class GvasFile:
|
|
header: GvasHeader
|
|
properties: dict[str, Any]
|
|
trailer: bytes
|
|
|
|
@staticmethod
|
|
def read(
|
|
data: bytes,
|
|
type_hints: dict[str, str] = {},
|
|
custom_properties: dict[str, tuple[Callable, Callable]] = {},
|
|
) -> "GvasFile":
|
|
gvas_file = GvasFile()
|
|
reader = FArchiveReader(data, type_hints, custom_properties)
|
|
gvas_file.header = GvasHeader.read(reader)
|
|
gvas_file.properties = reader.properties_until_end()
|
|
gvas_file.trailer = reader.read_to_end()
|
|
if gvas_file.trailer != b"\x00\x00\x00\x00":
|
|
print(
|
|
f"{len(gvas_file.trailer)} bytes of trailer data, file may not have fully parsed"
|
|
)
|
|
return gvas_file
|
|
|
|
@staticmethod
|
|
def load(dict: dict[str, Any]) -> "GvasFile":
|
|
gvas_file = GvasFile()
|
|
gvas_file.header = GvasHeader.load(dict["header"])
|
|
gvas_file.properties = dict["properties"]
|
|
gvas_file.trailer = base64.b64decode(dict["trailer"])
|
|
return gvas_file
|
|
|
|
def dump(self) -> dict[str, Any]:
|
|
return {
|
|
"header": self.header.dump(),
|
|
"properties": self.properties,
|
|
"trailer": base64.b64encode(self.trailer).decode("utf-8"),
|
|
}
|
|
|
|
def write(
|
|
self, custom_properties: dict[str, tuple[Callable, Callable]] = {}
|
|
) -> bytes:
|
|
writer = FArchiveWriter(custom_properties)
|
|
self.header.write(writer)
|
|
writer.properties(self.properties)
|
|
writer.write(self.trailer)
|
|
return writer.bytes()
|