Beispiel #1
0
def dump_files(bsp: BSP):
    """Dump packed files to a location.
    """
    dump_folder = CONF['packfile_dump', '']
    if not dump_folder:
        return

    dump_folder = os.path.abspath(dump_folder)
    
    LOGGER.info('Dumping packed files to "{}"...', dump_folder)

    # Delete files in the folder, but don't delete the folder itself.
    try:
        files = os.listdir(dump_folder)
    except FileNotFoundError:
        return

    for name in files:
        name = os.path.join(dump_folder, name)
        if os.path.isdir(name):
            try:
                shutil.rmtree(name)
            except OSError:
                # It's possible to fail here, if the window is open elsewhere.
                # If so, just skip removal and fill the folder.
                pass
        else:
            os.remove(name)

    with bsp.packfile() as zipfile:
        for zipinfo in zipfile.infolist():
            zipfile.extract(zipinfo, dump_folder)
Beispiel #2
0
def main(argv):
    LOGGER.info('Srctools VRAD hook started!')

    game_info = find_gameinfo(argv)

    fsys = game_info.get_filesystem()
    fsys.open_ref()

    packlist = PackList(fsys)

    LOGGER.info('Gameinfo: {}\nSearch path: \n{}', game_info.path,
                '\n'.join([sys[0].path for sys in fsys.systems]))

    fgd = load_fgd()

    LOGGER.info('Loading soundscripts...')
    packlist.load_soundscript_manifest('srctools_sndscript_data.vdf')
    LOGGER.info('Done! ({} sounds)', len(packlist.soundscripts))

    # The path is the last argument to VRAD
    # Hammer adds wrong slashes sometimes, so fix that.
    path = os.path.normpath(argv[-1])

    LOGGER.info("Map path is " + path)
    if path == "":
        raise Exception("No map passed!")

    if not path.endswith(".bsp"):
        path += ".bsp"

    LOGGER.info('Reading BSP...')
    bsp_file = BSP(path)
    bsp_file.read_header()
    bsp_file.read_game_lumps()

    LOGGER.info('Reading entities...')
    vmf = bsp_file.read_ent_data()
    LOGGER.info('Done!')

    run_transformations(vmf, fsys, packlist)

    bsp_file.replace_lump(
        bsp_file.filename,
        BSP_LUMPS.ENTITIES,
        bsp_file.write_ent_data(vmf),
    )

    LOGGER.info('Finished writing entities.')

    packlist.pack_fgd(vmf, fgd)

    packlist.pack_from_bsp(bsp_file)
    packlist.eval_dependencies()

    with bsp_file.packfile() as pak_zip:
        packlist.pack_into_zip(pak_zip)

    LOGGER.info("srctools VRAD hook finished!")
Beispiel #3
0
def main(argv):
    LOGGER.info('Srctools VRAD hook started!')

    game_info = find_gameinfo(argv)

    fsys = game_info.get_filesystem()
    fsys.open_ref()

    packlist = PackList(fsys)

    LOGGER.info('Gameinfo: {}\nSearch path: \n{}', game_info.path, '\n'.join([sys[0].path for sys in fsys.systems]))

    fgd = load_fgd()

    LOGGER.info('Loading soundscripts...')
    packlist.load_soundscript_manifest('srctools_sndscript_data.vdf')
    LOGGER.info('Done! ({} sounds)', len(packlist.soundscripts))

    # The path is the last argument to VRAD
    # Hammer adds wrong slashes sometimes, so fix that.
    path = os.path.normpath(argv[-1])

    LOGGER.info("Map path is " + path)
    if path == "":
        raise Exception("No map passed!")

    if not path.endswith(".bsp"):
        path += ".bsp"

    LOGGER.info('Reading BSP...')
    bsp_file = BSP(path)
    bsp_file.read_header()
    bsp_file.read_game_lumps()

    LOGGER.info('Reading entities...')
    vmf = bsp_file.read_ent_data()
    LOGGER.info('Done!')

    run_transformations(vmf, fsys, packlist)

    bsp_file.replace_lump(
        bsp_file.filename,
        BSP_LUMPS.ENTITIES,
        bsp_file.write_ent_data(vmf),
    )

    LOGGER.info('Finished writing entities.')

    packlist.pack_fgd(vmf, fgd)

    packlist.pack_from_bsp(bsp_file)
    packlist.eval_dependencies()

    with bsp_file.packfile() as pak_zip:
        packlist.pack_into_zip(pak_zip)

    LOGGER.info("srctools VRAD hook finished!")
Beispiel #4
0
def main(argv: List[str]) -> None:

    parser = argparse.ArgumentParser(
        description="Modifies the BSP file, allowing additional entities "
        "and bugfixes.", )

    parser.add_argument("--nopack",
                        dest="allow_pack",
                        action="store_false",
                        help="Prevent packing of files found in the map.")
    parser.add_argument(
        "--propcombine",
        action="store_true",
        help="Allow merging static props together.",
    )
    parser.add_argument(
        "--showgroups",
        action="store_true",
        help="Show propcombined props, by setting their tint to random groups",
    )
    parser.add_argument(
        "--dumpgroups",
        action="store_true",
        help="Write all props without propcombine groups to a new VMF.",
    )

    parser.add_argument(
        "map",
        help="The path to the BSP file.",
    )

    args = parser.parse_args(argv)

    # The path is the last argument to the compiler.
    # Hammer adds wrong slashes sometimes, so fix that.
    # Also if it's the VMF file, make it the BSP.
    path = Path(args.map).with_suffix('.bsp')

    # Open and start writing to the map's log file.
    handler = FileHandler(path.with_suffix('.log'))
    handler.setFormatter(
        Formatter(
            # One letter for level name
            '[{levelname}] {module}.{funcName}(): {message}',
            style='{',
        ))
    LOGGER.addHandler(handler)

    LOGGER.info('Srctools postcompiler hook started at {}!',
                datetime.datetime.now().isoformat())
    LOGGER.info("Map path is {}", path)

    conf, game_info, fsys, pack_blacklist, plugin = config.parse(path)

    fsys.open_ref()

    packlist = PackList(fsys)

    LOGGER.info('Gameinfo: {}', game_info.path)
    LOGGER.info(
        'Search paths: \n{}',
        '\n'.join([sys.path for sys, prefix in fsys.systems]),
    )

    fgd = FGD.engine_dbase()

    LOGGER.info('Loading soundscripts...')
    packlist.load_soundscript_manifest(
        conf.path.with_name('srctools_sndscript_data.vdf'))
    LOGGER.info('Done! ({} sounds)', len(packlist.soundscripts))

    LOGGER.info('Reading BSP...')
    bsp_file = BSP(path)

    LOGGER.info('Reading entities...')
    vmf = bsp_file.read_ent_data()
    LOGGER.info('Done!')

    # Mount the existing packfile, so the cubemap files are recognised.
    LOGGER.info('Mounting BSP packfile...')
    zipfile = ZipFile(BytesIO(bsp_file.get_lump(BSP_LUMPS.PAKFILE)))
    fsys.add_sys(ZipFileSystem('<BSP pakfile>', zipfile))

    studiomdl_path = conf.get(str, 'studiomdl')
    if studiomdl_path:
        studiomdl_loc = (game_info.root / studiomdl_path).resolve()
        if not studiomdl_loc.exists():
            LOGGER.warning('No studiomdl found at "{}"!', studiomdl_loc)
            studiomdl_loc = None
    else:
        LOGGER.warning('No studiomdl path provided.')
        studiomdl_loc = None

    LOGGER.info('Loading plugins...')
    plugin.load_all()

    use_comma_sep = conf.get(bool, 'use_comma_sep')
    if use_comma_sep is None:
        # Guess the format, by picking whatever the first output uses.
        for ent in vmf.entities:
            for out in ent.outputs:
                use_comma_sep = out.comma_sep
                break
        if use_comma_sep is None:
            LOGGER.warning(
                'No outputs in map, could not determine BSP I/O format!')
            LOGGER.warning('Set "use_comma_sep" in srctools.vdf.')
        use_comma_sep = False

    LOGGER.info('Running transforms...')
    run_transformations(vmf, fsys, packlist, bsp_file, game_info,
                        studiomdl_loc)

    if studiomdl_loc is not None and args.propcombine:
        decomp_cache_loc = conf.get(str, 'propcombine_cache')
        if decomp_cache_loc is not None:
            decomp_cache_loc = (game_info.root / decomp_cache_loc).resolve()
            decomp_cache_loc.mkdir(parents=True, exist_ok=True)
        if conf.get(bool, 'propcombine_crowbar'):
            # argv[0] is the location of our script/exe, which lets us locate
            # Crowbar from there.
            crowbar_loc = Path(sys.argv[0], '../Crowbar.exe').resolve()
        else:
            crowbar_loc = None

        LOGGER.info('Combining props...')
        propcombine.combine(
            bsp_file,
            vmf,
            packlist,
            game_info,
            studiomdl_loc=studiomdl_loc,
            qc_folders=[
                game_info.root / folder for folder in conf.get(
                    Property, 'propcombine_qc_folder').as_array(conv=Path)
            ],
            decomp_cache_loc=decomp_cache_loc,
            crowbar_loc=crowbar_loc,
            auto_range=conf.get(int, 'propcombine_auto_range'),
            min_cluster=conf.get(int, 'propcombine_min_cluster'),
            debug_tint=args.showgroups,
            debug_dump=args.dumpgroups,
        )
        LOGGER.info('Done!')
    else:  # Strip these if they're present.
        for ent in vmf.by_class['comp_propcombine_set']:
            ent.remove()

    bsp_file.lumps[BSP_LUMPS.ENTITIES].data = bsp_file.write_ent_data(
        vmf, use_comma_sep)

    if conf.get(bool, 'auto_pack') and args.allow_pack:
        LOGGER.info('Analysing packable resources...')
        packlist.pack_fgd(vmf, fgd)

        packlist.pack_from_bsp(bsp_file)

        packlist.eval_dependencies()
        if conf.get(bool, 'soundscript_manifest'):
            packlist.write_manifest()

    packlist.pack_into_zip(bsp_file,
                           blacklist=pack_blacklist,
                           ignore_vpk=False)

    with bsp_file.packfile() as pak_zip:
        # List out all the files, but group together files with the same extension.
        ext_for_name: Dict[str, List[str]] = defaultdict(list)
        for file in pak_zip.infolist():
            filename = Path(file.filename)
            if '.' in filename.name:
                stem, ext = filename.name.split('.', 1)
                file_path = str(filename.parent / stem)
            else:
                file_path = file.filename
                ext = ''

            ext_for_name[file_path].append(ext)

        LOGGER.info('Packed files: \n{}'.format('\n'.join([
            (f'{name}.{exts[0]}'
             if len(exts) == 1 else f'{name}.({"/".join(exts)})')
            for name, exts in sorted(ext_for_name.items())
        ])))

    LOGGER.info('Writing BSP...')
    bsp_file.save()

    LOGGER.info("srctools VRAD hook finished!")
Beispiel #5
0
def main(argv: List[str]) -> None:
    LOGGER.info('BEE2 VRAD hook started!')
        
    args = " ".join(argv)
    fast_args = argv[1:]
    full_args = argv[1:]
    
    if not fast_args:
        # No arguments!
        LOGGER.info(
            'No arguments!\n'
            "The BEE2 VRAD takes all the regular VRAD's "
            'arguments, with some extra arguments:\n'
            '-force_peti: Force enabling map conversion. \n'
            "-force_hammer: Don't convert the map at all.\n"
            "If not specified, the map name must be \"preview.bsp\" to be "
            "treated as PeTI."
        )
        sys.exit()

    # The path is the last argument to vrad
    # P2 adds wrong slashes sometimes, so fix that.
    fast_args[-1] = path = os.path.normpath(argv[-1])  # type: str

    LOGGER.info("Map path is " + path)

    load_config()

    for a in fast_args[:]:
        folded_a = a.casefold()
        if folded_a.casefold() in (
                "-final",
                "-staticproplighting",
                "-staticproppolys",
                "-textureshadows",
                ):
            # remove final parameters from the modified arguments
            fast_args.remove(a)
        elif folded_a == '-both':
            # LDR Portal 2 isn't actually usable, so there's not much
            # point compiling for it.
            pos = fast_args.index(a)
            fast_args[pos] = full_args[pos] = '-hdr'
        elif a in ('-force_peti', '-force_hammer', '-no_pack'):
            # we need to strip these out, otherwise VRAD will get confused
            fast_args.remove(a)
            full_args.remove(a)

    fast_args = ['-bounce', '2', '-noextra'] + fast_args

    # Fast args: -bounce 2 -noextra -game $gamedir $path\$file
    # Final args: -both -final -staticproplighting -StaticPropPolys
    # -textureshadows  -game $gamedir $path\$file

    if not path.endswith(".bsp"):
        path += ".bsp"

    if not os.path.exists(path):
        raise ValueError('"{}" does not exist!'.format(path))
    if not os.path.isfile(path):
        raise ValueError('"{}" is not a file!'.format(path))

    # If VBSP thinks it's hammer, trust it.
    if CONF.bool('is_hammer', False):
        is_peti = edit_args = False
    else:
        is_peti = True
        # Detect preview via knowing the bsp name. If we are in preview,
        # check the config file to see what was specified there.
        if os.path.basename(path) == "preview.bsp":
            edit_args = not CONF.bool('force_full', False)
        else:
            # publishing - always force full lighting.
            edit_args = False

    if '-force_peti' in args or '-force_hammer' in args:
        # we have override commands!
        if '-force_peti' in args:
            LOGGER.warning('OVERRIDE: Applying cheap lighting!')
            is_peti = edit_args = True
        else:
            LOGGER.warning('OVERRIDE: Preserving args!')
            is_peti = edit_args = False

    LOGGER.info('Final status: is_peti={}, edit_args={}', is_peti, edit_args)

    # Grab the currently mounted filesystems in P2.
    game = find_gameinfo(argv)
    root_folder = game.path.parent
    fsys = game.get_filesystem()

    # Put the Mel and Tag filesystems in so we can pack from there.
    fsys_tag = fsys_mel = None
    if is_peti and 'mel_vpk' in CONF:
        fsys_mel = VPKFileSystem(CONF['mel_vpk'])
        fsys.add_sys(fsys_mel)
    if is_peti and 'tag_dir' in CONF:
        fsys_tag = RawFileSystem(CONF['tag_dir'])
        fsys.add_sys(fsys_tag)

    # Special case - move the BEE2 fsys FIRST, so we always pack files found
    # there.
    for child_sys in fsys.systems[:]:
        if 'bee2' in child_sys[0].path.casefold():
            fsys.systems.remove(child_sys)
            fsys.systems.insert(0, child_sys)

    LOGGER.info('Reading BSP')
    bsp_file = BSP(path)

    bsp_ents = bsp_file.read_ent_data()

    zip_data = BytesIO()
    zip_data.write(bsp_file.get_lump(BSP_LUMPS.PAKFILE))
    zipfile = ZipFile(zip_data, mode='a')

    # Mount the existing packfile, so the cubemap files are recognised.
    fsys.systems.append((ZipFileSystem('', zipfile), ''))

    fsys.open_ref()

    LOGGER.info('Done!')

    LOGGER.debug('Filesystems:')
    for child_sys in fsys.systems[:]:
        LOGGER.debug('- {}: {!r}', child_sys[1], child_sys[0])

    LOGGER.info('Reading our FGD files...')
    fgd = load_fgd()

    packlist = PackList(fsys)
    packlist.load_soundscript_manifest(
        str(root_folder / 'bin/bee2/sndscript_cache.vdf')
    )

    # We nee to add all soundscripts in scripts/bee2_snd/
    # This way we can pack those, if required.
    for soundscript in fsys.walk_folder('scripts/bee2_snd/'):
        if soundscript.path.endswith('.txt'):
            packlist.load_soundscript(soundscript, always_include=False)

    if is_peti:
        LOGGER.info('Adding special packed files:')
        music_data = CONF.find_key('MusicScript', [])
        if music_data:
            packlist.pack_file(
                'scripts/BEE2_generated_music.txt',
                PackType.SOUNDSCRIPT,
                data=generate_music_script(music_data, packlist)
            )

        for filename, arcname in inject_files():
            LOGGER.info('Injecting "{}" into packfile.', arcname)
            with open(filename, 'rb') as f:
                packlist.pack_file(arcname, data=f.read())

    LOGGER.info('Run transformations...')
    run_transformations(bsp_ents, fsys, packlist, bsp_file, game)

    LOGGER.info('Scanning map for files to pack:')
    packlist.pack_from_bsp(bsp_file)
    packlist.pack_fgd(bsp_ents, fgd)
    packlist.eval_dependencies()
    LOGGER.info('Done!')

    if is_peti:
        packlist.write_manifest()
    else:
        # Write with the map name, so it loads directly.
        packlist.write_manifest(os.path.basename(path)[:-4])

    # We need to disallow Valve folders.
    pack_whitelist = set()  # type: Set[FileSystem]
    pack_blacklist = set()  # type: Set[FileSystem]
    if is_peti:
        if fsys_mel is not None:
            pack_whitelist.add(fsys_mel)
        if fsys_tag is not None:
            pack_whitelist.add(fsys_tag)
        # Exclude absolutely everything except our folder.
        for child_sys, _ in fsys.systems:
            # Add 'bee2/' and 'bee2_dev/' only.
            if (
                isinstance(child_sys, RawFileSystem) and
                'bee2' in os.path.basename(child_sys.path).casefold()
            ):
                pack_whitelist.add(child_sys)
            else:
                pack_blacklist.add(child_sys)

    if '-no_pack' not in args:
        # Cubemap files packed into the map already.
        with bsp_file.packfile() as zipfile:
            existing = set(zipfile.namelist())

        LOGGER.info('Writing to BSP...')
        packlist.pack_into_zip(
            bsp_file,
            ignore_vpk=True,
            whitelist=pack_whitelist,
            blacklist=pack_blacklist,
        )

        with bsp_file.packfile() as zipfile:
            LOGGER.info('Packed files:\n{}', '\n'.join(
                set(zipfile.namelist()) - existing
            ))

    dump_files(bsp_file)

    # Copy new entity data.
    bsp_file.lumps[BSP_LUMPS.ENTITIES].data = BSP.write_ent_data(bsp_ents)

    bsp_file.save()
    LOGGER.info(' - BSP written!')

    if is_peti:
        mod_screenshots()

    if edit_args:
        LOGGER.info("Forcing Cheap Lighting!")
        run_vrad(fast_args)
    else:
        if is_peti:
            LOGGER.info("Publishing - Full lighting enabled! (or forced to do so)")
        else:
            LOGGER.info("Hammer map detected! Not forcing cheap lighting..")
        run_vrad(full_args)

    LOGGER.info("BEE2 VRAD hook finished!")
def main(argv: List[str]) -> None:
    LOGGER.info('Srctools postcompiler hook started!')

    parser = argparse.ArgumentParser(
        description="Modifies the BSP file, allowing additional entities "
        "and bugfixes.", )

    parser.add_argument("--nopack",
                        dest="allow_pack",
                        action="store_false",
                        help="Prevent packing of files found in the map.")
    parser.add_argument(
        "--propcombine",
        action="store_true",
        help="Allow merging static props together.",
    )

    parser.add_argument(
        "map",
        help="The path to the BSP file.",
    )

    args = parser.parse_args(argv)

    # The path is the last argument to the compiler.
    # Hammer adds wrong slashes sometimes, so fix that.
    # Also if it's the VMF file, make it the BSP.
    path = Path(args.map).with_suffix('.bsp')

    LOGGER.info("Map path is {}", path)

    conf, game_info, fsys, pack_blacklist = config.parse(path)

    fsys.open_ref()

    packlist = PackList(fsys)

    LOGGER.info('Gameinfo: {}', game_info.path)
    LOGGER.info(
        'Search paths: \n{}',
        '\n'.join([sys.path for sys, prefix in fsys.systems]),
    )

    fgd = load_fgd()

    LOGGER.info('Loading soundscripts...')
    packlist.load_soundscript_manifest(
        conf.path.with_name('srctools_sndscript_data.vdf'))
    LOGGER.info('Done! ({} sounds)', len(packlist.soundscripts))

    LOGGER.info('Reading BSP...')
    bsp_file = BSP(path)

    LOGGER.info('Reading entities...')
    vmf = bsp_file.read_ent_data()
    LOGGER.info('Done!')

    run_transformations(vmf, fsys, packlist)

    studiomdl_loc = conf.get(str, 'propcombine_studiomdl')
    if studiomdl_loc and args.propcombine:
        LOGGER.info('Combining props...')
        propcombine.combine(
            bsp_file,
            vmf,
            packlist,
            game_info,
            game_info.root / studiomdl_loc,
            [
                game_info.root / folder for folder in conf.get(
                    Property, 'propcombine_qc_folder').as_array(conv=Path)
            ],
            conf.get(int, 'propcombine_auto_range'),
            conf.get(int, 'propcombine_min_cluster'),
        )
        LOGGER.info('Done!')

    bsp_file.lumps[BSP_LUMPS.ENTITIES].data = bsp_file.write_ent_data(vmf)

    if conf.get(bool, 'auto_pack') and args.allow_pack:
        LOGGER.info('Analysing packable resources...')
        packlist.pack_fgd(vmf, fgd)

        packlist.pack_from_bsp(bsp_file)

        packlist.eval_dependencies()

    with bsp_file.packfile() as pak_zip:
        packlist.pack_into_zip(pak_zip, blacklist=pack_blacklist)

    LOGGER.info('Packed files: \n{}'.format('\n'.join(pak_zip.namelist())))

    LOGGER.info('Writing BSP...')
    bsp_file.save()

    LOGGER.info("srctools VRAD hook finished!")
Beispiel #7
0
    def pack_into_zip(
        self,
        bsp: BSP,
        *,
        whitelist: Iterable[FileSystem]=(),
        blacklist: Iterable[FileSystem]=(),
        ignore_vpk: bool=True,
    ) -> None:
        """Pack all our files into the packfile in the BSP.

        The filesys is used to find files to pack.
        Filesystems must be in the whitelist and not in the blacklist, if provided.
        If ignore_vpk is True, files in VPK won't be packed unless that system
        is in allow_filesys.
        """
        # We need to rebuild the zipfile from scratch, so we can overwrite
        # old data if required.

        # First retrieve the data.
        with bsp.packfile() as start_zip:
            packed_files = {
                info.filename.casefold(): (info.filename, start_zip.read(info))
                for info in start_zip.infolist()
            }  # type: Dict[str, Tuple[str, bytes]]

        # The packed_files dict is a casefolded name -> (orig name, bytes) tuple.
        all_systems = {
            sys for sys, prefix in
            self.fsys.systems
        }  # type: Set[FileSystem]

        allowed = set(all_systems)

        if ignore_vpk:
            for fsys in all_systems:
                if isinstance(fsys, VPKFileSystem):
                    allowed.discard(fsys)

        # Add these on top, so this overrides ignore_vpk.
        allowed.update(whitelist)
        # Then remove blacklisted systems.
        allowed.difference_update(blacklist)

        LOGGER.debug('Allowed filesystems:\n{}', '\n'.join([
            ('+ ' if sys in allowed else '- ') + repr(sys) for sys, prefix in
            self.fsys.systems
        ]))

        with self.fsys:
            for file in self._files.values():
                # Need to ensure / separators.
                fname = file.filename.replace('\\', '/')

                already_packed = fname.casefold() in packed_files

                if file.data is not None:
                    # Always pack, we've got custom data.
                    packed_files[fname.casefold()] = (fname, file.data)
                    continue

                try:
                    sys_file = self.fsys[file.filename]
                except FileNotFoundError:
                    if not file.optional and not already_packed:
                        LOGGER.warning('WARNING: "{}" not packed!', file.filename)
                    continue

                if self.fsys.get_system(sys_file) in allowed:
                    LOGGER.debug('ADD:  {}', fname)
                    with sys_file.open_bin() as f:
                        packed_files[fname.casefold()] = (fname, f.read())
                else:
                    LOGGER.debug('SKIP: {}', fname)

        LOGGER.info('Compressing packfile...')
        with io.BytesIO() as new_data:
            with ZipFile(new_data, 'w') as new_zip:
                for fname, data in packed_files.values():
                    new_zip.writestr(fname, data)
            bsp.lumps[BSP_LUMPS.PAKFILE].data = new_data.getvalue()
Beispiel #8
0
def main(argv: List[str]) -> None:

    parser = argparse.ArgumentParser(
        description="Modifies the BSP file, allowing additional entities "
        "and bugfixes.", )

    parser.add_argument("--nopack",
                        dest="allow_pack",
                        action="store_false",
                        help="Prevent packing of files found in the map.")
    parser.add_argument(
        "--propcombine",
        action="store_true",
        help="Allow merging static props together.",
    )
    parser.add_argument(
        "--showgroups",
        action="store_true",
        help="Show propcombined props, by setting their tint to 0 255 0",
    )

    parser.add_argument(
        "map",
        help="The path to the BSP file.",
    )

    args = parser.parse_args(argv)

    # The path is the last argument to the compiler.
    # Hammer adds wrong slashes sometimes, so fix that.
    # Also if it's the VMF file, make it the BSP.
    path = Path(args.map).with_suffix('.bsp')

    # Open and start writing to the map's log file.
    handler = FileHandler(path.with_suffix('.log'))
    handler.setFormatter(
        Formatter(
            # One letter for level name
            '[{levelname}] {module}.{funcName}(): {message}',
            style='{',
        ))
    LOGGER.addHandler(handler)

    LOGGER.info('Srctools postcompiler hook started at {}!',
                datetime.datetime.now().isoformat())
    LOGGER.info("Map path is {}", path)

    conf, game_info, fsys, pack_blacklist, plugins = config.parse(path)

    fsys.open_ref()

    packlist = PackList(fsys)

    LOGGER.info('Gameinfo: {}', game_info.path)
    LOGGER.info(
        'Search paths: \n{}',
        '\n'.join([sys.path for sys, prefix in fsys.systems]),
    )

    fgd = FGD.engine_dbase()

    LOGGER.info('Loading soundscripts...')
    packlist.load_soundscript_manifest(
        conf.path.with_name('srctools_sndscript_data.vdf'))
    LOGGER.info('Done! ({} sounds)', len(packlist.soundscripts))

    LOGGER.info('Reading BSP...')
    bsp_file = BSP(path)

    LOGGER.info('Reading entities...')
    vmf = bsp_file.read_ent_data()
    LOGGER.info('Done!')

    studiomdl_path = conf.get(str, 'studiomdl')
    if studiomdl_path:
        studiomdl_loc = (game_info.root / studiomdl_path).resolve()
        if not studiomdl_loc.exists():
            LOGGER.warning('No studiomdl found at "{}"!', studiomdl_loc)
            studiomdl_loc = None
    else:
        LOGGER.warning('No studiomdl path provided.')
        studiomdl_loc = None

    for plugin in plugins:
        plugin.load()

    use_comma_sep = conf.get(bool, 'use_comma_sep')
    if use_comma_sep is None:
        # Guess the format, by picking whatever the first output uses.
        for ent in vmf.entities:
            for out in ent.outputs:
                use_comma_sep = out.comma_sep
                break
        if use_comma_sep is None:
            LOGGER.warning(
                'No outputs in map, could not determine BSP I/O format!')
            LOGGER.warning('Set "use_comma_sep" in srctools.vdf.')
        use_comma_sep = False

    run_transformations(vmf, fsys, packlist, bsp_file, game_info,
                        studiomdl_loc)

    if studiomdl_loc is not None and args.propcombine:
        LOGGER.info('Combining props...')
        propcombine.combine(
            bsp_file,
            vmf,
            packlist,
            game_info,
            studiomdl_loc,
            [
                game_info.root / folder for folder in conf.get(
                    Property, 'propcombine_qc_folder').as_array(conv=Path)
            ],
            conf.get(int, 'propcombine_auto_range'),
            conf.get(int, 'propcombine_min_cluster'),
            debug_tint=args.showgroups,
        )
        LOGGER.info('Done!')
    else:  # Strip these if they're present.
        for ent in vmf.by_class['comp_propcombine_set']:
            ent.remove()

    bsp_file.lumps[BSP_LUMPS.ENTITIES].data = bsp_file.write_ent_data(
        vmf, use_comma_sep)

    if conf.get(bool, 'auto_pack') and args.allow_pack:
        LOGGER.info('Analysing packable resources...')
        packlist.pack_fgd(vmf, fgd)

        packlist.pack_from_bsp(bsp_file)

        packlist.eval_dependencies()

    packlist.pack_into_zip(bsp_file,
                           blacklist=pack_blacklist,
                           ignore_vpk=False)

    with bsp_file.packfile() as pak_zip:
        LOGGER.info('Packed files: \n{}'.format('\n'.join(pak_zip.namelist())))

    LOGGER.info('Writing BSP...')
    bsp_file.save()

    LOGGER.info("srctools VRAD hook finished!")