Exemple #1
0
def res_set_faith_setup(res: Property) -> tuple:
    temp_name = res['template', '']

    if temp_name:
        template = template_brush.get_template(temp_name)
    else:
        template = None

    return (
        template,
        res.vec('offset'),
    )
Exemple #2
0
def res_piston_plat_setup(res: Property):
    # Allow reading instances direct from the ID.
    # But use direct ones first.
    item_id = res['itemid', None]
    inst = {}
    for name in INST_NAMES:
        if name in res:
            lookup = res[name]
            if lookup == '':
                # Special case, allow blank for no instance.
                inst[name] = ''
                continue
        elif item_id is not None:
            lookup = '<{}:bee2_pist_{}>'.format(item_id, name)
        else:
            raise ValueError('No "{}" specified!'.format(name))
        inst[name] = resolve_single(lookup, error=True)

    template = template_brush.get_template(res['template'])

    visgroup_names = [
        res['visgroup_1', 'pist_1'],
        res['visgroup_2', 'pist_2'],
        res['visgroup_3', 'pist_3'],
        res['visgroup_top', 'pist_4'],
    ]

    return (
        template,
        visgroup_names,
        inst,
        res.bool('has_dn_fizz'),
        res['auto_var', ''],
        res['color_var', ''],
        res['source_ent', ''],
        res['snd_start', ''],
        res['snd_loop', ''],
        res['snd_stop', ''],
    )
Exemple #3
0
def res_piston_plat_setup(res: Property):
    # Allow reading instances direct from the ID.
    # But use direct ones first.
    item_id = res['itemid', None]
    inst = {}
    for name in INST_NAMES:
        if name in res:
            lookup = res[name]
            if lookup == '':
                # Special case, allow blank for no instance.
                inst[name] = ''
                continue
        elif item_id is not None:
            lookup = '<{}:bee2_pist_{}>'.format(item_id, name)
        else:
            raise ValueError('No "{}" specified!'.format(name))
        inst[name] = resolve_single(lookup, error=True)

    template = template_brush.get_template(res['template'])

    visgroup_names = [
        res['visgroup_1', 'pist_1'],
        res['visgroup_2', 'pist_2'],
        res['visgroup_3', 'pist_3'],
        res['visgroup_top', 'pist_4'],
    ]

    return (
        template,
        visgroup_names,
        inst,
        res['auto_var', ''],
        res['color_var', ''],
        res['source_ent', ''],
        res['snd_start', ''],
        res['snd_loop', ''],
        res['snd_stop', ''],
    )
Exemple #4
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    Options:  
    - ID: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (temp_id:vis1,vis2)
    - 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.
    - replaceBrush: The position of a brush to replace (0 0 0=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:
            - Pos: The position to replace.
            - additionalIDs: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - removeBrush: If true, the original brush will not be removed.
            - transferOverlay: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be an instvar).
    - 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. If 'none' (default),
            they are ignored. If 'choose', one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - visgroup_force_var: If set and True, visgroup is ignored and all groups
            are added.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
    ) = res.value
    temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):

        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    temp_name, visgroups = template_brush.parse_temp_name(temp_id)
    try:
        template = template_brush.get_template(temp_name)
    except template_brush.InvalidTemplateName:
        # The template map is read in after setup is performed, so
        # it must be checked here!
        # We don't want an error, just quit
        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)
        return

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
    )
Exemple #5
0
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

    The texture is assumed to match plasticwall004a's shape.
    """
    template = template_brush.get_template(temp_name)
    temp_world, temp_detail, temp_over = template.visgrouped()
    try:
        [beam_template] = temp_world + temp_detail  # type: Solid
    except ValueError:
        raise ValueError('Bad Glass Floorbeam template!')

    # Grab the 'end' side, which we move around.
    for side in beam_template.sides:
        if side.normal() == (-1, 0, 0):
            beam_end_face = side
            break
    else:
        raise ValueError('Not aligned to world...')

    separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1
    separation *= 128

    # First we want to find all the groups of contiguous glass sections.
    # This is a mapping from some glass piece to its group list.
    groups = {}

    for (origin, normal), barr_type in BARRIERS.items():
        # Grating doesn't use it.
        if barr_type is not BarrierType.GLASS:
            continue

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

        groups[pos.as_tuple()] = [pos]

    # Loop over every pos and check in the +x/y directions for another glass
    # piece. If there, merge the two lists and set every pos in the group to
    # point to the new list.
    # Once done, every unique list = a group.

    for pos_tup in groups.keys():
        pos = Vec(pos_tup)
        for off in ((128, 0, 0), (0, 128, 0)):
            neighbour = (pos + off).as_tuple()
            if neighbour in groups:
                our_group = groups[pos_tup]
                neigh_group = groups[neighbour]
                if our_group is neigh_group:
                    continue

                # Now merge the two lists. We then need to update all dict locs
                # to point to the new list.

                if len(neigh_group) > len(our_group):
                    small_group, large_group = our_group, neigh_group
                else:
                    small_group, large_group = neigh_group, our_group

                large_group.extend(small_group)
                for pos in small_group:
                    groups[pos.as_tuple()] = large_group

    # Remove duplicates objects by using the ID as key..
    groups = list({
        id(group): group
        for group in groups.values()
    }.values())

    # Side -> u, v or None

    for group in groups:

        bbox_min, bbox_max = Vec.bbox(group)
        dimensions = bbox_max - bbox_min
        LOGGER.info('Size = {}', dimensions)

        # Our beams align to the smallest axis.
        if dimensions.y > dimensions.x:
            beam_ax = 'x'
            side_ax = 'y'
            rot = Vec(0, 0, 0)
        else:
            beam_ax = 'y'
            side_ax = 'x'
            rot = Vec(0, 90, 0)

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams = {}  # type: Dict[int, Tuple[int, int]]

        # Add 128 so the first pos isn't a beam.
        offset = bbox_min[side_ax] + 128

        for pos in group:
            side_off = pos[side_ax]
            beam_off = pos[beam_ax]
            # Skip over non-'sep' positions..
            if (side_off - offset) % separation != 0:
                continue

            try:
                min_pos, max_pos = beams[side_off]
            except KeyError:
                beams[side_off] = beam_off, beam_off
            else:
                beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off)

        detail = vmf.create_ent('func_detail')

        for side_off, (min_off, max_off) in beams.items():
            for min_pos, max_pos in beam_hole_split(
                beam_ax,
                Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min),
                Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min),
            ):

                if min_pos[beam_ax] >= max_pos[beam_ax]:
                    raise ValueError(min_pos, max_pos, beam_ax)

                # Make the beam.
                # Grab the end face and snap to the length we want.
                beam_end_off = max_pos[beam_ax] - min_pos[beam_ax]
                assert beam_end_off > 0, beam_end_off
                for plane in beam_end_face.planes:
                    plane.x = beam_end_off

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
Exemple #6
0
def make_barriers(vmf: VMF, get_tex: Callable[[str], str]):
    """Make barrier entities. get_tex is vbsp.get_tex."""
    glass_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "glass_template")
    )
    grate_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "grating_template")
    )
    # Avoid error without this package.
    if HOLES:
        # Grab the template solids we need.
        hole_temp = template_brush.get_template(
            vbsp_options.get(str, 'glass_hole_temp')
        )
        hole_world, hole_detail, _ = hole_temp.visgrouped({'small'})
        hole_temp_small = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large'})
        hole_temp_large = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'})
        hole_temp_corner = hole_world + hole_detail
    else:
        hole_temp_small = hole_temp_large = hole_temp_corner = None

    floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp')

    if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False):
        # Merge together these existing filters in global_pti_ents
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_multi',
            filtertype=0,
            negated=0,
            filter01='@not_pellet',
            filter02='@not_paint_bomb',
        )
    else:
        # Just skip paint bombs.
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_activator_class',
            negated=1,
            filterclass='prop_paint_bomb',
        )

    # Group the positions by planes in each orientation.
    # This makes them 2D grids which we can optimise.
    # (normal_dist, positive_axis, type) -> [(x, y)]
    slices = defaultdict(set)  # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]]
    # We have this on the 32-grid so we can cut squares for holes.

    for (origin, normal), barr_type in BARRIERS.items():
        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),  # distance from origin to this plane.
            normal[norm_axis] > 0,
            barr_type,
        ]
        for u_off in [-48, -16, 16, 48]:
            for v_off in [-48, -16, 16, 48]:
                slice_plane.add((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

    # Remove pane sections where the holes are. We then generate those with
    # templates for slanted parts.
    for (origin, normal), hole_type in HOLES.items():
        barr_type = BARRIERS[origin, normal]

        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),
            normal[norm_axis] > 0,
            barr_type,
        ]
        if hole_type is HoleType.LARGE:
            offsets = (-80, -48, -16, 16, 48, 80)
            hole_temp = hole_temp_large.copy()
        else:
            offsets = (-16, 16)
            hole_temp = hole_temp_small.copy()
        for u_off in offsets:
            for v_off in offsets:
                # Skip the corners on large holes.
                # Those aren't actually used, so skip them. That way
                # we can have them diagonally or  without glass in the corner.
                if u_off in (-80, 80) and v_off in (-80, 80):
                    continue

                slice_plane.discard((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

        # Now generate the curved brushwork.

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
            front_mat = get_tex('special.glass')
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
            front_mat = get_tex('special.grating')
        else:
            raise NotImplementedError

        angles = normal.to_angle(0)
        # Angle corresponding to the brush, for the corners.
        angle_list = [angles] * len(hole_temp)

        # This is a tricky bit. Two large templates would collide
        # diagonally,
        # so chop off the corners, then put them back only if there's not
        # one diagonally.
        if hole_type is HoleType.LARGE:
            for roll in (0, 90, 180, 270):
                corn_angles = angles.copy()
                corn_angles.z = roll
                hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles)
                diag_type = HOLES.get(
                    (hole_off.as_tuple(), normal.as_tuple()),
                    None,
                )
                if diag_type is not HoleType.LARGE:
                    hole_temp += hole_temp_corner
                    angle_list += [corn_angles] * len(hole_temp_corner)

        def solid_pane_func(off1, off2, mat):
            """Given the two thicknesses, produce the curved hole from the template."""
            off_min = min(off1, off2)
            off_max = max(off1, off2)
            new_brushes = [
                brush.copy(vmf_file=vmf)
                for brush in hole_temp
            ]

            for brush, br_angles in zip(new_brushes, angle_list):
                for face in brush.sides:
                    face.mat = mat
                    f_norm = face.normal()
                    if f_norm.x == 1:
                        face.translate(Vec(x=4 - off_max))
                        # face.mat = 'min'
                    elif f_norm.x == -1:
                        face.translate(Vec(x=-4 - off_min))
                        # face.mat = 'max'
                    face.localise(origin, br_angles)
            return new_brushes

        make_glass_grating(
            vmf,
            origin,
            normal,
            barr_type,
            front_temp,
            front_mat,
            solid_pane_func,
        )

    for (plane_pos, is_pos, barr_type), pos_slice in slices.items():
        plane_pos = Vec(plane_pos)
        norm_axis = plane_pos.axis()
        normal = Vec.with_axes(norm_axis, 1 if is_pos else -1)

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
            front_mat = get_tex('special.glass')
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
            front_mat = get_tex('special.grating')
        else:
            raise NotImplementedError

        u_axis, v_axis = Vec.INV_AXIS[norm_axis]

        for min_u, min_v, max_u, max_v in grid_optimise(dict.fromkeys(pos_slice, True)):
            # These are two points in the origin plane, at the borders.
            pos_min = Vec.with_axes(
                norm_axis, plane_pos,
                u_axis, min_u * 32,
                v_axis, min_v * 32,
            )
            pos_max = Vec.with_axes(
                norm_axis, plane_pos,
                u_axis, max_u * 32 + 32,
                v_axis, max_v * 32 + 32,
            )

            def solid_pane_func(pos1, pos2, mat):
                """Make the solid brush."""
                return [vmf.make_prism(
                    pos_min + normal * (64.0 - pos1),
                    pos_max + normal * (64.0 - pos2),
                    mat=mat,
                ).solid]

            make_glass_grating(
                vmf,
                (pos_min + pos_max)/2,
                normal,
                barr_type,
                front_temp,
                front_mat,
                solid_pane_func,
            )

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
Exemple #7
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    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`.
    - `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
            face ID.
    - `replaceBrush`: The position of a brush to replace (`0 0 0`=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:

            - `Pos`: The position to replace.
            - `additionalIDs`: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - `removeBrush`: If true, the original brush will not be removed.
            - `transferOverlay`: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be a `$fixup`).
    - `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. If `none` (default),
            they are ignored. If `choose`, one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - `visgroup_force_var`: If set and True, visgroup is ignored and all groups
            are added.
    - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
        outputs,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    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

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

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

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
        # Don't allow clumping if using custom keyvalues - then it won't be edited.
        no_clumping=key_block is not None,
    )
Exemple #8
0
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

    The texture is assumed to match plasticwall004a's shape.
    """
    template = template_brush.get_template(temp_name)
    temp_world, temp_detail, temp_over = template.visgrouped()
    try:
        [beam_template] = temp_world + temp_detail  # type: Solid
    except ValueError:
        raise ValueError('Bad Glass Floorbeam template!')

    # Grab the 'end' side, which we move around.
    for side in beam_template.sides:
        if side.normal() == (-1, 0, 0):
            beam_end_face = side
            break
    else:
        raise ValueError('Not aligned to world...')

    separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1
    separation *= 128

    # First we want to find all the groups of contiguous glass sections.
    # This is a mapping from some glass piece to its group list.
    groups = {}

    for (origin, normal), barr_type in BARRIERS.items():
        # Grating doesn't use it.
        if barr_type is not BarrierType.GLASS:
            continue

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

        groups[pos.as_tuple()] = [pos]

    # Loop over every pos and check in the +x/y directions for another glass
    # piece. If there, merge the two lists and set every pos in the group to
    # point to the new list.
    # Once done, every unique list = a group.

    for pos_tup in groups.keys():
        pos = Vec(pos_tup)
        for off in ((128, 0, 0), (0, 128, 0)):
            neighbour = (pos + off).as_tuple()
            if neighbour in groups:
                our_group = groups[pos_tup]
                neigh_group = groups[neighbour]
                if our_group is neigh_group:
                    continue

                # Now merge the two lists. We then need to update all dict locs
                # to point to the new list.

                if len(neigh_group) > len(our_group):
                    small_group, large_group = our_group, neigh_group
                else:
                    small_group, large_group = neigh_group, our_group

                large_group.extend(small_group)
                for pos in small_group:
                    groups[pos.as_tuple()] = large_group

    # Remove duplicates objects by using the ID as key..
    groups = list({id(group): group for group in groups.values()}.values())

    # Side -> u, v or None

    for group in groups:

        bbox_min, bbox_max = Vec.bbox(group)
        dimensions = bbox_max - bbox_min
        LOGGER.info('Size = {}', dimensions)

        # Our beams align to the smallest axis.
        if dimensions.y > dimensions.x:
            beam_ax = 'x'
            side_ax = 'y'
            rot = Vec(0, 0, 0)
        else:
            beam_ax = 'y'
            side_ax = 'x'
            rot = Vec(0, 90, 0)

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams = {}  # type: Dict[int, Tuple[int, int]]

        # Add 128 so the first pos isn't a beam.
        offset = bbox_min[side_ax] + 128

        for pos in group:
            side_off = pos[side_ax]
            beam_off = pos[beam_ax]
            # Skip over non-'sep' positions..
            if (side_off - offset) % separation != 0:
                continue

            try:
                min_pos, max_pos = beams[side_off]
            except KeyError:
                beams[side_off] = beam_off, beam_off
            else:
                beams[side_off] = min(min_pos,
                                      beam_off), max(max_pos, beam_off)

        detail = vmf.create_ent('func_detail')

        for side_off, (min_off, max_off) in beams.items():
            for min_pos, max_pos in beam_hole_split(
                    beam_ax,
                    Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z',
                                  bbox_min),
                    Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z',
                                  bbox_min),
            ):

                if min_pos[beam_ax] >= max_pos[beam_ax]:
                    raise ValueError(min_pos, max_pos, beam_ax)

                # Make the beam.
                # Grab the end face and snap to the length we want.
                beam_end_off = max_pos[beam_ax] - min_pos[beam_ax]
                assert beam_end_off > 0, beam_end_off
                for plane in beam_end_face.planes:
                    plane.x = beam_end_off

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
Exemple #9
0
def make_barriers(vmf: VMF):
    """Make barrier entities. get_tex is vbsp.get_tex."""
    glass_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "glass_template"))
    grate_temp = template_brush.get_scaling_template(
        vbsp_options.get(str, "grating_template"))
    # Avoid error without this package.
    if HOLES:
        # Grab the template solids we need.
        hole_temp = template_brush.get_template(
            vbsp_options.get(str, 'glass_hole_temp'))
        hole_world, hole_detail, _ = hole_temp.visgrouped({'small'})
        hole_temp_small = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large'})
        hole_temp_large = hole_world + hole_detail
        hole_world, hole_detail, _ = hole_temp.visgrouped({'large_corner'})
        hole_temp_corner = hole_world + hole_detail
    else:
        hole_temp_small = hole_temp_large = hole_temp_corner = None

    floorbeam_temp = vbsp_options.get(str, 'glass_floorbeam_temp')

    if vbsp_options.get_itemconf('BEE_PELLET:PelletGrating', False):
        # Merge together these existing filters in global_pti_ents
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_multi',
            filtertype=0,
            negated=0,
            filter01='@not_pellet',
            filter02='@not_paint_bomb',
        )
    else:
        # Just skip paint bombs.
        vmf.create_ent(
            origin=vbsp_options.get(Vec, 'global_pti_ents_loc'),
            targetname='@grating_filter',
            classname='filter_activator_class',
            negated=1,
            filterclass='prop_paint_bomb',
        )

    # Group the positions by planes in each orientation.
    # This makes them 2D grids which we can optimise.
    # (normal_dist, positive_axis, type) -> [(x, y)]
    slices = defaultdict(
        set
    )  # type: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Set[Tuple[float, float]]]
    # We have this on the 32-grid so we can cut squares for holes.

    for (origin, normal), barr_type in BARRIERS.items():
        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[
            norm_pos.as_tuple(),  # distance from origin to this plane.
            normal[norm_axis] > 0, barr_type, ]
        for u_off in [-48, -16, 16, 48]:
            for v_off in [-48, -16, 16, 48]:
                slice_plane.add((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

    # Remove pane sections where the holes are. We then generate those with
    # templates for slanted parts.
    for (origin, normal), hole_type in HOLES.items():
        barr_type = BARRIERS[origin, normal]

        origin = Vec(origin)
        normal = Vec(normal)
        norm_axis = normal.axis()
        u, v = origin.other_axes(norm_axis)
        norm_pos = Vec.with_axes(norm_axis, origin)
        slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0,
                             barr_type, ]
        if hole_type is HoleType.LARGE:
            offsets = (-80, -48, -16, 16, 48, 80)
            hole_temp = hole_temp_large.copy()
        else:
            offsets = (-16, 16)
            hole_temp = hole_temp_small.copy()
        for u_off in offsets:
            for v_off in offsets:
                # Skip the corners on large holes.
                # Those aren't actually used, so skip them. That way
                # we can have them diagonally or  without glass in the corner.
                if u_off in (-80, 80) and v_off in (-80, 80):
                    continue

                slice_plane.discard((
                    (u + u_off) // 32,
                    (v + v_off) // 32,
                ))

        # Now generate the curved brushwork.

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        else:
            raise NotImplementedError

        angles = normal.to_angle(0)
        # Angle corresponding to the brush, for the corners.
        angle_list = [angles] * len(hole_temp)

        # This is a tricky bit. Two large templates would collide
        # diagonally,
        # so chop off the corners, then put them back only if there's not
        # one diagonally.
        if hole_type is HoleType.LARGE:
            for roll in (0, 90, 180, 270):
                corn_angles = angles.copy()
                corn_angles.z = roll
                hole_off = origin + Vec(y=128, z=128).rotate(*corn_angles)
                diag_type = HOLES.get(
                    (hole_off.as_tuple(), normal.as_tuple()),
                    None,
                )
                if diag_type is not HoleType.LARGE:
                    hole_temp += hole_temp_corner
                    angle_list += [corn_angles] * len(hole_temp_corner)

        def solid_pane_func(off1, off2, mat):
            """Given the two thicknesses, produce the curved hole from the template."""
            off_min = min(off1, off2)
            off_max = max(off1, off2)
            new_brushes = [brush.copy(vmf_file=vmf) for brush in hole_temp]

            for brush, br_angles in zip(new_brushes, angle_list):
                for face in brush.sides:
                    face.mat = mat
                    f_norm = face.normal()
                    if f_norm.x == 1:
                        face.translate(Vec(x=4 - off_max))
                        # face.mat = 'min'
                    elif f_norm.x == -1:
                        face.translate(Vec(x=-4 - off_min))
                        # face.mat = 'max'
                    face.localise(origin, br_angles)
            return new_brushes

        make_glass_grating(
            vmf,
            origin,
            normal,
            barr_type,
            front_temp,
            solid_pane_func,
        )

    for (plane_pos, is_pos, barr_type), pos_slice in slices.items():
        plane_pos = Vec(plane_pos)
        norm_axis = plane_pos.axis()
        normal = Vec.with_axes(norm_axis, 1 if is_pos else -1)

        if barr_type is BarrierType.GLASS:
            front_temp = glass_temp
        elif barr_type is BarrierType.GRATING:
            front_temp = grate_temp
        else:
            raise NotImplementedError

        u_axis, v_axis = Vec.INV_AXIS[norm_axis]

        for min_u, min_v, max_u, max_v in grid_optimise(
                dict.fromkeys(pos_slice, True)):
            # These are two points in the origin plane, at the borders.
            pos_min = Vec.with_axes(
                norm_axis,
                plane_pos,
                u_axis,
                min_u * 32,
                v_axis,
                min_v * 32,
            )
            pos_max = Vec.with_axes(
                norm_axis,
                plane_pos,
                u_axis,
                max_u * 32 + 32,
                v_axis,
                max_v * 32 + 32,
            )

            def solid_pane_func(pos1, pos2, mat):
                """Make the solid brush."""
                return [
                    vmf.make_prism(
                        pos_min + normal * (64.0 - pos1),
                        pos_max + normal * (64.0 - pos2),
                        mat=mat,
                    ).solid
                ]

            make_glass_grating(
                vmf,
                (pos_min + pos_max) / 2,
                normal,
                barr_type,
                front_temp,
                solid_pane_func,
            )

    if floorbeam_temp:
        LOGGER.info('Adding Glass floor beams...')
        add_glass_floorbeams(vmf, floorbeam_temp)
        LOGGER.info('Done!')
Exemple #10
0
def res_import_template(inst: Entity, 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`.
    - `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.
    - `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.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        surf_cat,
        bind_tile_pos,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        visgroup_instvars,
        key_block,
        picker_vars,
        outputs,
        sense_offset,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    # 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, inst) for flag in vis_flag_block):
            visgroups.add(vis_flag_block.real_name)

    if color_var.casefold() == '<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[force_colour]
    # else: False value, no invert.

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
        bind_tile_pos=bind_tile_pos,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

        for out in outputs:  # type: Output
            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,
        )  # type: Optional[texturing.Portalable]
        if picker_val is not None:
            inst.fixup[picker_var] = picker_val.value
        else:
            inst.fixup[picker_var] = ''
Exemple #11
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    Options:  
    - ID: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (temp_id:vis1,vis2)
    - 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
            face ID.
    - replaceBrush: The position of a brush to replace (0 0 0=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:
            - Pos: The position to replace.
            - additionalIDs: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - removeBrush: If true, the original brush will not be removed.
            - transferOverlay: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be an instvar).
    - 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. If 'none' (default),
            they are ignored. If 'choose', one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - visgroup_force_var: If set and True, visgroup is ignored and all groups
            are added.
    - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
        outputs,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    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

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
    )

    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, angles)
        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).rotate(*angles)
            temp_data.detail['movedir'] = move_dir.to_angle()

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

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
        # Don't allow clumping if using custom keyvalues - then it won't be edited.
        no_clumping=key_block is not None,
    )