def get_intersect_testcases() -> list: """Use a VMF to make it easier to generate the bounding boxes.""" with Path(__file__, '../bbox_samples.vmf').open() as f: vmf = VMF.parse(Property.parse(f)) def process(brush: Solid | None) -> tuple[tuple[int, ...], tuple[int, ...]] | None: """Extract the bounding box from the brush.""" if brush is None: return None bb_min, bb_max = brush.get_bbox() for vec in [bb_min, bb_max]: for ax in 'xyz': # If one thick, make zero thick so we can test planes. if abs(vec[ax]) == 63: vec[ax] = math.copysign(64, vec[ax]) return (tuple(map(int, bb_min)), tuple(map(int, bb_max))) for ent in vmf.entities: test = expected = None for solid in ent.solids: if solid.sides[0].mat.casefold() == 'tools/toolsskip': expected = solid if solid.sides[0].mat.casefold() == 'tools/toolstrigger': test = solid if test is None: raise ValueError(ent.id) yield (*process(test), process(expected))
def make_tag_coop_inst(tag_loc: str): """Make the coop version of the tag instances. This needs to be shrunk, so all the logic entities are not spread out so much (coop tubes are small). This way we avoid distributing the logic. """ global TAG_COOP_INST_VMF TAG_COOP_INST_VMF = vmf = VMF.parse( os.path.join(tag_loc, TAG_GUN_COOP_INST) ) def logic_pos(): """Put the entities in a nice circle...""" while True: for ang in range(0, 44): ang *= 360/44 yield Vec(16*math.sin(ang), 16*math.cos(ang), 32) pos = logic_pos() # Move all entities that don't care about position to the base of the player for ent in TAG_COOP_INST_VMF.iter_ents(): if ent['classname'] == 'info_coop_spawn': # Remove the original spawn point from the instance. # That way it can overlay over other dropper instances. ent.remove() elif ent['classname'] in ('info_target', 'info_paint_sprayer'): pass else: ent['origin'] = next(pos) # These originally use the coop spawn point, but this doesn't # always work. Switch to the name of the player, which is much # more reliable. if ent['classname'] == 'logic_measure_movement': ent['measuretarget'] = '!player_blue' # Add in a trigger to start the gel gun, and reset the activated # gel whenever the player spawns. trig_brush = vmf.make_prism( Vec(-32, -32, 0), Vec(32, 32, 16), mat='tools/toolstrigger', ).solid start_trig = vmf.create_ent( classname='trigger_playerteam', target_team=3, # ATLAS spawnflags=1, # Clients only origin='0 0 8', ) start_trig.solids = [trig_brush] start_trig.add_out( # This uses the !activator as the target player so it must be via trigger. Output('OnStartTouchBluePlayer', '@gel_ui', 'Activate', delay=0, only_once=True), # Reset the gun to fire nothing. Output('OnStartTouchBluePlayer', '@blueisenabled', 'SetValue', 0, delay=0.1), Output('OnStartTouchBluePlayer', '@orangeisenabled', 'SetValue', 0, delay=0.1), )
def clean_vmf(vmf_path): """Optimise the VMFs, removing unneeded entities or objects.""" inst = VMF.parse(vmf_path) for ent in itertools.chain([inst.spawn], inst.entities[:]): # type: Entity # Remove comments ent.comments = '' # Remove entities that have their visgroups hidden. if ent.hidden or not ent.vis_shown: print('Removing hidden ent') inst.remove_ent(ent) continue # Remove info_null entities if ent['classname'] == 'info_null': print('Removing info_null...') inst.remove_ent(ent) continue # All instances must be in bee2/, so any reference outside there is a map error! # It's ok if it's in p2editor and not in a subfolder though. # There's also an exception needed for the Tag gun instance. if ent['classname'] == 'func_instance': inst_loc = ent['file'].casefold().replace('\\','/') if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc: input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path)) yield from clean_vmf(vmf_path) # Re-run so we check again.. for solid in ent.solids[:]: if all(face.mat.casefold() == 'tools/toolsskip' for face in solid): print('Removing SKIP brush') ent.solids.remove(solid) continue if solid.hidden or not solid.vis_shown: print('Removing hidden brush') ent.solids.remove(solid) continue for detail in inst.by_class['func_detail']: # Remove several unused default options from func_detail. # We're not on xbox! del detail['disableX360'] # These aren't used in any instances, and it doesn't seem as if # VBSP preserves these values anyway. del detail['maxcpulevel'], detail['mincpulevel'] del detail['maxgpulevel'], detail['mingpulevel'] # Since all VMFs are instances or similar (not complete maps), we'll never # use worldspawn's settings. Keep mapversion though. del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth'] del inst.spawn['maxblobcount'], del inst.spawn['detailvbsp'], inst.spawn['detailmaterial'] lines = inst.export(inc_version=False, minimal=True).splitlines() for line in lines: yield line.lstrip()
def clean_vmf(vmf_path): """Optimise the VMFs, removing unneeded entities or objects.""" inst = VMF.parse(vmf_path) for ent in itertools.chain([inst.spawn], inst.entities[:]): # type: Entity # Remove comments ent.comments = '' # Remove entities that have their visgroups hidden. if ent.hidden or not ent.vis_shown: print('Removing hidden ent') inst.remove_ent(ent) continue # Remove hammer_notes entities if ent['classname'] == 'hammer_notes': print('Removing hammer_notes...') inst.remove_ent(ent) continue # All instances must be in bee2/, so any reference outside there is a map error! # It's ok if it's in p2editor and not in a subfolder though. # There's also an exception needed for the Tag gun instance. if ent['classname'] == 'func_instance': inst_loc = ent['file'].casefold().replace('\\','/') if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc: input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path)) yield from clean_vmf(vmf_path) # Re-run so we check again.. for solid in ent.solids[:]: if all(face.mat.casefold() == 'tools/toolsskip' for face in solid): print('Removing SKIP brush') ent.solids.remove(solid) continue if solid.hidden or not solid.vis_shown: print('Removing hidden brush') ent.solids.remove(solid) continue for detail in inst.by_class['func_detail']: # Remove several unused default options from func_detail. # We're not on xbox! del detail['disableX360'] # These aren't used in any instances, and it doesn't seem as if # VBSP preserves these values anyway. del detail['maxcpulevel'], detail['mincpulevel'] del detail['maxgpulevel'], detail['mingpulevel'] # Since all VMFs are instances or similar (not complete maps), we'll never # use worldspawn's settings. Keep mapversion though. del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth'] del inst.spawn['maxblobcount'], del inst.spawn['detailvbsp'], inst.spawn['detailmaterial'] lines = inst.export(inc_version=False, minimal=True).splitlines() for line in lines: yield line.lstrip()
def make_tag_coop_inst(tag_loc: str): """Make the coop version of the tag instances. This needs to be shrunk, so all the logic entities are not spread out so much (coop tubes are small). This way we avoid distributing the logic. """ global TAG_COOP_INST_VMF TAG_COOP_INST_VMF = vmf = VMF.parse(os.path.join(tag_loc, TAG_GUN_COOP_INST)) def logic_pos(): """Put the entities in a nice circle...""" while True: for ang in range(0, 44): ang *= 360 / 44 yield Vec(16 * math.sin(ang), 16 * math.cos(ang), 32) pos = logic_pos() # Move all entities that don't care about position to the base of the player for ent in TAG_COOP_INST_VMF.iter_ents(): if ent["classname"] == "info_coop_spawn": # Remove the original spawn point from the instance. # That way it can overlay over other dropper instances. ent.remove() elif ent["classname"] in ("info_target", "info_paint_sprayer"): pass else: ent["origin"] = next(pos) # These originally use the coop spawn point, but this doesn't # always work. Switch to the name of the player, which is much # more reliable. if ent["classname"] == "logic_measure_movement": ent["measuretarget"] = "!player_blue" # Add in a trigger to start the gel gun, and reset the activated # gel whenever the player spawns. trig_brush = vmf.make_prism(Vec(-32, -32, 0), Vec(32, 32, 16), mat="tools/toolstrigger").solid start_trig = vmf.create_ent( classname="trigger_playerteam", target_team=3, spawnflags=1, origin="0 0 8" # ATLAS # Clients only ) start_trig.solids = [trig_brush] start_trig.add_out( # This uses the !activator as the target player so it must be via trigger. Output("OnStartTouchBluePlayer", "@gel_ui", "Activate", delay=0, only_once=True), # Reset the gun to fire nothing. Output("OnStartTouchBluePlayer", "@blueisenabled", "SetValue", 0, delay=0.1), Output("OnStartTouchBluePlayer", "@orangeisenabled", "SetValue", 0, delay=0.1), )
def parse(cls, data: ParseData): """Read templates from a package.""" file = get_config( prop_block=data.info, fsys=data.fsys, folder='templates', pak_id=data.pak_id, prop_name='file', extension='.vmf', ) file = VMF.parse(file) return cls( data.id, file, force=data.info['force', ''], keep_brushes=srctools.conv_bool(data.info['keep_brushes', '1'], True), )
def parse_item_folder( folders_to_parse: set[str], filesystem: FileSystem, pak_id: str, ) -> dict[str, ItemVariant]: """Parse through the data in item/ folders. folders is a dict, with the keys set to the folder names we want. The values will be filled in with itemVariant values """ folders: dict[str, ItemVariant] = {} for fold in folders_to_parse: prop_path = 'items/' + fold + '/properties.txt' editor_path = 'items/' + fold + '/editoritems.txt' config_path = 'items/' + fold + '/vbsp_config.cfg' first_item: EditorItem | None = None extra_items: list[EditorItem] = [] try: props = filesystem.read_prop(prop_path).find_key('Properties') f = filesystem[editor_path].open_str() except FileNotFoundError as err: raise IOError('"' + pak_id + ':items/' + fold + '" not valid! ' 'Folder likely missing! ') from err with f: tok = Tokenizer(f, editor_path) for tok_type, tok_value in tok: if tok_type is Token.STRING: if tok_value.casefold() != 'item': raise tok.error('Unknown item option "{}"!', tok_value) if first_item is None: first_item = EditorItem.parse_one(tok) else: extra_items.append(EditorItem.parse_one(tok)) elif tok_type is not Token.NEWLINE: raise tok.error(tok_type) if first_item is None: raise ValueError(f'"{pak_id}:items/{fold}/editoritems.txt has no ' '"Item" block!') try: editor_vmf = VMF.parse( filesystem.read_prop(editor_path[:-3] + 'vmf')) except FileNotFoundError: pass else: editoritems_vmf.load(first_item, editor_vmf) first_item.generate_collisions() # extra_items is any extra blocks (offset catchers, extent items). # These must not have a palette section - it'll override any the user # chooses. for extra_item in extra_items: extra_item.generate_collisions() for subtype in extra_item.subtypes: if subtype.pal_pos is not None: LOGGER.warning( f'"{pak_id}:items/{fold}/editoritems.txt has ' f'palette set for extra item blocks. Deleting.') subtype.pal_icon = subtype.pal_pos = subtype.pal_name = None # In files this is specified as PNG, but it's always really VTF. try: all_icon = FSPath(props['all_icon']).with_suffix('.vtf') except LookupError: all_icon = None folders[fold] = ItemVariant( editoritems=first_item, editor_extra=extra_items, # Add the folder the item definition comes from, # so we can trace it later for debug messages. source=f'<{pak_id}>/items/{fold}', pak_id=pak_id, vbsp_config=lazy_conf.BLANK, authors=sep_values(props['authors', '']), tags=sep_values(props['tags', '']), desc=desc_parse(props, f'{pak_id}:{prop_path}', pak_id), ent_count=props['ent_count', ''], url=props['infoURL', None], icons={ prop.name: img.Handle.parse( prop, pak_id, 64, 64, subfolder='items', ) for prop in props.find_children('icon') }, all_name=props['all_name', None], all_icon=all_icon, ) if Item.log_ent_count and not folders[fold].ent_count: LOGGER.warning( '"{id}:{path}" has missing entity count!', id=pak_id, path=prop_path, ) # If we have one of the grouping icon definitions but not both required # ones then notify the author. has_name = folders[fold].all_name is not None has_icon = folders[fold].all_icon is not None if (has_name or has_icon or 'all' in folders[fold].icons) and (not has_name or not has_icon): LOGGER.warning( 'Warning: "{id}:{path}" has incomplete grouping icon ' 'definition!', id=pak_id, path=prop_path, ) folders[fold].vbsp_config = lazy_conf.from_file( utils.PackagePath(pak_id, config_path), missing_ok=True, source=folders[fold].source, ) return folders
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)