From 44886a55a552b6a6a6bbc7d26dafe85e890569d4 Mon Sep 17 00:00:00 2001 From: Jared Ketterer <36507175+snake-biscuits@users.noreply.github.com> Date: Sat, 29 Jun 2024 21:42:48 +1000 Subject: [PATCH] (extensions.archives) adding `id_software.Pak` support --- bsp_tool/extensions/archives/id_software.py | 56 ++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/bsp_tool/extensions/archives/id_software.py b/bsp_tool/extensions/archives/id_software.py index 41fa8ae1..6fae4b8b 100644 --- a/bsp_tool/extensions/archives/id_software.py +++ b/bsp_tool/extensions/archives/id_software.py @@ -1,14 +1,66 @@ +from __future__ import annotations +import io +import struct +from typing import Dict, List import zipfile from . import base +class PakFileEntry: + def __init__(self, filepath: str, offset: int, size: int): + self.filepath = filepath + self.offset = offset + self.size = size + + def __repr__(self) -> str: + return f"PakFileEntry(filepath={self.filepath}, offset={self.offset}, size={self.size})" + + @classmethod + def from_stream(cls, stream: io.BytesIO) -> PakFileEntry: + return cls(*struct.unpack("56s2I", stream.read(64))) + + class Pak(base.Archive): # https://quakewiki.org/wiki/.pak ext = "*.pak" + files: Dict[str, PakFileEntry] + + def __init__(self, filepath: str = None): + self.files = dict() + if filepath is not None: + self._from_file(filepath) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {len(self.files)} files @ 0x{id(self):016X}>" + + def __del__(self): + self.file.close() + + def __exit__(self): + self.file.close() + + def _from_file(self, filepath: str): + self.file = open(filepath, "rb") + assert self.file.read(4) == b"PACK", "not a .pak file" + # file table + offset, size = struct.unpack("2I", self.file.read(8)) + assert size % 64 == 0, "unexpected file table size" + self.file.seek(offset) + self.files = { + e.filepath.split(b"\0")[0].decode(): e + for e in [ + PakFileEntry.from_stream(self.file) + for i in range(size // 64)]} + + def read(self, filepath: str) -> bytes: + assert filepath in self.files + entry = self.files[filepath] + self.file.seek(entry.offset) + return self.file.read(entry.size) - def __init__(self): - raise NotImplementedError() + def namelist(self) -> List[str]: + return sorted(self.files.keys()) class Pk3(zipfile.ZipFile, base.Archive):