Ejemplo n.º 1
0
    def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
        """Write the constructed models to the cache file and remove unused models."""
        if exc_type is not None or exc_val is not None:
            return False
        data = []
        used_mdls = set()
        for key, mdl in self._built_models.items():
            if mdl.used:
                data.append((key, mdl.name, mdl.result))
                used_mdls.add(mdl.name.casefold())

        with AtomicWriter(self.model_folder_abs / 'manifest.bin',
                          is_bytes=True) as f:
            pickle.dump(data, f, pickle.HIGHEST_PROTOCOL)

        for mdl_file in self.model_folder_abs.glob('*'):
            if mdl_file.suffix not in {'.mdl', '.phy', '.vtx', '.vvd'}:
                continue

            # Strip all suffixes.
            if mdl_file.name[:mdl_file.name.find('.')].casefold() in used_mdls:
                continue

            LOGGER.info('Culling {}...', mdl_file)
            try:
                mdl_file.unlink()
            except FileNotFoundError:
                pass
Ejemplo n.º 2
0
    def __init__(
        self,
        filename: str,
        *,
        in_conf_folder: bool = True,
        auto_load: bool = True,
    ) -> None:
        """Initialise the config file.

        `filename` is the name of the config file, in the `root` directory.
        If `auto_load` is true, this file will immediately be read and parsed.
        If in_conf_folder is set, The folder is relative to the 'config/'
        folder in the BEE2 folder.
        """
        super().__init__()

        self.has_changed = False

        if filename is not None:
            if in_conf_folder:
                self.filename = utils.conf_location('config/' + filename)
            else:
                self.filename = filename

            self.writer = AtomicWriter(self.filename)
            self.has_changed = False

            if auto_load:
                self.load()
        else:
            self.filename = self.writer = None
Ejemplo n.º 3
0
    def replace_lump(self, new_name: str, lump: Union[BSP_LUMPS, 'Lump'],
                     new_data: bytes):
        """Write out the BSP file, replacing a lump with the given bytes.

        """
        if isinstance(lump, BSP_LUMPS):
            lump = self.lumps[lump]  # type: Lump
        with open(self.filename, 'rb') as file:
            data = file.read()

        before_lump = data[self.header_off:lump.offset]
        after_lump = data[lump.offset + lump.length:]
        del data  # This contains the entire file, we don't want to keep
        # this memory around for long.

        # Adjust the length to match the new data block.
        len_change = len(new_data) - lump.length
        lump.length = len(new_data)

        # Find all lumps after this one, and adjust offsets.
        # The order of headers doesn't need to match data order!
        for other_lump in self.lumps.values():
            # Not >=, that would adjust us too!
            if other_lump.offset > lump.offset:
                other_lump.offset += len_change

        with AtomicWriter(new_name, is_bytes=True) as file:
            self.write_header(file)
            file.write(before_lump)
            file.write(new_data)
            file.write(after_lump)

            # Game lumps need their data to apply offsets.
            # We're not adding/removing headers, so we can rewrite in-place.
            game_lump = self.lumps[BSP_LUMPS.GAME_LUMP]
            if game_lump.offset > lump.offset:
                file.seek(game_lump.offset)
                file.write(struct.pack('i', len(self.game_lumps)))
                for lump_id, (
                        flags,
                        version,
                        file_off,
                        file_len,
                ) in self.game_lumps.items():
                    self.game_lumps[lump_id] = (
                        flags,
                        version,
                        file_off + len_change,
                        file_len,
                    )
                    file.write(
                        struct.pack(
                            '<4s HH ii',
                            lump_id[::-1],
                            flags,
                            version,
                            file_off + len_change,
                            file_len,
                        ))
Ejemplo n.º 4
0
def write_settings() -> None:
    """Write the settings to disk."""
    props = get_curr_settings()
    props.name = None
    with AtomicWriter(
            str(utils.conf_location('config/config.vdf')),
            is_bytes=False,
    ) as file:
        for line in props.export():
            file.write(line)
Ejemplo n.º 5
0
    def replace_lump(self, new_name, lump, new_data: bytes):
        """Write out the BSP file, replacing a lump with the given bytes.

        """
        if isinstance(lump, BSP_LUMPS):
            lump = self.lumps[lump]
        with open(self.filename, 'rb') as file:
            data = file.read()

        before_lump = data[self.header_off:lump.offset]
        after_lump = data[lump.offset + lump.length:]
        del data  # This contains the entire file, we don't want to keep
        # this memory around for long.

        # Adjust the length to match the new data block.
        lump.length = len(new_data)

        with AtomicWriter(new_name, is_bytes=True) as file:
            self.write_header(file)
            file.write(before_lump)
            file.write(new_data)
            file.write(after_lump)
Ejemplo n.º 6
0
    def save(self, filename=None) -> None:
        """Write the BSP back into the given file."""

        game_lumps = list(self.game_lumps.values())  # Lock iteration order.

        with AtomicWriter(filename or self.filename,
                          is_bytes=True) as file:  # type: BinaryIO
            # Needed to allow writing out the header before we know the position
            # data will be.
            defer = DeferredWrites(file)

            if isinstance(self.version, VERSIONS):
                version = self.version.value
            else:
                version = self.version

            file.write(struct.pack(HEADER_1, BSP_MAGIC, version))

            # Write headers.
            for lump_name in BSP_LUMPS:
                lump = self.lumps[lump_name]
                defer.defer(lump_name, '<ii')
                file.write(
                    struct.pack(
                        HEADER_LUMP,
                        0,  # offset
                        0,  # length
                        lump.version,
                        bytes(lump.ident),
                    ))

            # After lump headers, the map revision...
            file.write(struct.pack(HEADER_2, self.map_revision))

            # Then each lump.
            for lump_name in LUMP_WRITE_ORDER:
                # Write out the actual data.
                lump = self.lumps[lump_name]
                if lump_name is BSP_LUMPS.GAME_LUMP:
                    # Construct this right here.
                    lump_start = file.tell()
                    file.write(struct.pack('<i', len(game_lumps)))
                    for game_lump in game_lumps:
                        file.write(
                            struct.pack(
                                '<4s HH',
                                game_lump.id[::-1],
                                game_lump.flags,
                                game_lump.version,
                            ))
                        defer.defer(game_lump.id, '<i', write=True)
                        file.write(struct.pack('<i', len(game_lump.data)))

                    # Now write data.
                    for game_lump in game_lumps:
                        defer.set_data(game_lump.id, file.tell())
                        file.write(game_lump.data)
                    # Length of the game lump is current - start.
                    defer.set_data(
                        lump_name,
                        lump_start,
                        file.tell() - lump_start,
                    )
                else:
                    # Normal lump.
                    defer.set_data(lump_name, file.tell(), len(lump.data))
                    file.write(lump.data)
            # Apply all the deferred writes.
            defer.write()
Ejemplo n.º 7
0
def parse(path: Path) -> Tuple[
    Config,
    Game,
    FileSystemChain,
    Set[FileSystem],
    Set[Plugin],
]:
    """From some directory, locate and parse the config file.

    This then constructs and customises each object according to config
    options.

    The first srctools.vdf file found in a parent directory is parsed.
    If none can be found, it tries to find the first subfolder of 'common/' and
    writes a default copy there. FileNotFoundError is raised if none can be
    found.

    This returns:
        * The config.
        * Parsed gameinfo.
        * The chain of filesystems.
        * A packing blacklist.
        * A list of plugins.
    """
    conf = Config(OPTIONS)

    # If the path is a folder, add a dummy folder so parents yields it.
    # That way we check for a config in this folder.
    if not path.suffix:
        path /= 'unused'

    for folder in path.parents:
        conf_path = folder / CONF_NAME
        if conf_path.exists():
            LOGGER.info('Config path: "{}"', conf_path.absolute())
            with open(conf_path) as f:
                props = Property.parse(f, conf_path)
            conf.path = conf_path
            conf.load(props)
            break
    else:
        LOGGER.warning('Cannot find a valid config file!')
        # Apply all the defaults.
        conf.load(Property(None, []))

        # Try to write out a default file in the game folder.
        for folder in path.parents:
            if folder.parent.stem == 'common':
                break
        else:
            # Give up, write to working directory.
            folder = Path()
        conf.path = folder / CONF_NAME

        LOGGER.warning('Writing default to "{}"', conf.path)

    with AtomicWriter(str(conf.path)) as f:
        conf.save(f)

    game = Game((folder / conf.get(str, 'gameinfo')).resolve())

    fsys_chain = game.get_filesystem()

    blacklist = set()  # type: Set[FileSystem]

    if not conf.get(bool, 'pack_vpk'):
        for fsys, prefix in fsys_chain.systems:
            if isinstance(fsys, VPKFileSystem):
                blacklist.add(fsys)

    game_root = game.root

    for prop in conf.get(Property, 'searchpaths'):  # type: Property
        if prop.has_children():
            raise ValueError('Config "searchpaths" value cannot have children.')
        assert isinstance(prop.value, str)

        if prop.value.endswith('.vpk'):
            fsys = VPKFileSystem(str((game_root / prop.value).resolve()))
        else:
            fsys = RawFileSystem(str((game_root / prop.value).resolve()))

        if prop.name in ('prefix', 'priority'):
            fsys_chain.add_sys(fsys, priority=True)
        elif prop.name == 'nopack':
            blacklist.add(fsys)
        elif prop.name in ('path', 'pack'):
            fsys_chain.add_sys(fsys)
        else:
            raise ValueError(
                'Unknown searchpath '
                'key "{}"!'.format(prop.real_name)
            )

    plugins = set()  # type: Set[Plugin]

    # find all the plugins and make plugin objects out of them
    for prop in conf.get(Property, 'plugins'):  # type: Property
        if prop.has_children():
            raise ValueError('Config "plugins" value cannot have children.')
        assert isinstance(prop.value, str)
        
        path = (game_root / Path(prop.value)).resolve()
        if prop.name in ("path", "recursive"):
            if not path.is_dir():
                raise ValueError("'{}' is not a directory".format(path))

            # want to recursive glob if key is recursive
            pattern = "*.py" if prop.name == "path" else "**/*.py"

            #find all .py files, make Plugins
            for p in path.glob(pattern):
                plugins.add(Plugin(path / p))

        elif prop.name == "single":
            plugins.add(Plugin(path))
        else:
            raise ValueError("Unknown plugins key {}".format(prop.real_name))

    return conf, game, fsys_chain, blacklist, plugins
Ejemplo n.º 8
0
    def save(self, filename=None) -> None:
        """Write the BSP back into the given file."""
        # This gets difficult. The offsets need to be written before we know
        # what they are. So write empty bytes, record that location then go
        # back to fill them in after we actually determine where they are.
        # We use either BSP_LUMPS enums or game-lump byte IDs for dict keys.

        # Location of the header field.
        fixup_loc = {}  # type: Dict[Union[BSP_LUMPS, bytes], int]
        # The data to write.
        fixup_data = {}  # type: Dict[Union[BSP_LUMPS, bytes], bytes]

        game_lumps = list(self.game_lumps.values())  # Lock iteration order.

        with AtomicWriter(filename or self.filename,
                          is_bytes=True) as file:  # type: BinaryIO
            file.write(BSP_MAGIC)
            if isinstance(self.version, VERSIONS):
                file.write(struct.pack('<i', self.version.value))
            else:
                file.write(struct.pack('<i', self.version))

            # Write headers.
            for lump_name in BSP_LUMPS:
                lump = self.lumps[lump_name]
                fixup_loc[lump_name] = file.tell()
                file.write(
                    struct.pack(
                        '<8xi4s',
                        # offset,
                        # length,
                        lump.version,
                        bytes(lump.ident),
                    ))

            # After lump headers, the map revision...
            file.write(struct.pack('<i', self.map_revision))

            # Then each lump.
            for lump_name in LUMP_WRITE_ORDER:
                # Write out the actual data.
                lump = self.lumps[lump_name]
                if lump_name is BSP_LUMPS.GAME_LUMP:
                    # Construct this right here.
                    lump_start = file.tell()
                    file.write(struct.pack('<i', len(game_lumps)))
                    for game_lump in game_lumps:
                        file.write(
                            struct.pack(
                                '<4s HH',
                                game_lump.id[::-1],
                                game_lump.flags,
                                game_lump.version,
                            ))
                        fixup_loc[
                            game_lump.id] = file.tell()  # Offset goes here.
                        file.write(struct.pack('<4xi', len(game_lump.data)))

                    # Now write data.
                    for game_lump in game_lumps:
                        fixup_data[game_lump.id] = struct.pack(
                            '<i', file.tell())
                        file.write(game_lump.data)
                    # Length of the game lump is current - start.
                    fixup_data[lump_name] = struct.pack(
                        '<ii',
                        lump_start,
                        file.tell() - lump_start,
                    )
                else:
                    # Normal lump.
                    fixup_data[lump_name] = struct.pack(
                        '<ii',
                        file.tell(),
                        len(lump.data),
                    )
                    file.write(lump.data)

            # Now apply all the fixups we deferred.
            for fixup_key in fixup_loc:
                file.seek(fixup_loc[fixup_key])
                file.write(fixup_data[fixup_key])
Ejemplo n.º 9
0
def parse(
    path: Path
) -> Tuple[Config, Game, FileSystemChain, Set[FileSystem], PluginFinder, ]:
    """From some directory, locate and parse the config file.

    This then constructs and customises each object according to config
    options.

    The first srctools.vdf file found in a parent directory is parsed.
    If none can be found, it tries to find the first subfolder of 'common/' and
    writes a default copy there. FileNotFoundError is raised if none can be
    found.

    This returns:
        * The config.
        * Parsed gameinfo.
        * The chain of filesystems.
        * A packing blacklist.
        * The plugin loader.
    """
    conf = Config(OPTIONS)

    # If the path is a folder, add a dummy folder so parents yields it.
    # That way we check for a config in this folder.
    if not path.suffix:
        path /= 'unused'

    for folder in path.parents:
        conf_path = folder / CONF_NAME
        if conf_path.exists():
            LOGGER.info('Config path: "{}"', conf_path.absolute())
            with open(conf_path) as f:
                props = Property.parse(f, conf_path)
            conf.path = conf_path
            conf.load(props)
            break
    else:
        LOGGER.warning('Cannot find a valid config file!')
        # Apply all the defaults.
        conf.load(Property(None, []))

        # Try to write out a default file in the game folder.
        for folder in path.parents:
            if folder.parent.stem == 'common':
                break
        else:
            # Give up, write to working directory.
            folder = Path()
        conf.path = folder / CONF_NAME

        LOGGER.warning('Writing default to "{}"', conf.path)

    with AtomicWriter(str(conf.path)) as f:
        conf.save(f)

    game = Game((folder / conf.get(str, 'gameinfo')).resolve())

    fsys_chain = game.get_filesystem()

    blacklist = set()  # type: Set[FileSystem]

    if not conf.get(bool, 'pack_vpk'):
        for fsys, prefix in fsys_chain.systems:
            if isinstance(fsys, VPKFileSystem):
                blacklist.add(fsys)

    game_root = game.root

    for prop in conf.get(Property, 'searchpaths'):  # type: Property
        if prop.has_children():
            raise ValueError(
                'Config "searchpaths" value cannot have children.')
        assert isinstance(prop.value, str)

        if prop.value.endswith('.vpk'):
            fsys = VPKFileSystem(str((game_root / prop.value).resolve()))
        else:
            fsys = RawFileSystem(str((game_root / prop.value).resolve()))

        if prop.name in ('prefix', 'priority'):
            fsys_chain.add_sys(fsys, priority=True)
        elif prop.name == 'nopack':
            blacklist.add(fsys)
        elif prop.name in ('path', 'pack'):
            fsys_chain.add_sys(fsys)
        else:
            raise ValueError('Unknown searchpath '
                             'key "{}"!'.format(prop.real_name))

    sources: Dict[Path, PluginSource] = {}

    builtin_transforms = (Path(sys.argv[0]).parent / 'transforms').resolve()

    # find all the plugins and make plugin objects out of them
    for prop in conf.get(Property, 'plugins'):
        if prop.has_children():
            raise ValueError('Config "plugins" value cannot have children.')
        assert isinstance(prop.value, str)

        path = (game_root / Path(prop.value)).resolve()
        if prop.name in ('path', "recursive", 'folder'):
            if not path.is_dir():
                raise ValueError("'{}' is not a directory".format(path))

            is_recursive = prop.name == "recursive"

            try:
                source = sources[path]
            except KeyError:
                sources[path] = PluginSource(path, is_recursive)
            else:
                if is_recursive and not source.recursive:
                    # Upgrade to recursive.
                    source.recursive = True

        elif prop.name in ('single', 'file'):
            parent = path.parent
            try:
                source = sources[parent]
            except KeyError:
                source = sources[parent] = PluginSource(parent, False)
            source.autoload_files.add(path)

        elif prop.name == "_builtin_":
            # For development purposes, redirect builtin folder.
            builtin_transforms = path
        else:
            raise ValueError("Unknown plugins key {}".format(prop.real_name))

    for source in sources.values():
        LOGGER.debug('Plugin path: "{}", recursive={}, files={}',
                     source.folder, source.recursive,
                     sorted(source.autoload_files))
    LOGGER.debug('Builtin plugin path is {}', builtin_transforms)
    if builtin_transforms not in sources:
        sources[builtin_transforms] = PluginSource(builtin_transforms, True)

    plugin_finder = PluginFinder('srctools.bsp_transforms.plugin',
                                 sources.values())
    sys.meta_path.append(plugin_finder)

    return conf, game, fsys_chain, blacklist, plugin_finder
Ejemplo n.º 10
0
def save_load_compile_pane(
        props: Optional[Property] = None) -> Optional[Property]:
    """Save/load compiler options from the palette.

    Note: We specifically do not save/load the following:
        - packfile dumping
        - compile counts
    This is because these are more system-dependent than map dependent.
    """
    if props is None:  # Saving
        corr_prop = Property('corridor', [])
        props = Property('', [
            Property('sshot_type', chosen_thumb.get()),
            Property('sshot_cleanup', str(cleanup_screenshot.get())),
            Property('spawn_elev', str(start_in_elev.get())),
            Property('player_model',
                     PLAYER_MODELS_REV[player_model_var.get()]),
            Property('use_voice_priority', str(VOICE_PRIORITY_VAR.get())),
            corr_prop,
        ])
        for group, win in CORRIDOR.items():
            corr_prop[group] = win.chosen_id or '<NONE>'

        # Embed the screenshot in so we can load it later.
        if chosen_thumb.get() == 'CUST':
            # encodebytes() splits it into multiple lines, which we write
            # in individual blocks to prevent having a massively long line
            # in the file.
            with open(SCREENSHOT_LOC, 'rb') as f:
                screenshot_data = base64.encodebytes(f.read())
            props.append(
                Property('sshot_data', [
                    Property('b64', data)
                    for data in screenshot_data.decode('ascii').splitlines()
                ]))

        return props

    # else: Loading

    chosen_thumb.set(props['sshot_type', chosen_thumb.get()])
    cleanup_screenshot.set(
        props.bool('sshot_cleanup', cleanup_screenshot.get()))

    if 'sshot_data' in props:
        screenshot_parts = b'\n'.join([
            prop.value.encode('ascii')
            for prop in props.find_children('sshot_data')
        ])
        screenshot_data = base64.decodebytes(screenshot_parts)
        with AtomicWriter(SCREENSHOT_LOC, is_bytes=True) as f:
            f.write(screenshot_data)

    # Refresh these.
    set_screen_type()
    set_screenshot()

    start_in_elev.set(props.bool('spawn_elev', start_in_elev.get()))

    try:
        player_mdl = props['player_model']
    except LookupError:
        pass
    else:
        player_model_var.set(PLAYER_MODELS[player_mdl])
        COMPILE_CFG['General']['player_model'] = player_mdl

    VOICE_PRIORITY_VAR.set(
        props.bool('use_voice_priority', VOICE_PRIORITY_VAR.get()))

    corr_prop = props.find_key('corridor', [])
    for group, win in CORRIDOR.items():
        try:
            sel_id = corr_prop[group]
        except LookupError:
            "No config option, ok."
        else:
            win.sel_item_id(sel_id)
            COMPILE_CFG['Corridor'][
                group] = '0' if sel_id == '<NONE>' else sel_id

    COMPILE_CFG.save_check()
    return None