def write_sound( file: StringIO, snds: Property, pack_list: PackList, snd_prefix: str = '*', ) -> None: """Write either a single sound, or multiple rndsound. snd_prefix is the prefix for each filename - *, #, @, etc. """ if snds.has_children(): file.write('"rndwave"\n\t{\n') for snd in snds: file.write('\t"wave" "{sndchar}{file}"\n'.format( file=snd.value.lstrip(SOUND_CHARS), sndchar=snd_prefix, )) pack_list.pack_file('sound/' + snd.value.casefold()) file.write('\t}\n') else: file.write('"wave" "{sndchar}{file}"\n'.format( file=snds.value.lstrip(SOUND_CHARS), sndchar=snd_prefix, )) pack_list.pack_file('sound/' + snds.value.casefold())
def write_sound( file: StringIO, snds: Property, pack_list: PackList, snd_prefix: str='*', ) -> None: """Write either a single sound, or multiple rndsound. snd_prefix is the prefix for each filename - *, #, @, etc. """ if snds.has_children(): file.write('"rndwave"\n\t{\n') for snd in snds: file.write( '\t"wave" "{sndchar}{file}"\n'.format( file=snd.value.lstrip(SOUND_CHARS), sndchar=snd_prefix, ) ) pack_list.pack_file('sound/' + snd.value.casefold()) file.write('\t}\n') else: file.write( '"wave" "{sndchar}{file}"\n'.format( file=snds.value.lstrip(SOUND_CHARS), sndchar=snd_prefix, ) ) pack_list.pack_file('sound/' + snds.value.casefold())
def write_sound( file: StringIO, snds: List[str], pack_list: PackList, snd_prefix: str = '*', ) -> None: """Write either a single sound, or multiple rndsound. snd_prefix is the prefix for each filename - *, #, @, etc. """ if len(snds) > 1: file.write('"rndwave"\n\t{\n') for snd in snds: file.write('\t"wave" "{sndchar}{file}"\n'.format( file=snd.lstrip(SND_CHARS), sndchar=snd_prefix, )) pack_list.pack_file('sound/' + snd.casefold()) file.write('\t}\n') else: file.write('"wave" "{sndchar}{file}"\n'.format( file=snds[0].lstrip(SND_CHARS), sndchar=snd_prefix, )) pack_list.pack_file('sound/' + snds[0].casefold())
def team_control_point(pack: PackList, ent: Entity) -> None: """Special '_locked' materials.""" for kvalue in ['team_icon_0', 'team_icon_1', 'team_icon_2']: mat = ent[kvalue] if mat: pack.pack_file('materials/{}.vmt'.format(mat), FileType.MATERIAL) pack.pack_file('materials/{}_locked.vmt'.format(mat), FileType.MATERIAL)
def pack_ent_class(pack: PackList, clsname: str) -> None: """Call to pack another entity class.""" reslist = CLASS_RESOURCES[clsname] if callable(reslist): raise ValueError( "Can't pack \"{}\", has a custom function!".format(clsname)) for fname, ftype in reslist: pack.pack_file(fname, ftype)
def skybox_swapper(pack: PackList, ent: Entity) -> None: """This needs to pack a skybox.""" sky_name = ent['skyboxname'] for suffix in ['bk', 'dn', 'ft', 'lf', 'rt', 'up']: pack.pack_file( 'materials/skybox/{}{}.vmt'.format(sky_name, suffix), FileType.MATERIAL, ) pack.pack_file( 'materials/skybox/{}{}_hdr.vmt'.format(sky_name, suffix), FileType.MATERIAL, optional=True, )
def move_rope(pack: PackList, ent: Entity) -> None: """Implement move_rope and keyframe_rope resources.""" old_shader_type = conv_int(ent['RopeShader']) if old_shader_type == 0: pack.pack_file('materials/cable/cable.vmt', FileType.MATERIAL) elif old_shader_type == 1: pack.pack_file('materials/cable/rope.vmt', FileType.MATERIAL) else: pack.pack_file('materials/cable/chain.vmt', FileType.MATERIAL) pack.pack_file('materials/cable/rope_shadowdepth.vmt', FileType.MATERIAL)
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[:]: if a.casefold() in ( "-both", "-final", "-staticproplighting", "-staticproppolys", "-textureshadows", ): # remove final parameters from the modified arguments fast_args.remove(a) 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() 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) 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.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) 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: pack_blacklist |= { RawFileSystem(root_folder / 'portal2_dlc2'), RawFileSystem(root_folder / 'portal2_dlc1'), RawFileSystem(root_folder / 'portal2'), RawFileSystem(root_folder / 'platform'), RawFileSystem(root_folder / 'update'), } if fsys_mel is not None: pack_whitelist.add(fsys_mel) if fsys_tag is not None: pack_whitelist.add(fsys_tag) if '-no_pack' not in args: # Cubemap files packed into the map already. existing = set(zipfile.infolist()) LOGGER.info('Writing to BSP...') packlist.pack_into_zip( zipfile, ignore_vpk=True, whitelist=pack_whitelist, blacklist=pack_blacklist, ) LOGGER.info( 'Packed files:\n{}', '\n'.join([ zipinfo.filename for zipinfo in zipfile.infolist() if zipinfo.filename not in existing ])) dump_files(zipfile) zipfile.close() # Finalise the zip modification # Copy the zipfile into the BSP file, and adjust the headers. bsp_file.lumps[BSP_LUMPS.PAKFILE].data = zip_data.getvalue() # 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 generate(bsp: VMF, pack_list: PackList): """Generate a soundscript file for music.""" # We also pack the filenames used for the tracks - that way funnel etc # only get packed when needed. Stock sounds are in VPKS or in aperturetag/, # we don't check there. tracks: Dict[Channel, List[str]] = {} sync_funnel = False for conf_ent in bsp.by_class['bee2_music_channel']: conf_ent.remove() channel = Channel(conf_ent['channel'].casefold()) if channel is Channel.TBEAM: sync_funnel = srctools.conv_bool(conf_ent['sync']) track = [] for i in itertools.count(1): snd = conf_ent[f'track{i:02}'] if not snd: break track.append(snd) if track: tracks[channel] = track if not tracks: return None # No music. if Channel.BASE not in tracks: tracks[Channel.BASE] = ['bee2/silent_lp.wav'] # Don't sync to a 2-second sound. sync_funnel = False file = StringIO() # Write the base music track file.write(MUSIC_START.format(name='', vol='1')) write_sound(file, tracks[Channel.BASE], pack_list, snd_prefix='#*') file.write(MUSIC_BASE) # The 'soundoperators' section is still open now. # Add the operators to play the auxiliary sounds.. if Channel.TBEAM in tracks: file.write(MUSIC_FUNNEL_MAIN) if Channel.BOUNCE in tracks: file.write(MUSIC_GEL_BOUNCE_MAIN) if Channel.SPEED in tracks: file.write(MUSIC_GEL_SPEED_MAIN) # End the main sound block file.write(MUSIC_END) if Channel.TBEAM in tracks: # Write the 'music.BEE2_funnel' sound entry file.write('\n') file.write(MUSIC_START.format(name='_funnel', vol='1')) write_sound(file, tracks[Channel.TBEAM], pack_list) # Some tracks want the funnel music to sync with the normal # track, others randomly choose a start. file.write(MUSIC_FUNNEL_SYNC_STACK if sync_funnel else MUSIC_FUNNEL_RAND_STACK) file.write(MUSIC_FUNNEL_UPDATE_STACK) if Channel.BOUNCE in tracks: file.write('\n') file.write(MUSIC_START.format(name='_gel_bounce', vol='0.5')) write_sound(file, tracks[Channel.BOUNCE], pack_list) # Fade in fast (we never get false positives, but fade out slow # since this disables when falling back.. file.write(MUSIC_GEL_STACK.format(fadein=0.25, fadeout=1.5)) if Channel.SPEED in tracks: file.write('\n') file.write(MUSIC_START.format(name='_gel_speed', vol='0.5')) write_sound(file, tracks[Channel.SPEED], pack_list) # We need to shut off the sound fast, so portals don't confuse it. # Fade in slow so it doesn't make much sound (and also as we get # up to speed). We stop almost immediately on gel too. file.write(MUSIC_GEL_STACK.format(fadein=0.5, fadeout=0.1)) pack_list.pack_file('scripts/BEE2_generated_music.txt', FileType.SOUNDSCRIPT, data=file.getvalue().encode())
def vgui_movie_display(pack: PackList, ent: Entity): """Mark the BIK movie as being used, though it can't be packed.""" pack.pack_file(ent['MovieFilename'])
def color_correction_volume(pack: PackList, ent: Entity) -> None: """Pack the color correction file for this too.""" pack.pack_file(ent['filename'])
def color_correction(pack: PackList, ent: Entity) -> None: """Pack the color correction file.""" pack.pack_file(ent['filename'])
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[:]: if a.casefold() in ( "-both", "-final", "-staticproplighting", "-staticproppolys", "-textureshadows", ): # remove final parameters from the modified arguments fast_args.remove(a) 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() 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) 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.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) 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: pack_blacklist |= { RawFileSystem(root_folder / 'portal2_dlc2'), RawFileSystem(root_folder / 'portal2_dlc1'), RawFileSystem(root_folder / 'portal2'), RawFileSystem(root_folder / 'platform'), RawFileSystem(root_folder / 'update'), } if fsys_mel is not None: pack_whitelist.add(fsys_mel) if fsys_tag is not None: pack_whitelist.add(fsys_tag) if '-no_pack' not in args: # Cubemap files packed into the map already. existing = set(zipfile.infolist()) LOGGER.info('Writing to BSP...') packlist.pack_into_zip( zipfile, ignore_vpk=True, whitelist=pack_whitelist, blacklist=pack_blacklist, ) LOGGER.info('Packed files:\n{}', '\n'.join([ zipinfo.filename for zipinfo in zipfile.infolist() if zipinfo.filename not in existing ])) dump_files(zipfile) zipfile.close() # Finalise the zip modification # Copy the zipfile into the BSP file, and adjust the headers. bsp_file.lumps[BSP_LUMPS.PAKFILE].data = zip_data.getvalue() # 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 parse( vmf: VMF, pack: PackList ) -> Tuple[int, Dict[Tuple[str, str, int], VacObject], Dict[str, str], ]: """Parse out the cube objects from the map. The return value is the number of objects, a dict of objects, and the filenames of the script generated for each group. The dict is (group, model, skin) -> object. """ cube_objects: Dict[Tuple[str, str, int], VacObject] = {} vac_objects: Dict[str, List[VacObject]] = defaultdict(list) for i, ent in enumerate(vmf.by_class['comp_vactube_object']): offset = Vec.from_str(ent['origin']) - Vec.from_str(ent['offset']) obj = VacObject( f'obj_{i:x}', ent['group'], ent['model'], ent['cube_model'], offset, srctools.conv_int(ent['weight']), srctools.conv_int(ent['tv_skin']), srctools.conv_int(ent['cube_skin']), srctools.conv_int(ent['skin']), ) vac_objects[obj.group].append(obj) # Convert the ent into a precache ent, stripping the other keyvalues. mdl_name = ent['model'] ent.keys = {'model': mdl_name} make_precache_prop(ent) pack.pack_file(mdl_name, FileType.MODEL, skinset={obj.skin_vac}) if obj.model_drop: cube_objects[obj.group, obj.model_drop.replace('\\', '/'), obj.skin_drop, ] = obj # Generate and pack the vactube object scripts. # Each group is the same, so it can be shared among them all. codes = {} for group, objects in sorted(vac_objects.items(), key=lambda t: t[0]): # First, see if there's a common multiple among the weights, allowing # us to simplify. multiple = objects[0].weight for obj in objects[1:]: multiple = math.gcd(multiple, obj.weight) if multiple > 1: LOGGER.info('Group "{}" has common factor of {}, simplifying.', group, multiple) code = [] for i, obj in enumerate(objects): obj.weight /= multiple if obj.model_drop: model_code = f'"{obj.model_drop}"' else: model_code = 'null' code.append( f'{obj.id} <- obj("{obj.model_vac}", {obj.skin_vac}, ' f'{model_code}, {obj.weight}, "{obj.offset}", {obj.skin_tv});') codes[group] = pack.inject_vscript('\n'.join(code)) return len(vac_objects), cube_objects, codes
def main(args: List[str]) -> None: """Main script.""" parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-f", "--filter", help="filter output to only display resources in this subfolder. " "This can be used multiple times.", type=str.casefold, action='append', metavar='folder', dest='filters', ) parser.add_argument( "-u", "--unused", help="Instead of showing depenencies, show files in the filtered " "folders that are unused.", action='store_true', ) parser.add_argument( "game", help="either location of a gameinfo.txt file, or any root folder.", ) parser.add_argument( "path", help="the files to load. The path can have a single * in the " "filename to match files with specific extensions and a prefix.", ) result = parser.parse_args(args) if result.unused and not result.filters: raise ValueError( 'At least one filter must be provided in "unused" mode.') if result.game: try: fsys = Game(result.game).get_filesystem() except FileNotFoundError: fsys = FileSystemChain(RawFileSystem(result.game)) else: fsys = FileSystemChain() packlist = PackList(fsys) file_path: str = result.path print('Finding files...') with fsys: if '*' in file_path: # Multiple files if file_path.count('*') > 1: raise ValueError('Multiple * in path!') prefix, suffix = file_path.split('*') folder, prefix = os.path.split(prefix) prefix = prefix.casefold() suffix = suffix.casefold() print(f'Prefix: {prefix!r}, suffix: {suffix!r}') print(f'Searching folder {folder}...') files = [] for file in fsys.walk_folder(folder): file_path = file.path.casefold() if not os.path.basename(file_path).startswith(prefix): continue if file_path.endswith(suffix): print(' ' + file.path) files.append(file) else: # Single file files = [fsys[file_path]] for file in files: ext = file.path[-4:].casefold() if ext == '.vmf': with file.open_str() as f: vmf_props = Property.parse(f) vmf = VMF.parse(vmf_props) packlist.pack_fgd(vmf, fgd) del vmf, vmf_props # Hefty, don't want to keep. elif ext == '.bsp': child_sys = fsys.get_system(file) if not isinstance(child_sys, RawFileSystem): raise ValueError('Cannot inspect BSPs in VPKs!') bsp = BSP(os.path.join(child_sys.path, file.path)) packlist.pack_from_bsp(bsp) packlist.pack_fgd(bsp.read_ent_data(), fgd) del bsp else: packlist.pack_file(file.path) print('Evaluating dependencies...') packlist.eval_dependencies() print('Done.') if result.unused: print('Unused files:') used = set(packlist.filenames()) for folder in result.filters: for file in fsys.walk_folder(folder): if file.path.casefold() not in used: print(' ' + file.path) else: print('Dependencies:') for filename in packlist.filenames(): if not result.filters or any( map(filename.casefold().startswith, result.filters)): print(' ' + filename)