def test_instances() -> None: """Test instance definitions.""" [[item], renderables] = Item.parse(START_EXPORTING + ''' "Instances" { "0" // Full PeTI style definition { "Name" "instances/p2editor/something.vmf" "EntityCount" "30" "BrushCount" "28" "BrushSideCount" "4892" } "another_name" "instances/more_custom.vmf" "bee2_second_CUst" "instances/even_more.vmf" "1" { "Name" "instances/somewhere_else/item.vmf" } "5" "instances/skipping_indexes.vmf" "2" "instances/direct_path.vmf" "cust_name" { "Name" "instances/a_custom_item.vmf" "EntityCount" "327" "BrushCount" "1" "BrushSideCount" "32" } } }} // End exporting + item ''') assert len(item.instances) == 6 assert item.instances[0] == InstCount( FSPath("instances/p2editor/something.vmf"), 30, 28, 4892) assert item.instances[1] == InstCount( FSPath("instances/somewhere_else/item.vmf"), 0, 0, 0) assert item.instances[2] == InstCount(FSPath("instances/direct_path.vmf"), 0, 0, 0) assert item.instances[3] == InstCount(FSPath(), 0, 0, 0) assert item.instances[4] == InstCount(FSPath(), 0, 0, 0) assert item.instances[5] == InstCount( FSPath("instances/skipping_indexes.vmf"), 0, 0, 0) # Counts discarded for custom items, and casefolded. assert item.cust_instances == { "another_name": FSPath("instances/more_custom.vmf"), "second_cust": FSPath("instances/even_more.vmf"), "cust_name": FSPath("instances/a_custom_item.vmf"), }
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 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: Optional[Item] = None extra_items: List[EditorItem] = [] with filesystem: 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('"{}:items/{}/editoritems.txt has no ' '"Item" block!'.format(pak_id, fold)) # 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: for subtype in extra_item.subtypes: if subtype.pal_pos is not None: LOGGER.warning( '"{}:items/{}/editoritems.txt has palette set for extra' ' item blocks. Deleting.'.format(pak_id, fold)) subtype.pal_icon = subtype.pal_pos = subtype.pal_name = None try: all_icon = FSPath(props['all_icon']) 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='<{}>/items/{}'.format(pak_id, fold), vbsp_config=Property(None, []), authors=sep_values(props['authors', '']), tags=sep_values(props['tags', '']), desc=desc_parse(props, pak_id + ':' + prop_path), ent_count=props['ent_count', ''], url=props['infoURL', None], icons={p.name: p.value for p in props['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 at least 1, but not all of the grouping icon # definitions then notify the author. num_group_parts = ((folders[fold].all_name is not None) + (folders[fold].all_icon is not None) + ('all' in folders[fold].icons)) if 0 < num_group_parts < 3: LOGGER.warning( 'Warning: "{id}:{path}" has incomplete grouping icon ' 'definition!', id=pak_id, path=prop_path, ) try: with filesystem: folders[fold].vbsp_config = conf = filesystem.read_prop( config_path, ) except FileNotFoundError: folders[fold].vbsp_config = conf = Property(None, []) set_cond_source(conf, folders[fold].source) return folders
def test_parse_goo() -> None: """Verify all the values in a goo item definition are correct.""" [[item], _] = Item.parse(''' Item { "Type" "ITEM_GOO" "ItemClass" "ItemGoo" "Editor" { "SubType" { "Name" "PORTAL2_PuzzleEditor_Item_goo" "Model" { "ModelName" "goo_man.3ds" } "Model" { "ModelName" "goo_man_water.mdl" } "Palette" { "Tooltip" "PORTAL2_PuzzleEditor_Palette_goo" "Image" "palette/goo.png" "Position" "2 6 0" } "Sounds" { "SOUND_CREATED" "P2Editor.PlaceOther" "SOUND_EDITING_ACTIVATE" "P2Editor.ExpandOther" "SOUND_EDITING_DEACTIVATE" "P2Editor.CollapseOther" "SOUND_DELETED" "P2Editor.RemoveOther" } } "MovementHandle" "HANDLE_NONE" "DesiredFacing" "DESIRES_UP" } "Exporting" { "TargetName" "goo" "Offset" "64 64 64" "OccupiedVoxels" { "Voxel" { "Pos" "0 0 0" "CollideType" "COLLIDE_NOTHING" "CollideAgainst" "COLLIDE_NOTHING" "Surface" { "Normal" "0 0 1" } } } } } ''') assert item.id == "ITEM_GOO" assert item.cls is ItemClass.GOO assert len(item.subtypes) == 1 [subtype] = item.subtypes assert subtype.name == "PORTAL2_PuzzleEditor_Item_goo" assert subtype.models == [ # Regardless of original extension, both become .mdl since that's more # correct. FSPath("goo_man.mdl"), FSPath("goo_man_water.mdl"), ] assert subtype.pal_name == "PORTAL2_PuzzleEditor_Palette_goo" assert subtype.pal_icon == FSPath("palette/goo.vtf") assert subtype.pal_pos == (2, 6) assert subtype.sounds == { Sound.CREATE: "P2Editor.PlaceOther", Sound.PROPS_OPEN: "P2Editor.ExpandOther", Sound.PROPS_CLOSE: "P2Editor.CollapseOther", Sound.DELETE: "P2Editor.RemoveOther", # Default values. Sound.SELECT: '', Sound.DESELECT: '', } assert item.handle is Handle.NONE assert item.facing is DesiredFacing.UP assert item.targetname == "goo" assert item.offset == Vec(64, 64, 64) assert len(item.occupy_voxels) == 1 occupation: OccupiedVoxel [occupation] = item.occupy_voxels assert occupation.type is CollType.NOTHING assert occupation.against is CollType.NOTHING assert occupation.pos == Vec(0, 0, 0) assert occupation.normal == Vec(0, 0, 1) assert occupation.subpos is None # Check these are default. assert item.occupies_voxel is False assert item.copiable is True assert item.deletable is True assert item.anchor_goo is False assert item.anchor_barriers is False assert item.pseudo_handle is False assert item.force_input is False assert item.force_output is False assert item.antline_points == { ConnSide.UP: [], ConnSide.DOWN: [], ConnSide.LEFT: [], ConnSide.RIGHT: [], } assert item.animations == {} assert item.properties == {} assert item.invalid_surf == set() assert item.cust_instances == {} assert item.instances == [] assert item.embed_voxels == set() assert item.embed_faces == [] assert item.overlays == [] assert item.conn_inputs == {} assert item.conn_outputs == {} assert item.conn_config is None
def parse(cls, data: ParseData): """Parse a style definition.""" info = data.info # type: Property filesystem = data.fsys # type: FileSystem selitem_data = SelitemData.parse(info) base = info['base', ''] has_video = srctools.conv_bool( info['has_video', ''], not data.is_override, # Assume no video for override ) vpk_name = info['vpk_name', ''].casefold() sugg = info.find_key('suggested', []) if data.is_override: # For overrides, we default to no suggestion.. sugg = ( sugg['quote', ''], sugg['music', ''], sugg['skybox', ''], sugg['elev', ''], ) else: sugg = ( sugg['quote', '<NONE>'], sugg['music', '<NONE>'], sugg['skybox', 'SKY_BLACK'], sugg['elev', '<NONE>'], ) corr_conf = info.find_key('corridors', []) corridors = {} icon_folder = corr_conf['icon_folder', ''] for group, length in CORRIDOR_COUNTS.items(): group_prop = corr_conf.find_key(group, []) for i in range(1, length + 1): prop = group_prop.find_key(str(i), '') # type: Property if icon_folder: icon = '{}/{}/{}.jpg'.format(icon_folder, group, i) # If this doesn't actually exist, don't use this. if 'resources/bee2/corr/' + icon not in data.fsys: LOGGER.debug('No "resources/bee2/{}"!', icon) icon = '' else: icon = '' if prop.has_children(): corridors[group, i] = CorrDesc( name=prop['name', ''], icon=prop['icon', icon], desc=prop['Desc', ''], ) else: corridors[group, i] = CorrDesc( name=prop.value, icon=icon, desc='', ) if base == '': base = None try: folder = 'styles/' + info['folder'] except IndexError: # It's OK for override styles to be missing their 'folder' # value. if data.is_override: items = [] renderables = {} vbsp = None else: raise ValueError( f'Style "{data.id}" missing configuration folder!') else: with filesystem: with filesystem[folder + '/items.txt'].open_str() as f: items, renderables = EditorItem.parse(f) try: vbsp = filesystem.read_prop(folder + '/vbsp_config.cfg') except FileNotFoundError: vbsp = None return cls( style_id=data.id, selitem_data=selitem_data, items=items, renderables=renderables, config=vbsp, base_style=base, suggested=sugg, has_video=has_video, corridors=corridors, vpk_name=vpk_name, )