Beispiel #1
0
def build_itemclass_dict(prop_block: Property):
    """Load in the item ID database.

    This maps item IDs to their item class, and their embed locations.
    """
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class

    # Now load in the embed data.
    for prop in prop_block.find_children('ItemEmbeds'):
        if prop.name not in CLASS_FOR_ITEM:
            LOGGER.warning('Unknown item ID with embeds "{}"!', prop.real_name)

        vecs = EMBED_OFFSETS.setdefault(prop.name, [])
        if ':' in prop.value:
            first, last = prop.value.split(':')
            bbox_min, bbox_max = Vec.bbox(Vec.from_str(first),
                                          Vec.from_str(last))
            vecs.extend(Vec.iter_grid(bbox_min, bbox_max))
        else:
            vecs.append(Vec.from_str(prop.value))

    LOGGER.info(
        'Read {} item IDs, with {} embeds!',
        len(ITEMS_WITH_CLASS),
        len(EMBED_OFFSETS),
    )
Beispiel #2
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Beispiel #3
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Beispiel #4
0
def res_timed_relay(vmf: VMF, res: Property) -> Callable[[Entity], None]:
    """Generate a logic_relay with outputs delayed by a certain amount.

    This allows triggering outputs based $timer_delay values.
    """
    delay_var = res['variable', consts.FixupVars.TIM_DELAY]
    name = res['targetname']
    disabled_var = res['disabled', '0']
    flags = res['spawnflags', '0']

    final_outs = [
        Output.parse(prop)
        for prop in res.find_children('FinalOutputs')
    ]

    rep_outs = [
        Output.parse(prop)
        for prop in res.find_children('RepOutputs')
    ]

    def make_relay(inst: Entity) -> None:
        """Places the relay."""
        relay = vmf.create_ent(
            classname='logic_relay',
            spawnflags=flags,
            origin=inst['origin'],
            targetname=local_name(inst, name),
        )

        relay['StartDisabled'] = inst.fixup.substitute(disabled_var, allow_invert=True)

        delay = srctools.conv_float(inst.fixup.substitute(delay_var))

        for off in range(int(math.ceil(delay))):
            for out in rep_outs:
                new_out = out.copy()
                new_out.target = local_name(inst, new_out.target)
                new_out.delay += off
                new_out.comma_sep = False
                relay.add_out(new_out)

        for out in final_outs:
            new_out = out.copy()
            new_out.target = local_name(inst, new_out.target)
            new_out.delay += delay
            new_out.comma_sep = False
            relay.add_out(new_out)

    return make_relay
Beispiel #5
0
def parse_packlists(props: Property) -> None:
    """Parse the packlists.cfg file, to load our packing lists."""
    for prop in props.find_children('Packlist'):
        PACKLISTS[prop.name] = {
            file.value
            for file in prop
        }
Beispiel #6
0
def parse_packlists(props: Property) -> None:
    """Parse the packlists.cfg file, to load our packing lists."""
    for prop in props.find_children('Packlist'):
        PACKLISTS[prop.name] = {
            file.value
            for file in prop
        }
Beispiel #7
0
        def iter_lines(conf: Property) -> Iterator[Property]:
            """Iterate over the varios line blocks."""
            yield from conf.find_all("Quotes", "Group", "Quote", "Line")

            yield from conf.find_all("Quotes", "Midchamber", "Quote", "Line")

            for group in conf.find_children("Quotes", "CoopResponses"):
                if group.has_children():
                    yield from group
Beispiel #8
0
def build_itemclass_dict(prop_block: Property):
    """Load in the dictionary mapping item classes to item ids"""
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class
Beispiel #9
0
def build_itemclass_dict(prop_block: Property):
    """Load in the dictionary mapping item classes to item ids"""
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class
Beispiel #10
0
def read_configs(conf: Property):
    """Build our connection configuration from the config files."""
    for prop in conf.find_children('Connections'):
        if prop.name in ITEM_TYPES:
            raise ValueError('Duplicate item type "{}"'.format(prop.real_name))
        ITEM_TYPES[prop.name] = ItemType.parse(prop.real_name, prop)

    if 'item_indicator_panel' not in ITEM_TYPES:
        raise ValueError('No checkmark panel item type!')

    if 'item_indicator_panel_timer' not in ITEM_TYPES:
        raise ValueError('No timer panel item type!')
Beispiel #11
0
def load_handler(props: Property) -> None:
    """Load compiler options from the palette."""
    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 atomic_write(SCREENSHOT_LOC, mode='wb', overwrite=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('voiceline_priority', VOICE_PRIORITY_VAR.get()))

    corr_prop = props.find_block('corridor', or_blank=True)
    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
Beispiel #12
0
def load_signs(conf: Property) -> None:
    """Load in the signage data."""
    for prop in conf.find_children('Signage'):
        SIGNAGES[prop.name] = sign = Sign.parse(prop)
        try:
            prim = prop.find_key('primary')
        except NoKeyError:
            pass
        else:
            sign.primary = Sign.parse(prim)
        try:
            sec = prop.find_key('secondary')
        except NoKeyError:
            pass
        else:
            sign.secondary = Sign.parse(sec)
    if 'arrow' not in SIGNAGES:
        LOGGER.warning('No ARROW signage type!')
Beispiel #13
0
def res_import_template_setup(res: Property):
    temp_id = res['id']

    force = res['force', ''].casefold().split()
    if 'white' in force:
        force_colour = template_brush.MAT_TYPES.white
    elif 'black' in force:
        force_colour = template_brush.MAT_TYPES.black
    elif 'invert' in force:
        force_colour = 'INVERT'
    else:
        force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    for size in ('2x2', '4x4', 'wall', 'special'):
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']

    replace_tex = defaultdict(list)
    for prop in res.find_key('replace', []):
        replace_tex[prop.name].append(prop.value)

    rem_replace_brush = True
    additional_ids = set()
    transfer_overlays = '1'
    try:
        replace_brush = res.find_key('replaceBrush')
    except NoKeyError:
        replace_brush_pos = None
    else:
        if replace_brush.has_children():
            replace_brush_pos = replace_brush['Pos', '0 0 0']
            additional_ids = set(map(
                srctools.conv_int,
                replace_brush['additionalIDs', ''].split(),
            ))
            rem_replace_brush = replace_brush.bool('removeBrush', True)
            transfer_overlays = replace_brush['transferOverlay', '1']
        else:
            replace_brush_pos = replace_brush.value  # type: str

        replace_brush_pos = Vec.from_str(replace_brush_pos)
        replace_brush_pos.z -= 64  # 0 0 0 defaults to the floor.

    key_values = res.find_key("Keys", [])
    if key_values:
        keys = Property("", [
            key_values,
            res.find_key("LocalKeys", []),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [
            Output.parse(prop)
            for prop in
            res.find_children('Outputs')
        ]
    else:
        keys = None
        outputs = []
    visgroup_mode = res['visgroup', 'none'].casefold()
    if visgroup_mode not in ('none', 'choose'):
        visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
        if visgroup_mode == 0:
            visgroup_mode = 'none'

    # Generate the function which picks which visgroups to add to the map.
    if visgroup_mode == 'none':
        def visgroup_func(_):
            """none = don't add any visgroups."""
            return ()
    elif visgroup_mode == 'choose':
        def visgroup_func(groups):
            """choose = add one random group."""
            return [random.choice(groups)]
    else:
        def visgroup_func(groups):
            """Number = percent chance for each to be added"""
            for group in groups:
                val = random.uniform(0, 100)
                if val <= visgroup_mode:
                    yield group

    # If true, force visgroups to all be used.
    visgroup_force_var = res['forceVisVar', '']

    return (
        temp_id,
        dict(replace_tex),
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        keys,
        outputs,
    )
Beispiel #14
0
def load_config(conf: Property):
    """Setup all the generators from the config data."""
    global SPECIAL, OVERLAYS
    global_options = {
        prop.name: prop.value
        for prop in conf.find_children('Options')
    }
    # Give generators access to the global settings.
    Generator.global_settings.update(
        parse_options(
            # Pass it to both, the second will fail too.
            global_options,
            global_options,
        ))

    data: Dict[Any, Tuple[Dict[str, Any], Dict[str, List[str]]]] = {}

    gen_cat: GenCat
    gen_orient: Optional[Orient]
    gen_portal: Optional[Portalable]

    # Use this to allow alternate names for generators.
    conf_for_gen: Dict[Tuple[GenCat, Optional[Orient], Optional[Portalable]],
                       Property, ] = {}

    for prop in conf:
        if prop.name in ('options', 'antlines'):
            continue
        if '.' in prop.name:
            try:
                gen_cat_name, gen_portal_raw, gen_orient_raw = prop.name.split(
                    '.')
                gen_cat = GEN_CATS[gen_cat_name]
                gen_orient = ORIENTS[gen_orient_raw]
                gen_portal = Portalable(gen_portal_raw)
            except (KeyError, ValueError):
                LOGGER.warning('Could not parse texture generator type "{}"!',
                               prop.name)
                continue
            conf_for_gen[gen_cat, gen_orient, gen_portal] = prop
        else:
            try:
                gen_cat = GEN_CATS[prop.name]
            except KeyError:
                LOGGER.warning('Unknown texture generator type "{}"!',
                               prop.name)
                continue
            conf_for_gen[gen_cat, None, None] = prop

    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        if isinstance(gen_key, GenCat):
            # It's a non-tile generator.
            is_tile = False
            gen_cat = gen_key
            try:
                gen_conf = conf_for_gen[gen_key, None, None]
            except KeyError:
                gen_conf = Property(gen_key.value, [])
        else:
            # Tile-type generator
            is_tile = True
            try:
                gen_conf = conf_for_gen[gen_key]
            except KeyError:
                gen_conf = Property('', [])

            if not gen_conf.has_children():
                # Special case - using a single value to indicate that all
                # textures are the same.
                gen_conf = Property(
                    gen_conf.real_name,
                    [
                        Property('4x4', gen_conf.value),
                        Property(
                            'Options',
                            [
                                # Clumping isn't useful since it's all the same.
                                Property('Algorithm', 'RAND'),
                            ])
                    ])
        textures = {}

        # First parse the options.
        options = parse_options(
            {
                prop.name: prop.value
                for prop in gen_conf.find_children('Options')
            }, global_options)

        # Now do textures.
        if is_tile:
            # Tile generator, always have all tile sizes, and
            # only use the defaults if no textures were specified.
            for tex_name in TileSize:
                textures[tex_name] = [
                    prop.value for prop in gen_conf.find_all(str(tex_name))
                ]

            # In case someone switches them around, add on 2x1 to 1x2 textures.
            textures[TileSize.TILE_2x1] += [
                prop.value for prop in gen_conf.find_all('1x2')
            ]

            if not any(textures.values()):
                for tex_name, tex_default in tex_defaults.items():
                    textures[tex_name] = [tex_default]
        else:
            # Non-tile generator, use defaults for each value
            for tex_name, tex_default in tex_defaults.items():
                textures[tex_name] = tex = [
                    prop.value for prop in gen_conf.find_all(str(tex_name))
                ]
                if not tex and tex_default:
                    tex.append(tex_default)

        data[gen_key] = options, textures

        # Next, do a check to see if any texture names were specified that
        # we don't recognise.
        extra_keys = {prop.name for prop in gen_conf}
        extra_keys.discard('options')  # Not a texture name, but valid.

        if isinstance(gen_key, GenCat):
            extra_keys.difference_update(map(str.casefold,
                                             tex_defaults.keys()))
        else:
            # The defaults are just the size values.
            extra_keys.difference_update(map(str, TileSize))

        if extra_keys:
            LOGGER.warning('{}: Unknown texture names {}',
                           format_gen_key(gen_key),
                           ', '.join(sorted(extra_keys)))

    # Now complete textures for tile types,
    # copying over data from other generators.
    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        if isinstance(gen_key, GenCat):
            continue
        gen_cat, gen_orient, gen_portal = gen_key

        options, textures = data[gen_key]

        if not any(textures.values()) and gen_cat is not GenCat.NORMAL:
            # For the additional categories of tiles, we copy the entire
            # NORMAL one over if it's not set.
            textures.update(data[GenCat.NORMAL, gen_orient, gen_portal][1])

        if not textures[TileSize.TILE_4x4]:
            raise ValueError('No 4x4 tile set for "{}"!'.format(gen_key))

        # Copy 4x4, 2x2, 2x1 textures to the 1x1 size if the option was set.
        # Do it before inheriting tiles, so there won't be duplicates.
        if options['mixtiles']:
            block_tex = textures[TileSize.TILE_1x1]
            block_tex += textures[TileSize.TILE_4x4]
            block_tex += textures[TileSize.TILE_2x2]
            block_tex += textures[TileSize.TILE_2x1]

        # We need to do more processing.
        for orig, targ in TILE_INHERIT:
            if not textures[targ]:
                textures[targ] = textures[orig].copy()

    # Now finally create the generators.
    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        options, textures = data[gen_key]

        if isinstance(gen_key, tuple):
            # Check the algorithm to use.
            algo = options['algorithm']
            gen_cat, gen_orient, gen_portal = gen_key
            try:
                generator: Type[Generator] = GEN_CLASSES[algo]  # type: ignore
            except KeyError:
                raise ValueError('Invalid algorithm "{}" for {}!'.format(
                    algo, gen_key))
        else:
            # Signage, Overlays always use the Random generator.
            generator = GenRandom
            gen_cat = gen_key
            gen_orient = gen_portal = None

        GENERATORS[gen_key] = gen = generator(gen_cat, gen_orient, gen_portal,
                                              options, textures)

        # Allow it to use the default enums as direct lookups.
        if isinstance(gen, GenRandom):
            if gen_portal is None:
                gen.set_enum(tex_defaults.items())
            else:
                # Tiles always use TileSize.
                gen.set_enum((size.value, size) for size in TileSize)

    SPECIAL = GENERATORS[GenCat.SPECIAL]
    OVERLAYS = GENERATORS[GenCat.OVERLAYS]
Beispiel #15
0
    def _modify_editoritems(
        self,
        props: Property,
        editor: list[EditorItem],
        pak_id: str,
        source: str,
        is_extra: bool,
    ) -> list[EditorItem]:
        """Modify either the base or extra editoritems block."""
        # We can share a lot of the data, if it isn't changed and we take
        # care to copy modified parts.
        editor = list(map(copy.copy, editor))

        # Create a list of subtypes in the file, in order to edit.
        subtype_lookup = [(item, i, subtype) for item in editor
                          for i, subtype in enumerate(item.subtypes)]

        # Implement overriding palette items
        for item in props.find_children('Palette'):
            try:
                pal_icon = FSPath(item['icon'])
            except LookupError:
                pal_icon = None
            pal_name = item['pal_name', None]  # Name for the palette icon
            try:
                bee2_icon = img.Handle.parse(
                    item.find_key('BEE2'),
                    pak_id,
                    64,
                    64,
                    subfolder='items',
                )
            except LookupError:
                bee2_icon = None

            if item.name == 'all':
                if is_extra:
                    raise Exception('Cannot specify "all" for hidden '
                                    f'editoritems blocks in {source}!')
                if pal_icon is not None:
                    self.all_icon = pal_icon
                    # If a previous BEE icon was present, remove so we use the VTF.
                    self.icons.pop('all', None)
                if pal_name is not None:
                    self.all_name = pal_name
                if bee2_icon is not None:
                    self.icons['all'] = bee2_icon
                continue

            try:
                subtype_ind = int(item.name)
                subtype_item, subtype_ind, subtype = subtype_lookup[
                    subtype_ind]
            except (IndexError, ValueError, TypeError):
                raise Exception(f'Invalid index "{item.name}" when modifying '
                                f'editoritems for {source}')
            subtype_item.subtypes = subtype_item.subtypes.copy()
            subtype_item.subtypes[subtype_ind] = subtype = copy.deepcopy(
                subtype)

            # Overriding model data.
            if 'models' in item or 'model' in item:
                subtype.models = []
                for prop in item:
                    if prop.name in ('models', 'model'):
                        if prop.has_children():
                            subtype.models.extend(
                                [FSPath(subprop.value) for subprop in prop])
                        else:
                            subtype.models.append(FSPath(prop.value))

            if item['name', None]:
                subtype.name = item['name']  # Name for the subtype

            if bee2_icon:
                if is_extra:
                    raise ValueError('Cannot specify BEE2 icons for hidden '
                                     f'editoritems blocks in {source}!')
                self.icons[item.name] = bee2_icon
            elif pal_icon is not None:
                # If a previous BEE icon was present, remove so we use the VTF.
                self.icons.pop(item.name, None)

            if pal_name is not None:
                subtype.pal_name = pal_name
            if pal_icon is not None:
                subtype.pal_icon = pal_icon

        if 'Instances' in props:
            if len(editor) != 1:
                raise ValueError('Cannot specify instances for multiple '
                                 f'editoritems blocks in {source}!')
            editor[0].instances = editor[0].instances.copy()
            editor[0].cust_instances = editor[0].cust_instances.copy()

        for inst in props.find_children('Instances'):
            if inst.has_children():
                inst_data = InstCount(
                    FSPath(inst['name']),
                    inst.int('entitycount'),
                    inst.int('brushcount'),
                    inst.int('brushsidecount'),
                )
            else:  # Allow just specifying the file.
                inst_data = InstCount(FSPath(inst.value), 0, 0, 0)

            if inst.real_name.isdecimal():  # Regular numeric
                try:
                    ind = int(inst.real_name)
                except IndexError:
                    # This would likely mean there's an extra definition or
                    # something.
                    raise ValueError(f'Invalid index {inst.real_name} for '
                                     f'instances in {source}') from None
                editor[0].set_inst(ind, inst_data)
            else:  # BEE2 named instance
                inst_name = inst.name
                if inst_name.startswith('bee2_'):
                    inst_name = inst_name[5:]
                editor[0].cust_instances[inst_name] = inst_data.inst

        # Override IO commands.
        try:
            io_props = props.find_key('IOConf')
        except LookupError:
            pass
        else:
            if len(editor) != 1:
                raise ValueError('Cannot specify I/O for multiple '
                                 f'editoritems blocks in {source}!')
            force = io_props['force', '']
            editor[0].conn_config = ConnConfig.parse(editor[0].id, io_props)
            editor[0].force_input = 'in' in force
            editor[0].force_output = 'out' in force

        return editor
Beispiel #16
0
    async def modify(self, pak_id: str, props: Property,
                     source: str) -> ItemVariant:
        """Apply a config to this item variant.

        This produces a copy with various modifications - switching
        out palette or instance values, changing the config, etc.
        """
        vbsp_config: lazy_conf.LazyConf
        if 'config' in props:
            # Item.parse() has resolved this to the actual config.
            vbsp_config = get_config(
                props,
                'items',
                pak_id,
            )
        else:
            vbsp_config = self.vbsp_config

        if 'replace' in props:
            # Replace property values in the config via regex.
            vbsp_config = lazy_conf.replace(
                vbsp_config,
                [(re.compile(prop.real_name, re.IGNORECASE), prop.value)
                 for prop in props.find_children('Replace')])

        vbsp_config = lazy_conf.concat(
            vbsp_config,
            get_config(
                props,
                'items',
                pak_id,
                prop_name='append',
            ))

        if 'description' in props:
            desc = desc_parse(props, source, pak_id)
        else:
            desc = self.desc.copy()

        if 'appenddesc' in props:
            desc = tkMarkdown.join(
                desc,
                desc_parse(props, source, pak_id, prop_name='appenddesc'),
            )

        if 'authors' in props:
            authors = sep_values(props['authors', ''])
        else:
            authors = self.authors

        if 'tags' in props:
            tags = sep_values(props['tags', ''])
        else:
            tags = self.tags.copy()

        variant = ItemVariant(
            pak_id,
            self.editor,
            vbsp_config,
            self.editor_extra.copy(),
            authors=authors,
            tags=tags,
            desc=desc,
            icons=self.icons.copy(),
            ent_count=props['ent_count', self.ent_count],
            url=props['url', self.url],
            all_name=self.all_name,
            all_icon=self.all_icon,
            source=f'{source} from {self.source}',
        )
        [variant.editor] = variant._modify_editoritems(
            props,
            [variant.editor],
            pak_id,
            source,
            is_extra=False,
        )

        if 'extra' in props:
            variant.editor_extra = variant._modify_editoritems(
                props.find_key('extra'),
                variant.editor_extra,
                pak_id,
                source,
                is_extra=True)

        return variant
Beispiel #17
0
def res_add_shuffle_group(vmf: VMF, res: Property) -> Callable[[Entity], None]:
    """Pick from a pool of instances to randomise decoration.

    For each sub-condition that succeeds, a random instance is placed, with
    a fixup set to a value corresponding to the condition.

    Parameters:
        - Var: The fixup variable to set on each item. This is used to tweak it
          to match the condition.
        - Conditions: Each value here is the value to produce if this instance
          is required. The contents of the block is then a condition flag to
          check.
        - Pool: A list of instances to randomly allocate to the conditions. There
          should be at least as many pool values as there are conditions.
        - Seed: Value to modify the seed with before placing.
    """
    conf_variable = res['var']
    conf_seed = 'sg' + res['seed', '']
    conf_pools: dict[str, list[str]] = {}
    for prop in res.find_children('pool'):
        if prop.has_children():
            raise ValueError('Instances in pool cannot be a property block!')
        conf_pools.setdefault(prop.name, []).append(prop.value)

    # (flag, value, pools)
    conf_selectors: list[tuple[list[Property], str, frozenset[str]]] = []
    for prop in res.find_all('selector'):
        conf_value = prop['value', '']
        conf_flags = list(prop.find_children('conditions'))
        try:
            picked_pools = prop['pools'].casefold().split()
        except LookupError:
            picked_pools = frozenset(conf_pools)
        else:
            for pool_name in picked_pools:
                if pool_name not in conf_pools:
                    raise ValueError(f'Unknown pool name {pool_name}!')
        conf_selectors.append(
            (conf_flags, conf_value, frozenset(picked_pools)))

    all_pools = [(name, inst) for name, instances in conf_pools.items()
                 for inst in instances]
    all_pools.sort()  # Ensure consistent order.

    def add_group(inst: Entity) -> None:
        """Place the group."""
        rng = rand.seed(b'shufflegroup', conf_seed, inst)
        pools = all_pools.copy()
        for (flags, value, potential_pools) in conf_selectors:
            for flag in flags:
                if not conditions.check_flag(vmf, flag, inst):
                    break
            else:  # Succeeded.
                allowed_inst = [(name, inst) for (name, inst) in pools
                                if name in potential_pools]
                name, filename = rng.choice(allowed_inst)
                pools.remove((name, filename))
                vmf.create_ent(
                    'func_instance',
                    targetname=inst['targetname'],
                    file=filename,
                    angles=inst['angles'],
                    origin=inst['origin'],
                    fixup_style='0',
                ).fixup[conf_variable] = value

    return add_group
Beispiel #18
0
def res_import_template_setup(res: Property):
    temp_id = res['id']

    force = res['force', ''].casefold().split()
    if 'white' in force:
        force_colour = template_brush.MAT_TYPES.white
    elif 'black' in force:
        force_colour = template_brush.MAT_TYPES.black
    elif 'invert' in force:
        force_colour = 'INVERT'
    else:
        force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    for size in ('2x2', '4x4', 'wall', 'special'):
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']

    replace_tex = defaultdict(list)
    for prop in res.find_key('replace', []):
        replace_tex[prop.name].append(prop.value)

    rem_replace_brush = True
    additional_ids = set()
    transfer_overlays = '1'
    try:
        replace_brush = res.find_key('replaceBrush')
    except NoKeyError:
        replace_brush_pos = None
    else:
        if replace_brush.has_children():
            replace_brush_pos = replace_brush['Pos', '0 0 0']
            additional_ids = set(map(
                srctools.conv_int,
                replace_brush['additionalIDs', ''].split(),
            ))
            rem_replace_brush = replace_brush.bool('removeBrush', True)
            transfer_overlays = replace_brush['transferOverlay', '1']
        else:
            replace_brush_pos = replace_brush.value  # type: str

        replace_brush_pos = Vec.from_str(replace_brush_pos)
        replace_brush_pos.z -= 64  # 0 0 0 defaults to the floor.

    key_values = res.find_key("Keys", [])
    if key_values:
        keys = Property("", [
            key_values,
            res.find_key("LocalKeys", []),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [
            Output.parse(prop)
            for prop in
            res.find_children('Outputs')
        ]
    else:
        keys = None
        outputs = []
    visgroup_mode = res['visgroup', 'none'].casefold()
    if visgroup_mode not in ('none', 'choose'):
        visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
        if visgroup_mode == 0:
            visgroup_mode = 'none'

    # Generate the function which picks which visgroups to add to the map.
    if visgroup_mode == 'none':
        def visgroup_func(_):
            """none = don't add any visgroups."""
            return ()
    elif visgroup_mode == 'choose':
        def visgroup_func(groups):
            """choose = add one random group."""
            return [random.choice(groups)]
    else:
        def visgroup_func(groups):
            """Number = percent chance for each to be added"""
            for group in groups:
                val = random.uniform(0, 100)
                if val <= visgroup_mode:
                    yield group

    # If true, force visgroups to all be used.
    visgroup_force_var = res['forceVisVar', '']

    return (
        temp_id,
        dict(replace_tex),
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        keys,
        outputs,
    )
Beispiel #19
0
def res_import_template(vmf: VMF, coll: Collisions, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance. If no block is used, only
    ID can be specified.
    Options:

    - `ID`: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (`temp_id:vis1,vis2`).
            Either section, or the whole value can be a `$fixup`.
    - `angles`: Override the instance rotation, so it is always rotated this much.
    - `rotation`: Apply the specified rotation before the instance's rotation.
    - `offset`: Offset the template from the instance's position.
    - `force`: a space-seperated list of overrides. If 'white' or 'black' is
             present, the colour of tiles will be overridden. If `invert` is
            added, white/black tiles will be swapped. If a tile size
            (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will
            be switched to that size (if not a floor/ceiling). If 'world' or
            'detail' is present, the brush will be forced to that type.
    - `replace`: A block of template material -> replacement textures.
            This is case insensitive - any texture here will not be altered
            otherwise. If the material starts with a `#`, it is instead a
            list of face IDs separated by spaces. If the result evaluates
            to "", no change occurs. Both can be $fixups (parsed first).
    - `bindOverlay`: Bind overlays in this template to the given surface, and
            bind overlays on a surface to surfaces in this template.
            The value specifies the offset to the surface, where 0 0 0 is the
            floor position. It can also be a block of multiple positions.
    - `alignBindOverlay`: If set, align the bindOverlay offsets to the grid.
    - `keys`/`localkeys`: If set, a brush entity will instead be generated with
            these values. This overrides force world/detail.
            Specially-handled keys:
            - `"origin"`, offset automatically.
            - `"movedir"` on func_movelinear - set a normal surrounded by `<>`,
              this gets replaced with angles.
    - `colorVar`: If this fixup var is set
            to `white` or `black`, that colour will be forced.
            If the value is `<editor>`, the colour will be chosen based on
            the color of the surface for ItemButtonFloor, funnels or
            entry/exit frames.
    - `invertVar`: If this fixup value is true, tile colour will be
            swapped to the opposite of the current force option. This applies
            after colorVar.
    - `visgroup`: Sets how visgrouped parts are handled. Several values are possible:
            - A property block: Each name should match a visgroup, and the
              value should be a block of flags that if true enables that group.
            - 'none' (default): All extra groups are ignored.
            - 'choose': One group is chosen randomly.
            - a number: The percentage chance for each visgroup to be added.
    - `visgroup_force_var`: If set and True, visgroup is ignored and all groups
            are added.
    - `pickerVars`:
            If this is set, the results of colorpickers can be read
            out of the template. The key is the name of the picker, the value
            is the fixup name to write to. The output is either 'white',
            'black' or ''.
    - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    - `senseOffset`: If set, colorpickers and tilesetters will be treated
            as being offset by this amount.
    """
    if res.has_children():
        orig_temp_id = res['id']
    else:
        orig_temp_id = res.value
        res = Property('TemplateBrush', [])

    force = res['force', ''].casefold().split()
    if 'white' in force:
        conf_force_colour = texturing.Portalable.white
    elif 'black' in force:
        conf_force_colour = texturing.Portalable.black
    elif 'invert' in force:
        conf_force_colour = 'INVERT'
    else:
        conf_force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    force_grid: texturing.TileSize | None
    size: texturing.TileSize
    for size in texturing.TileSize:
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    if 'bullseye' in force:
        surf_cat = texturing.GenCat.BULLSEYE
    elif 'special' in force or 'panel' in force:
        surf_cat = texturing.GenCat.PANEL
    else:
        surf_cat = texturing.GenCat.NORMAL

    replace_tex: dict[str, list[str]] = {}
    for prop in res.find_block('replace', or_blank=True):
        replace_tex.setdefault(prop.name, []).append(prop.value)

    if 'replaceBrush' in res:
        LOGGER.warning(
            'replaceBrush command used for template "{}", which is no '
            'longer used.',
            orig_temp_id,
        )
    bind_tile_pos = [
        # So it's the floor block location.
        Vec.from_str(value) - (0, 0, 128)
        for value in res.find_key('BindOverlay', or_blank=True).as_array()
    ]
    align_bind_overlay = res.bool('alignBindOverlay')

    key_values = res.find_block("Keys", or_blank=True)
    if key_values:
        key_block = Property("", [
            key_values,
            res.find_block("LocalKeys", or_blank=True),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [Output.parse(prop) for prop in res.find_children('Outputs')]
    else:
        key_block = None
        outputs = []

    # None = don't add any more.
    visgroup_func: Callable[[Random, list[str]], Iterable[str]] | None = None

    try:  # allow both spellings.
        visgroup_prop = res.find_key('visgroups')
    except NoKeyError:
        visgroup_prop = res.find_key('visgroup', 'none')
    if visgroup_prop.has_children():
        visgroup_instvars = list(visgroup_prop)
    else:
        visgroup_instvars = []
        visgroup_mode = res['visgroup', 'none'].casefold()
        # Generate the function which picks which visgroups to add to the map.
        if visgroup_mode == 'none':
            pass
        elif visgroup_mode == 'choose':

            def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]:
                """choose = add one random group."""
                return [rng.choice(groups)]
        else:
            percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
            if percent > 0.0:

                def visgroup_func(rng: Random,
                                  groups: list[str]) -> Iterable[str]:
                    """Number = percent chance for each to be added"""
                    for group in sorted(groups):
                        if rng.uniform(0, 100) <= percent:
                            yield group

    picker_vars = [(prop.real_name, prop.value)
                   for prop in res.find_children('pickerVars')]
    try:
        ang_override = to_matrix(Angle.from_str(res['angles']))
    except LookupError:
        ang_override = None
    try:
        rotation = to_matrix(Angle.from_str(res['rotation']))
    except LookupError:
        rotation = Matrix()

    offset = res['offset', '0 0 0']
    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']
    if color_var.casefold() == '<editor>':
        color_var = '<editor>'

    # If true, force visgroups to all be used.
    visgroup_force_var = res['forceVisVar', '']

    sense_offset = res.vec('senseOffset')

    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

        # Special case - if blank, just do nothing silently.
        if not temp_id:
            return

        temp_name, visgroups = template_brush.parse_temp_name(temp_id)
        try:
            template = template_brush.get_template(temp_name)
        except template_brush.InvalidTemplateName:
            # If we did lookup, display both forms.
            if temp_id != orig_temp_id:
                LOGGER.warning('{} -> "{}" is not a valid template!',
                               orig_temp_id, temp_name)
            else:
                LOGGER.warning('"{}" is not a valid template!', temp_name)
            # We don't want an error, just quit.
            return

        for vis_flag_block in visgroup_instvars:
            if all(
                    conditions.check_flag(flag, coll, inst)
                    for flag in vis_flag_block):
                visgroups.add(vis_flag_block.real_name)

        force_colour = conf_force_colour
        if color_var == '<editor>':
            # Check traits for the colour it should be.
            traits = instance_traits.get(inst)
            if 'white' in traits:
                force_colour = texturing.Portalable.white
            elif 'black' in traits:
                force_colour = texturing.Portalable.black
            else:
                LOGGER.warning(
                    '"{}": Instance "{}" '
                    "isn't one with inherent color!",
                    temp_id,
                    inst['file'],
                )
        elif color_var:
            color_val = conditions.resolve_value(inst, color_var).casefold()

            if color_val == 'white':
                force_colour = texturing.Portalable.white
            elif color_val == 'black':
                force_colour = texturing.Portalable.black
        # else: no color var

        if srctools.conv_bool(conditions.resolve_value(inst, invert_var)):
            force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour]
        # else: False value, no invert.

        if ang_override is not None:
            orient = ang_override
        else:
            orient = rotation @ Angle.from_str(inst['angles', '0 0 0'])
        origin = conditions.resolve_offset(inst, offset)

        # If this var is set, it forces all to be included.
        if srctools.conv_bool(
                conditions.resolve_value(inst, visgroup_force_var)):
            visgroups.update(template.visgroups)
        elif visgroup_func is not None:
            visgroups.update(
                visgroup_func(
                    rand.seed(b'temp', template.id, origin, orient),
                    list(template.visgroups),
                ))

        LOGGER.debug('Placing template "{}" at {} with visgroups {}',
                     template.id, origin, visgroups)

        temp_data = template_brush.import_template(
            vmf,
            template,
            origin,
            orient,
            targetname=inst['targetname'],
            force_type=force_type,
            add_to_map=True,
            coll=coll,
            additional_visgroups=visgroups,
            bind_tile_pos=bind_tile_pos,
            align_bind=align_bind_overlay,
        )

        if key_block is not None:
            conditions.set_ent_keys(temp_data.detail, inst, key_block)
            br_origin = Vec.from_str(key_block.find_key('keys')['origin'])
            br_origin.localise(origin, orient)
            temp_data.detail['origin'] = br_origin

            move_dir = temp_data.detail['movedir', '']
            if move_dir.startswith('<') and move_dir.endswith('>'):
                move_dir = Vec.from_str(move_dir) @ orient
                temp_data.detail['movedir'] = move_dir.to_angle()

            for out in outputs:
                out = out.copy()
                out.target = conditions.local_name(inst, out.target)
                temp_data.detail.add_out(out)

        template_brush.retexture_template(
            temp_data,
            origin,
            inst.fixup,
            replace_tex,
            force_colour,
            force_grid,
            surf_cat,
            sense_offset,
        )

        for picker_name, picker_var in picker_vars:
            picker_val = temp_data.picker_results.get(picker_name, None)
            if picker_val is not None:
                inst.fixup[picker_var] = picker_val.value
            else:
                inst.fixup[picker_var] = ''

    return place_template
Beispiel #20
0
def res_import_template_setup(res: Property):
    if res.has_children():
        temp_id = res['id']
    else:
        temp_id = res.value
        res = Property('TemplateBrush', [])

    force = res['force', ''].casefold().split()
    if 'white' in force:
        force_colour = texturing.Portalable.white
    elif 'black' in force:
        force_colour = texturing.Portalable.black
    elif 'invert' in force:
        force_colour = 'INVERT'
    else:
        force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    force_grid: Optional[texturing.TileSize]
    size: texturing.TileSize
    for size in texturing.TileSize:
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    if 'bullseye' in force:
        surf_cat = texturing.GenCat.BULLSEYE
    elif 'special' in force or 'panel' in force:
        surf_cat = texturing.GenCat.PANEL
    else:
        surf_cat = texturing.GenCat.NORMAL

    replace_tex = defaultdict(list)
    for prop in res.find_key('replace', []):
        replace_tex[prop.name].append(prop.value)

    if 'replaceBrush' in res:
        LOGGER.warning(
            'replaceBrush command used for template "{}", which is no '
            'longer used.',
            temp_id,
        )
    bind_tile_pos = [
        # So it's the floor block location.
        Vec.from_str(value) - (0, 0, 128)
        for value in res.find_key('BindOverlay', []).as_array()
    ]

    key_values = res.find_key("Keys", [])
    if key_values:
        keys = Property("", [
            key_values,
            res.find_key("LocalKeys", []),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [Output.parse(prop) for prop in res.find_children('Outputs')]
    else:
        keys = None
        outputs = []

    visgroup_func: Callable[[Set[str]], Iterable[str]]

    def visgroup_func(groups):
        """none = don't add any visgroups."""
        return ()

    visgroup_prop = res.find_key('visgroup', 'none')
    if visgroup_prop.has_children():
        visgroup_vars = list(visgroup_prop)
    else:
        visgroup_vars = []
        visgroup_mode = res['visgroup', 'none'].casefold()
        # Generate the function which picks which visgroups to add to the map.
        if visgroup_mode == 'none':
            pass
        elif visgroup_mode == 'choose':

            def visgroup_func(groups):
                """choose = add one random group."""
                return [random.choice(groups)]
        else:
            percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
            if percent > 0.0:

                def visgroup_func(groups):
                    """Number = percent chance for each to be added"""
                    for group in groups:
                        val = random.uniform(0, 100)
                        if val <= percent:
                            yield group

    picker_vars = [(prop.real_name, prop.value)
                   for prop in res.find_children('pickerVars')]

    return (
        temp_id,
        dict(replace_tex),
        force_colour,
        force_grid,
        force_type,
        surf_cat,
        bind_tile_pos,
        res['invertVar', ''],
        res['colorVar', ''],
        visgroup_func,
        # If true, force visgroups to all be used.
        res['forceVisVar', ''],
        visgroup_vars,
        keys,
        picker_vars,
        outputs,
        res.vec('senseOffset'),
    )
Beispiel #21
0
def res_insert_overlay(vmf: VMF, res: Property):
    """Use a template to insert one or more overlays on a surface.

    Options:

    - ID: The template ID. Brushes will be ignored.
    - Replace: old -> new material replacements.
    - Face_pos: The offset of the brush face.
    - Normal: The direction of the brush face.
    - Offset: An offset to move the overlays by.
    """
    orig_temp_id = res['id'].casefold()

    face_str = res['face_pos', '0 0 -64']
    orig_norm = Vec.from_str(res['normal', '0 0 1'])

    replace_tex: dict[str, list[str]] = {}
    for prop in res.find_children('replace'):
        replace_tex.setdefault(prop.name.replace('\\', '/'),
                               []).append(prop.value)

    offset = Vec.from_str(res['offset', '0 0 0'])

    def insert_over(inst: Entity) -> None:
        """Apply the result."""
        temp_id = inst.fixup.substitute(orig_temp_id)

        origin = Vec.from_str(inst['origin'])
        angles = Angle.from_str(inst['angles', '0 0 0'])

        face_pos = conditions.resolve_offset(inst, face_str)
        normal = orig_norm @ angles

        # Don't make offset change the face_pos value..
        origin += offset @ angles

        for axis, norm in enumerate(normal):
            # Align to the center of the block grid. The normal direction is
            # already correct.
            if norm == 0:
                face_pos[axis] = face_pos[axis] // 128 * 128 + 64

        # Shift so that the user perceives the position as the pos of the face
        # itself.
        face_pos -= 64 * normal

        try:
            tiledef = tiling.TILES[face_pos.as_tuple(), normal.as_tuple()]
        except KeyError:
            LOGGER.warning(
                'Overlay brush position is not valid: {}',
                face_pos,
            )
            return

        temp = template_brush.import_template(
            vmf,
            temp_id,
            origin,
            angles,
            targetname=inst['targetname', ''],
            force_type=TEMP_TYPES.detail,
        )

        for over in temp.overlay:
            pos = Vec.from_str(over['basisorigin'])
            mat = over['material']
            try:
                replace = replace_tex[mat.casefold().replace('\\', '/')]
            except KeyError:
                pass
            else:
                mat = rand.seed(b'temp_over', temp_id, pos).choice(replace)

            if mat[:1] == '$':
                mat = inst.fixup[mat]
            if mat.startswith('<') or mat.endswith('>'):
                # Lookup in the texture data.
                gen, mat = texturing.parse_name(mat[1:-1])
                mat = gen.get(pos, mat)
            over['material'] = mat
            tiledef.bind_overlay(over)

        # Wipe the brushes from the map.
        if temp.detail is not None:
            temp.detail.remove()
            LOGGER.info(
                'Overlay template "{}" could set keep_brushes=0.',
                temp_id,
            )

    return insert_over
Beispiel #22
0
    def modify(self, fsys: FileSystem, props: Property,
               source: str) -> 'ItemVariant':
        """Apply a config to this item variant.

        This produces a copy with various modifications - switching
        out palette or instance values, changing the config, etc.
        """
        if 'config' in props:
            # Item.parse() has resolved this to the actual config.
            vbsp_config = get_config(
                props,
                fsys,
                'items',
                pak_id=fsys.path,
            )
        else:
            vbsp_config = self.vbsp_config.copy()

        if 'replace' in props:
            # Replace property values in the config via regex.
            replace_vals = [(re.compile(prop.real_name,
                                        re.IGNORECASE), prop.value)
                            for prop in props.find_children('Replace')]
            for prop in vbsp_config.iter_tree():
                for regex, sub in replace_vals:
                    prop.name = regex.sub(sub, prop.real_name)
                    prop.value = regex.sub(sub, prop.value)

        vbsp_config += list(
            get_config(
                props,
                fsys,
                'items',
                prop_name='append',
                pak_id=fsys.path,
            ))

        if 'description' in props:
            desc = desc_parse(props, source)
        else:
            desc = self.desc.copy()

        if 'appenddesc' in props:
            desc = tkMarkdown.join(
                desc,
                desc_parse(props, source, prop_name='appenddesc'),
            )

        if 'authors' in props:
            authors = sep_values(props['authors', ''])
        else:
            authors = self.authors

        if 'tags' in props:
            tags = sep_values(props['tags', ''])
        else:
            tags = self.tags.copy()

        variant = ItemVariant(
            self.editor,
            vbsp_config,
            self.editor_extra.copy(),
            authors=authors,
            tags=tags,
            desc=desc,
            icons=self.icons.copy(),
            ent_count=props['ent_count', self.ent_count],
            url=props['url', self.url],
            all_name=self.all_name,
            all_icon=self.all_icon,
            source='{} from {}'.format(source, self.source),
        )
        [variant.editor] = variant._modify_editoritems(
            props,
            [variant.editor],
            source,
            is_extra=False,
        )

        if 'extra' in props:
            variant.editor_extra = variant._modify_editoritems(
                props.find_key('extra'),
                variant.editor_extra,
                source,
                is_extra=True)

        return variant