예제 #1
0
def test_fixup_substitution() -> None:
    """Test Entity.fixup.substitute()."""
    ent = VMF().create_ent('any')
    ent.fixup['$var'] = 'out'

    assert ent.fixup.substitute('no var 1234') == 'no var 1234'
    assert ent.fixup.substitute('$var') == 'out'
    assert ent.fixup.substitute('prefix$varsuffix') == 'prefixoutsuffix'
    with raises(KeyError) as exc:
        ent.fixup.substitute('$notPRESent')
    assert exc.value.args == ('$notPRESent', )

    assert ent.fixup.substitute('blah_$notPresent45:more', 'def') == 'blah_def:more'
    # Potential edge case - 1-long var.
    assert ent.fixup.substitute('blah$x:more', 'def') == 'blahdef:more'
    # Blank and fully numeric vars are not allowed.
    assert ent.fixup.substitute('blah$:more$45', 'def') == 'blah$:more$45'

    ent.fixup['$variable'] = 'long'
    # Value changed, should have remade the regex.
    assert ent.fixup.substitute('X=$variable') == 'X=long'

    # Check common prefixes don't break it.
    assert ent.fixup.substitute('_$var_and_$variable_') == '_out_and_long_'

    ent.fixup.update({'x': 'dunder'})
    assert ent.fixup.substitute('__$x__') == '__dunder__'

    # Ensure regex chars in the var are escaped.
    ent.fixup['$a_var_with*_[]_regex'] = 'ignored'
    assert ent.fixup.substitute('V = $a_var_with*_[]_regex') == 'V = ignored'
예제 #2
0
def save_connectionpoint(item: Item, vmf: VMF) -> None:
    """Write connectionpoints to a VMF."""
    for side, points in item.antline_points.items():
        yaw = side.yaw
        inv_orient = Matrix.from_yaw(-yaw)
        for point in points:
            ant_pos = Vec(point.pos.x, -point.pos.y, -64)
            sign_pos = Vec(point.sign_off.x, -point.sign_off.y, -64)

            offset = (ant_pos - sign_pos) @ inv_orient
            try:
                skin = CONN_OFFSET_TO_SKIN[offset.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'Pos=({}), Sign=({}) -> ({}) is not a valid offset for signs!',
                    point.pos, point.sign_off, offset)
                continue
            pos: Vec = round((ant_pos + sign_pos) / 2.0 * 16.0, 0)

            vmf.create_ent(
                'bee2_editor_connectionpoint',
                origin=Vec(pos.x - 56, pos.y + 56, -64),
                angles=f'0 {yaw} 0',
                skin=skin,
                priority=point.priority,
                group_id='' if point.group is None else point.group,
            )
예제 #3
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(
                        'Too many closing brackets after {} ents'.format(
                            len(vmf.entities)))
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf
            else:
                # Line is of the form <"key" "val">
                key, value = line.split(b'" "')
                decoded_key = key[1:].decode('ascii')
                decoded_val = value[:-1].decode('ascii')
                if 27 in value:
                    # All outputs use the comma_sep, so we can ID them.
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_val)))
                else:
                    # Normal keyvalue.
                    cur_ent[decoded_key] = decoded_val

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
예제 #4
0
def save_embeddedvoxel(item: Item, vmf: VMF) -> None:
    """Save embedded voxel volumes."""
    for bbox_min, bbox_max in bounding_boxes(item.embed_voxels):
        vmf.create_ent('bee2_editor_embeddedvoxel').solids.append(
            vmf.make_prism(
                Vec(bbox_min) * 128 + (-64.0, -64.0, -192.0),
                Vec(bbox_max) * 128 + (+64.0, +64.0, -64.0),
                # Entirely ignored, but makes it easier to distinguish.
                'tools/toolshint',
            ).solid)
예제 #5
0
def save(item: Item) -> VMF:
    """Export out relevant item options into a VMF."""
    vmf = VMF()
    with logger.context(item.id):
        for func in SAVE_FUNCS:
            func(item, vmf)
    return vmf
예제 #6
0
def save_occupiedvoxel(item: Item, vmf: VMF) -> None:
    """Save occupied voxel volumes."""
    for voxel in item.occupy_voxels:
        pos = Vec(voxel.pos) * 128

        if voxel.subpos is not None:
            pos += Vec(voxel.subpos) * 32 - (48, 48, 48)
            p1 = pos - (16.0, 16.0, 16.0)
            p2 = pos + (16.0, 16.0, 16.0)
            norm_dist = 32.0 - 4.0
        else:
            p1 = pos - (64.0, 64.0, 64.0)
            p2 = pos + (64.0, 64.0, 64.0)
            norm_dist = 128.0 - 4.0

        if voxel.normal is not None:
            for axis in ['x', 'y', 'z']:
                val = getattr(voxel.normal, axis)
                if val == +1:
                    p2[axis] -= norm_dist
                elif val == -1:
                    p1[axis] += norm_dist

        if voxel.against is not None:
            against = str(voxel.against).replace('COLLIDE_', '')
        else:
            against = ''

        vmf.create_ent(
            'bee2_editor_occupiedvoxel',
            coll_type=str(voxel.type).replace('COLLIDE_', ''),
            coll_against=against,
        ).solids.append(
            vmf.make_prism(
                p1,
                p2,
                # Use clip for voxels, invisible for normals.
                # Entirely ignored, but makes it easier to use.
                'tools/toolsclip'
                if voxel.normal is None else 'tools/toolsinvisible',
            ).solid)
예제 #7
0
def test_fixup_basic() -> None:
    """Test ent.fixup functionality."""
    obj = object()  # Arbitrary example object.

    ent = VMF().create_ent('any')
    assert len(ent.fixup) == 0
    assert list(ent.fixup) == []
    assert list(ent.fixup.keys()) == []
    assert list(ent.fixup.values()) == []
    assert list(ent.fixup.items()) == []

    ent.fixup['$test'] = 'hello'

    assert ent.fixup['$test'] == 'hello', 'Standard'
    assert ent.fixup['$teSt'] == 'hello', 'Case-insentive'
    assert ent.fixup['test'] == 'hello', 'No $ sign is allowed'

    assert ent.fixup['$notPresent'] == '', 'Defaults to ""'
    assert ent.fixup['$test', 'default'] == 'hello', 'Unused default.'
    assert ent.fixup['not_here', 'default'] == 'default', 'Used default.'
    assert ent.fixup['not_here', obj] is obj, 'Default can be anything.'

    assert ent.fixup.get('$notPresent') == '', 'Defaults to ""'
    assert ent.fixup.get('$test', 'default') == 'hello', 'Unused default.'
    assert ent.fixup.get('not_here', obj) is obj, 'Default can be anything.'

    ent.fixup['$VALUE'] = 42  # Integer, converted to string.
    assert ent.fixup['$value'] == '42'
    ent.fixup['$value'] = 45.75
    assert ent.fixup['$value'] == '45.75'
    ent.fixup['$true'] = True  # Special case, bools become 1/0.
    assert ent.fixup['true'] == '1'
    ent.fixup['$false'] = False
    assert ent.fixup['$false'] == '0'

    # Order not guaranteed.
    assert len(ent.fixup) == 4
    assert set(ent.fixup) == {'test', 'value', 'true', 'false'}
    assert set(ent.fixup.keys()) == {'test', 'value', 'true', 'false'}
    assert set(ent.fixup.values()) == {'hello', '1', '0', '45.75'}
    assert set(ent.fixup.items()) == {
        ('test', 'hello'),
        ('value', '45.75'),
        ('true', '1'),
        ('false', '0')
    }
    # Keys/values/items should have the same order.
    assert list(ent.fixup.items()) == list(zip(ent.fixup.keys(), ent.fixup.values()))
예제 #8
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
                continue
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(f'Too many closing brackets after'
                                     f' {len(vmf.entities)} ents!')
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
                continue
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf

            if cur_ent is None:
                raise ValueError("Keyvalue outside brackets!")

            # Line is of the form <"key" "val">
            key, value = line.split(b'" "')
            decoded_key = key[1:].decode('ascii')
            decoded_value = value[:-1].decode('ascii')

            # Now, we need to figure out if this is a keyvalue,
            # or connection.
            # If we're L4D+, this is easy - they use 0x1D as separator.
            # Before, it's a comma which is common in keyvalues.
            # Assume it's an output if it has exactly 4 commas, and the last two
            # successfully parse as numbers.
            if 27 in value:
                # All outputs use the comma_sep, so we can ID them.
                cur_ent.add_out(
                    Output.parse(Property(decoded_key, decoded_value)))
            elif value.count(b',') == 4:
                try:
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_value)))
                except ValueError:
                    cur_ent[decoded_key] = decoded_value
            else:
                # Normal keyvalue.
                cur_ent[decoded_key] = decoded_value

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
예제 #9
0
def retexture_template(
    template_data: ExportedTemplate,
    origin: Vec,
    fixup: EntityFixup = None,
    replace_tex: Mapping[str, Union[list[str], str]] = srctools.EmptyMapping,
    force_colour: Portalable = None,
    force_grid: TileSize = None,
    generator: GenCat = GenCat.NORMAL,
    sense_offset: Optional[Vec] = None,
):
    """Retexture a template at the given location.

    - Only textures in the TEMPLATE_RETEXTURE dict will be replaced.
    - Others will be ignored (nodraw, plasticwall, etc)
    - Wall textures pointing up and down will switch to floor/ceiling textures.
    - Textures of the same type, normal and inst origin will randomise to the
      same type.
    - replace_tex is a replacement table. This overrides everything else.
      The values should either be a list (random), or a single value.
    - If force_colour is set, all tile textures will be switched accordingly.
      If set to 'INVERT', white and black textures will be swapped.
    - If force_grid is set, all tile textures will be that size.
    - generator defines the generator category to use for surfaces.
    - Fixup is the inst.fixup value, used to allow $replace in replace_tex.
    - If sense_offset is set, color pickers and tilesetters will be treated
      as if they were locally offset this far in the template.
    """

    template = template_data.template

    rev_id_mapping = {
        new_id: str(old_id)
        for old_id, new_id in template_data.orig_ids.items()
    }

    all_brushes = list(template_data.world)
    if template_data.detail is not None:
        all_brushes.extend(template_data.detail.solids)

    # Template faces are randomised per block and side. This means
    # multiple templates in the same block get the same texture, so they
    # can clip into each other without looking bad.
    rand_prefix = 'TEMPLATE_{0.x}_{0.y}_{0.z}:'.format(origin // 128)

    # Reprocess the replace_tex passed in, converting values.
    evalled_replace_tex: dict[str, list[str]] = {}
    for key, value in replace_tex.items():
        if isinstance(value, str):
            value = [value]
        if fixup is not None:
            # Convert the material and key for fixup names.
            value = [
                fixup[mat] if mat.startswith('$') else mat for mat in value
            ]
            if key.startswith('$'):
                key = fixup[key]
        # If starting with '#', it's a face id, or a list of those.
        if key.startswith('#'):
            for k in key[1:].split():
                try:
                    old_id = int(k)
                except (ValueError, TypeError):
                    pass
                else:
                    evalled_replace_tex.setdefault('#' + str(old_id),
                                                   []).extend(value)
        else:
            evalled_replace_tex.setdefault(key.casefold(), []).extend(value)

    if sense_offset is None:
        sense_offset = Vec()
    else:
        sense_offset = sense_offset @ template_data.orient

    # For each face, if it needs to be forced to a colour, or None if not.
    # If a string it's forced to that string specifically.
    force_colour_face: dict[str, Union[Portalable, str,
                                       None]] = defaultdict(lambda: None)
    # Picker names to their results.
    picker_results = template_data.picker_results
    picker_type_results: dict[str, Optional[TileType]] = {}

    # If the "use patterns" option is enabled, face ID -> temp face to copy from.
    picker_patterned: dict[str, Optional[Side]] = defaultdict(lambda: None)
    # Then also a cache of the tiledef -> dict of template faces.
    pattern_cache: dict[tiling.TileDef, dict[tuple[int, int], Side]] = {}

    # Already sorted by priority.
    for color_picker in template.color_pickers:
        if not color_picker.visgroups.issubset(template_data.visgroups):
            continue

        picker_pos = round(
            color_picker.offset @ template_data.orient + template_data.origin +
            sense_offset, 6)
        picker_norm = round(color_picker.normal @ template_data.orient, 6)

        if color_picker.grid_snap:
            for axis in 'xyz':
                # Don't realign things in the normal's axis -
                # those are already fine.
                if abs(picker_norm[axis]) < 0.01:
                    picker_pos[axis] = picker_pos[axis] // 128 * 128 + 64

        try:
            tiledef, u, v = tiling.find_tile(picker_pos, picker_norm)
        except KeyError:
            # Doesn't exist. But only set if not already present.
            if color_picker.name:
                picker_results.setdefault(color_picker.name, None)
                picker_type_results.setdefault(color_picker.name, None)
            continue

        tile_type = tiledef[u, v]

        picker_type_results[color_picker.name] = tile_type

        try:
            tile_color = tile_type.color
        except ValueError:
            # Not a tile with color (void, etc). Treat as missing a color.
            picker_results.setdefault(color_picker.name, None)
            continue

        if color_picker.name and picker_results.get(color_picker.name,
                                                    None) is None:
            picker_results[color_picker.name] = tile_color

        if color_picker.use_pattern:
            # Generate the brushwork for the tile to determine the top faces
            # required. We can then throw away the brushes themselves.
            try:
                pattern = pattern_cache[tiledef]
            except KeyError:
                pattern = pattern_cache[tiledef] = {}
                tiledef.gen_multitile_pattern(
                    VMF(),
                    {(u, v): tiledef[u, v]
                     for u in (0, 1, 2, 3) for v in (0, 1, 2, 3)},
                    is_wall=abs(tiledef.normal.z) > 0.01,
                    bevels=set(),
                    normal=tiledef.normal,
                    face_output=pattern,
                )

            for side in color_picker.sides:
                if picker_patterned[side] is None and (u, v) in pattern:
                    picker_patterned[side] = pattern[u, v]
        else:
            # Only do the highest priority successful one.
            for side in color_picker.sides:
                if force_colour_face[side] is None:
                    if tile_color is tile_color.WHITE:
                        force_colour_face[
                            side] = color_picker.force_tex_white or tile_color
                    else:
                        force_colour_face[
                            side] = color_picker.force_tex_black or tile_color

        if color_picker.after is AfterPickMode.VOID:
            tiledef[u, v] = TileType.VOID
        elif color_picker.after is AfterPickMode.NODRAW:
            tiledef[u, v] = TileType.NODRAW

    for voxel_setter in template.voxel_setters:
        if not voxel_setter.visgroups.issubset(template_data.visgroups):
            continue

        setter_pos = round(
            voxel_setter.offset @ template_data.orient + template_data.origin +
            sense_offset, 6)
        setter_norm = round(voxel_setter.normal @ template_data.orient, 6)

        norm_axis = setter_norm.axis()
        u_axis, v_axis = Vec.INV_AXIS[norm_axis]
        offsets = (-48, -16, 16, 48)
        for uoff in offsets:
            for voff in offsets:
                tiling.edit_quarter_tile(
                    setter_pos + Vec.with_axes(u_axis, uoff, v_axis, voff),
                    setter_norm,
                    voxel_setter.tile_type,
                    silent=True,  # Don't log missing positions.
                    force=voxel_setter.force,
                )

    for tile_setter in template.tile_setters:
        if not tile_setter.visgroups.issubset(template_data.visgroups):
            continue

        setter_pos = round(
            tile_setter.offset @ template_data.orient + template_data.origin +
            sense_offset, 6)
        setter_norm = round(tile_setter.normal @ template_data.orient, 6)

        setter_type: TileType = tile_setter.tile_type

        if tile_setter.color == 'copy':
            if not tile_setter.picker_name:
                raise ValueError('"{}": Tile Setter set to copy mode '
                                 'must have a color picker!'.format(
                                     template.id))
            # If a color picker is set, it overrides everything else.
            try:
                picker_res = picker_type_results[tile_setter.picker_name]
            except KeyError:
                raise ValueError('"{}": Tile Setter specified color picker '
                                 '"{}" which does not exist!'.format(
                                     template.id, tile_setter.picker_name))
            if picker_res is None:
                raise ValueError(
                    '"{}": Color picker "{}" has no tile to pick!'.format(
                        template.id, tile_setter.picker_name))
            setter_type = picker_res
        elif setter_type.is_tile:
            if tile_setter.picker_name:
                # If a color picker is set, it overrides everything else.
                try:
                    setter_color = picker_results[tile_setter.picker_name]
                except KeyError:
                    raise ValueError(
                        '"{}": Tile Setter specified color picker '
                        '"{}" which does not exist!'.format(
                            template.id, tile_setter.picker_name))
                if setter_color is None:
                    raise ValueError(
                        '"{}": Color picker "{}" has no tile to pick!'.format(
                            template.id, tile_setter.picker_name))
            elif isinstance(tile_setter.color, Portalable):
                # The color was specifically set.
                setter_color = tile_setter.color
            elif isinstance(force_colour, Portalable):
                # Otherwise it copies the forced colour -
                # it needs to be white or black.
                setter_color = force_colour
            else:
                # We need a forced color, but none was provided.
                raise ValueError(
                    '"{}": Tile Setter set to use colour value from the '
                    "template's overall color, "
                    'but not given one!'.format(template.id))

            # Inverting applies to all of these.
            if force_colour == 'INVERT':
                setter_color = ~setter_color

            setter_type = TileType.with_color_and_size(
                setter_type.tile_size,
                setter_color,
            )

        tiling.edit_quarter_tile(
            setter_pos,
            setter_norm,
            setter_type,
            silent=True,  # Don't log missing positions.
            force=tile_setter.force,
        )

    for brush in all_brushes:
        for face in brush:
            orig_id = rev_id_mapping.get(face.id) or ''

            if orig_id in template.skip_faces:
                continue

            template_face = picker_patterned[orig_id]
            if template_face is not None:
                face.mat = template_face.mat
                face.uaxis = template_face.uaxis.copy()
                face.vaxis = template_face.vaxis.copy()
                continue

            folded_mat = face.mat.casefold()

            norm = face.normal()

            if orig_id in template.realign_faces:
                try:
                    uaxis, vaxis = REALIGN_UVS[norm.as_tuple()]
                except KeyError:
                    LOGGER.warning(
                        'Realign face in template "{}" ({} in final) is '
                        'not on grid!',
                        template.id,
                        face.id,
                    )
                else:
                    face.uaxis = uaxis.copy()
                    face.vaxis = vaxis.copy()
            elif orig_id in template.vertical_faces:
                # Rotate the face in increments of 90 degrees, so it is as
                # upright as possible.
                pos_u = face.uaxis
                pos_v = face.vaxis
                # If both are zero, we're on the floor/ceiling and this is
                # pointless.
                if pos_u.z != 0 or pos_v.z != 0:
                    neg_u = UVAxis(-pos_u.x, -pos_u.y, -pos_u.z, pos_u.offset,
                                   pos_u.scale)
                    neg_v = UVAxis(-pos_v.x, -pos_v.y, -pos_v.z, pos_v.offset,
                                   pos_v.scale)
                    # Each row does u, v = v, -u to rotate 90 degrees.
                    # We want whichever makes V point vertically.
                    face.uaxis, face.vaxis = max([
                        (pos_u, pos_v),
                        (pos_v, neg_u),
                        (neg_u, neg_v),
                        (neg_v, pos_u),
                    ],
                                                 key=lambda uv: -uv[1].z)

            override_mat: Optional[list[str]]
            try:
                override_mat = evalled_replace_tex['#' + orig_id]
            except KeyError:
                try:
                    override_mat = evalled_replace_tex[folded_mat]
                except KeyError:
                    override_mat = None

            if override_mat is not None:
                # Replace_tex overrides everything.
                mat = rand.seed(b'template', norm,
                                face.get_origin()).choice(override_mat)
                if mat[:1] == '$' and fixup is not None:
                    mat = fixup[mat]
                if mat.startswith('<') and mat.endswith('>'):
                    # Lookup in the style data.
                    gen, mat = texturing.parse_name(mat[1:-1])
                    mat = gen.get(face.get_origin() + face.normal(), mat)
                # If blank, don't set.
                if mat:
                    face.mat = mat
                    continue

            if folded_mat == 'tile/white_wall_tile003b':
                LOGGER.warning(
                    '"{}": white_wall_tile003b has changed definitions.',
                    template.id)

            try:
                tex_type = TEMPLATE_RETEXTURE[folded_mat]
            except KeyError:
                continue  # It's nodraw, or something we shouldn't change

            tex_colour: Optional[Portalable]
            gen_type, tex_name, tex_colour = tex_type

            if not gen_type.is_tile:
                # It's something like squarebeams or backpanels, so
                # we don't need much special handling.
                texturing.apply(gen_type, face, tex_name)

                if tex_name in ('goo', 'goo_cheap'):
                    if norm != (0, 0, -1):
                        # Goo must be facing upright!
                        # Retexture to nodraw, so a template can be made with
                        # all faces goo to work in multiple orientations.
                        face.mat = 'tools/toolsnodraw'
                    else:
                        # Goo always has the same orientation!
                        face.uaxis = UVAxis(
                            1,
                            0,
                            0,
                            offset=0,
                            scale=options.get(float, 'goo_scale') or 0.25,
                        )
                        face.vaxis = UVAxis(
                            0,
                            -1,
                            0,
                            offset=0,
                            scale=options.get(float, 'goo_scale') or 0.25,
                        )
                continue
            else:
                assert isinstance(tex_colour, Portalable)
                # Allow overriding to panel or bullseye types.
                if gen_type is GenCat.NORMAL:
                    gen_type = generator
            # Otherwise, it's a panel wall or the like

            if force_colour_face[orig_id] is not None:
                tex_colour = force_colour_face[orig_id]
                if isinstance(tex_colour, str):
                    face.mat = tex_colour
                    continue
            elif force_colour == 'INVERT':
                # Invert the texture
                tex_colour = ~tex_colour
            elif force_colour is not None:
                tex_colour = force_colour

            if force_grid is not None:
                tex_name = force_grid

            texturing.apply(gen_type, face, tex_name, tex_colour)

    for over in template_data.overlay[:]:
        over_pos = Vec.from_str(over['basisorigin'])
        mat = over['material'].casefold()

        if mat in replace_tex:
            rng = rand.seed(b'temp', template_data.template.id, over_pos, mat)
            mat = rng.choice(replace_tex[mat])
            if mat[:1] == '$' and fixup is not None:
                mat = fixup[mat]
            if mat.startswith('<') or mat.endswith('>'):
                mat = mat[1:-1]
                gen, tex_name = texturing.parse_name(mat[1:-1])
                mat = gen.get(over_pos, tex_name)
        else:
            try:
                sign_type = consts.Signage(mat)
            except ValueError:
                pass
            else:
                mat = texturing.OVERLAYS.get(over_pos, sign_type)

        if mat == '':
            # If blank, remove the overlay from the map and the list.
            # (Since it's inplace, this can affect the tuple.)
            template_data.overlay.remove(over)
            over.remove()
        else:
            over['material'] = mat
예제 #10
0
def import_template(
    vmf: VMF,
    temp_name: Union[str, Template],
    origin: Vec,
    angles: Optional[Union[Angle, Matrix]] = None,
    targetname: str = '',
    force_type: TEMP_TYPES = TEMP_TYPES.default,
    add_to_map: bool = True,
    additional_visgroups: Iterable[str] = (),
    bind_tile_pos: Iterable[Vec] = (),
    align_bind: bool = False,
    coll: collisions.Collisions = None,
    coll_add: Optional[
        collisions.CollideType] = collisions.CollideType.NOTHING,
    coll_mask: collisions.CollideType = collisions.CollideType.EVERYTHING,
) -> ExportedTemplate:
    """Import the given template at a location.

    * `temp_name` can be a string, or a template instance.
    * `visgroups` is a list of additional visgroups to use after the ones in the name string (if given).
    * If `force_type` is set to 'detail' or 'world', all brushes will be converted
      to the specified type instead. A list of world brushes and the func_detail
      entity will be returned. If there are no detail brushes, None will be
      returned instead of an invalid entity.
    * If `targetname` is set, it will be used to localise overlay names.
      add_to_map sets whether to add the brushes and func_detail to the map.
    * IF `coll` is provided, the template may have bee2_collision volumes. The targetname must be
      provided in this case.
    * If any `bound_tile_pos` are provided, these are offsets to tiledefs which
      should have all the overlays in this template bound to them, and vice versa.
    * If `align_bind` is set, these will be first aligned to grid.
    * `coll_mask` and `coll_force` allow modifying the collision types added. `coll_mask` is AND-ed
      with the bbox type, then `coll_add` is OR-ed in. If the collide type ends up being NOTHING, it
      is skipped.
    """
    import vbsp
    if isinstance(temp_name, Template):
        template, temp_name = temp_name, temp_name.id
        chosen_groups: set[str] = set()
    else:
        temp_name, chosen_groups = parse_temp_name(temp_name)
        template = get_template(temp_name)

    chosen_groups.update(additional_visgroups)
    chosen_groups.add('')

    orig_world, orig_detail, orig_over = template.visgrouped(chosen_groups)

    new_world: list[Solid] = []
    new_detail: list[Solid] = []
    new_over: list[Entity] = []

    # A map of the original -> new face IDs.
    id_mapping: dict[int, int] = {}
    orient = to_matrix(angles)

    for orig_list, new_list in [(orig_world, new_world),
                                (orig_detail, new_detail)]:
        for old_brush in orig_list:
            brush = old_brush.copy(
                vmf_file=vmf,
                side_mapping=id_mapping,
                keep_vis=False,
            )
            brush.localise(origin, orient)
            new_list.append(brush)

    for overlay in orig_over:
        new_overlay = overlay.copy(
            vmf_file=vmf,
            keep_vis=False,
        )
        del new_overlay[
            'template_id']  # Remove this, it's not part of overlays
        new_overlay['classname'] = 'info_overlay'

        sides = overlay['sides'].split()
        new_overlay['sides'] = ' '.join(
            str(id_mapping[int(side)]) for side in sides
            if int(side) in id_mapping)

        srctools.vmf.localise_overlay(new_overlay, origin, orient)
        orig_target = new_overlay['targetname']

        # Only change the targetname if the overlay is not global, and we have
        # a passed name.
        if targetname and orig_target and orig_target[0] != '@':
            new_overlay['targetname'] = targetname + '-' + orig_target

        vmf.add_ent(new_overlay)
        new_over.append(new_overlay)

        # Don't let the overlays get retextured too!
        vbsp.IGNORED_OVERLAYS.add(new_overlay)

    if force_type is TEMP_TYPES.detail:
        new_detail.extend(new_world)
        new_world.clear()
    elif force_type is TEMP_TYPES.world:
        new_world.extend(new_detail)
        new_detail.clear()

    if add_to_map:
        vmf.add_brushes(new_world)

    detail_ent: Optional[Entity] = None

    if new_detail:
        detail_ent = vmf.create_ent(classname='func_detail')
        detail_ent.solids = new_detail
        if not add_to_map:
            detail_ent.remove()

    if bind_tile_pos:
        # Bind all our overlays without IDs to a set of tiles,
        # and add any marked faces to those tiles to be given overlays.
        new_overlay_faces = set(
            map(id_mapping.get, map(int, template.overlay_faces)))
        new_overlay_faces.discard(None)
        bound_overlay_faces = [
            face for brush in (new_world + new_detail) for face in brush.sides
            if face.id in new_overlay_faces
        ]

        tile_norm = orient.up()
        for tile_off in bind_tile_pos:
            tile_off = tile_off.copy()
            tile_off.localise(origin, orient)
            for axis in ('xyz' if align_bind else ''):
                # Don't realign things in the normal's axis -
                # those are already fine.
                if abs(tile_norm[axis]) < 1e-6:
                    tile_off[axis] = tile_off[axis] // 128 * 128 + 64
            try:
                tile = tiling.TILES[tile_off.as_tuple(), tile_norm.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'No tile to bind at {} for "{}"!',
                    tile_off,
                    template.id,
                )
                continue
            for over in new_over:
                if over['sides'] == '':
                    tile.bind_overlay(over)
            tile.brush_faces.extend(bound_overlay_faces)

    if template.collisions:
        if coll is None:
            LOGGER.warning(
                'Template "{}" has collisions, but unable to apply these!',
                template.id)
        elif targetname:
            for coll_def in template.collisions:
                if not coll_def.visgroups.issubset(chosen_groups):
                    continue
                contents = (coll_def.bbox.contents & coll_mask) | coll_add
                if contents is not contents.NOTHING:
                    bbox = coll_def.bbox @ orient + origin
                    coll.add(
                        bbox.with_attrs(name=targetname, contents=contents))
        else:
            LOGGER.warning(
                'With collisions provided, the instance name must not be blank!'
            )

    return ExportedTemplate(
        world=new_world,
        detail=detail_ent,
        overlay=new_over,
        orig_ids=id_mapping,
        template=template,
        origin=origin,
        orient=orient,
        visgroups=chosen_groups,
        picker_results={},  # Filled by retexture_template.
        picker_type_results={},
    )
예제 #11
0
    """
    world: list[Solid]
    detail: Optional[Entity]
    overlay: list[Entity]
    orig_ids: dict[int, int]
    template: 'Template'
    origin: Vec
    orient: Matrix
    visgroups: set[str]
    picker_results: dict[str, Optional[Portalable]]
    picker_type_results: dict[str, Optional[TileType]]


# Make_prism() generates faces aligned to world, copy the required UVs.
realign_solid: Solid = VMF().make_prism(Vec(-16, -16, -16), Vec(16, 16,
                                                                16)).solid
REALIGN_UVS = {
    face.normal().as_tuple(): (face.uaxis, face.vaxis)
    for face in realign_solid
}
del realign_solid


class Template:
    """Represents a template before it's imported into a map."""
    _data: dict[str, tuple[list[Solid], list[Solid], list[Entity]]]

    def __init__(
            self,
            temp_id: str,
            visgroup_names: set[str],
예제 #12
0
def import_template(
        vmf: VMF,
        temp_name: Union[str, Template],
        origin: Vec,
        angles: Optional[Union[Angle, Matrix]] = None,
        targetname: str = '',
        force_type: TEMP_TYPES = TEMP_TYPES.default,
        add_to_map: bool = True,
        additional_visgroups: Iterable[str] = (),
        visgroup_choose: Callable[[Iterable[str]], Iterable[str]] = lambda x:
    (),
        bind_tile_pos: Iterable[Vec] = (),
) -> ExportedTemplate:
    """Import the given template at a location.

    temp_name can be a string, or a template instance. visgroups is a list
    of additional visgroups to use after the ones in the name string (if given).

    If force_type is set to 'detail' or 'world', all brushes will be converted
    to the specified type instead. A list of world brushes and the func_detail
    entity will be returned. If there are no detail brushes, None will be
    returned instead of an invalid entity.

    If targetname is set, it will be used to localise overlay names.
    add_to_map sets whether to add the brushes and func_detail to the map.
    visgroup_choose is a callback used to determine if visgroups should be
    added - it's passed a list of names, and should return a list of ones to use.

    If any bound_tile_pos are provided, these are offsets to tiledefs which
    should have all the overlays in this template bound to them, and vice versa.
    """
    import vbsp
    if isinstance(temp_name, Template):
        template, temp_name = temp_name, temp_name.id
        chosen_groups = set()  # type: Set[str]
    else:
        temp_name, chosen_groups = parse_temp_name(temp_name)
        template = get_template(temp_name)

    chosen_groups.update(additional_visgroups)
    chosen_groups.update(visgroup_choose(template.visgroups))
    chosen_groups.add('')

    orig_world, orig_detail, orig_over = template.visgrouped(chosen_groups)

    new_world = []  # type: List[Solid]
    new_detail = []  # type: List[Solid]
    new_over = []  # type: List[Entity]

    # A map of the original -> new face IDs.
    id_mapping = {}  # type: Dict[int, int]
    orient = to_matrix(angles)

    for orig_list, new_list in [(orig_world, new_world),
                                (orig_detail, new_detail)]:
        for old_brush in orig_list:
            brush = old_brush.copy(
                vmf_file=vmf,
                side_mapping=id_mapping,
                keep_vis=False,
            )
            brush.localise(origin, orient)
            new_list.append(brush)

    for overlay in orig_over:  # type: Entity
        new_overlay = overlay.copy(
            vmf_file=vmf,
            keep_vis=False,
        )
        del new_overlay[
            'template_id']  # Remove this, it's not part of overlays
        new_overlay['classname'] = 'info_overlay'

        sides = overlay['sides'].split()
        new_overlay['sides'] = ' '.join(
            str(id_mapping[int(side)]) for side in sides
            if int(side) in id_mapping)

        srctools.vmf.localise_overlay(new_overlay, origin, orient)
        orig_target = new_overlay['targetname']

        # Only change the targetname if the overlay is not global, and we have
        # a passed name.
        if targetname and orig_target and orig_target[0] != '@':
            new_overlay['targetname'] = targetname + '-' + orig_target

        vmf.add_ent(new_overlay)
        new_over.append(new_overlay)

        # Don't let the overlays get retextured too!
        vbsp.IGNORED_OVERLAYS.add(new_overlay)

    if force_type is TEMP_TYPES.detail:
        new_detail.extend(new_world)
        new_world.clear()
    elif force_type is TEMP_TYPES.world:
        new_world.extend(new_detail)
        new_detail.clear()

    if add_to_map:
        vmf.add_brushes(new_world)

    detail_ent: Optional[Entity] = None

    if new_detail:
        detail_ent = vmf.create_ent(classname='func_detail')
        detail_ent.solids = new_detail
        if not add_to_map:
            detail_ent.remove()

    if bind_tile_pos:
        # Bind all our overlays without IDs to a set of tiles,
        # and add any marked faces to those tiles to be given overlays.
        new_overlay_faces = set(map(id_mapping.get, template.overlay_faces))
        new_overlay_faces.discard(None)
        bound_overlay_faces = [
            face for brush in (new_world + new_detail) for face in brush.sides
            if face.id in new_overlay_faces
        ]

        tile_norm = orient.up()
        for tile_off in bind_tile_pos:
            tile_off = tile_off.copy()
            tile_off.localise(origin, orient)
            try:
                tile = tiling.TILES[tile_off.as_tuple(), tile_norm.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'No tile to bind at {} for "{}"!',
                    tile_off,
                    template.id,
                )
                continue
            for over in new_over:
                if over['sides'] == '':
                    tile.bind_overlay(over)
            tile.brush_faces.extend(bound_overlay_faces)

    return ExportedTemplate(
        world=new_world,
        detail=detail_ent,
        overlay=new_over,
        orig_ids=id_mapping,
        template=template,
        origin=origin,
        orient=orient,
        visgroups=chosen_groups,
        picker_results={},  # Filled by retexture_template.
        picker_type_results={},
    )
예제 #13
0
    def setup(self, vmf: VMF, global_seed: str,
              tiles: List['TileDef']) -> None:
        """Build the list of clump locations."""
        assert self.portal is not None
        assert self.orient is not None

        # Convert the generator key to a generator-specific seed.
        # That ensures different surfaces don't end up reusing the same
        # texture indexes.
        self.gen_seed = int.from_bytes(
            self.category.name.encode() + self.portal.name.encode() +
            self.orient.name.encode(),
            'big',
        )

        LOGGER.info('Generating texture clumps...')

        clump_length: int = self.options['clump_length']
        clump_width: int = self.options['clump_width']

        # The tiles currently present in the map.
        orient_z = self.orient.z
        remaining_tiles: Set[Tuple[float, float, float]] = {
            (tile.pos + 64 * tile.normal // 128 * 128).as_tuple()
            for tile in tiles if tile.normal.z == orient_z
        }

        # A global RNG for picking clump positions.
        clump_rand = random.Random(global_seed + '_clumping')

        pos_min = Vec()
        pos_max = Vec()

        # For debugging, generate skip brushes with the shape of the clumps.
        debug_visgroup: Optional[VisGroup]
        if self.options['clump_debug']:
            debug_visgroup = vmf.create_visgroup(
                f'{self.category.name}_{self.orient.name}_{self.portal.name}')
        else:
            debug_visgroup = None

        while remaining_tiles:
            # Pick from a random tile.
            tile_pos = next(
                itertools.islice(
                    remaining_tiles,
                    clump_rand.randrange(0, len(remaining_tiles)),
                    len(remaining_tiles),
                ))
            remaining_tiles.remove(tile_pos)

            pos = Vec(tile_pos)

            # Clumps are long strips mainly extended in one direction
            # In the other directions extend by 'width'. It can point any axis.
            direction = clump_rand.choice('xyz')
            for axis in 'xyz':
                if axis == direction:
                    dist = clump_length
                else:
                    dist = clump_width
                pos_min[axis] = pos[axis] - clump_rand.randint(0, dist) * 128
                pos_max[axis] = pos[axis] + clump_rand.randint(0, dist) * 128

            remaining_tiles.difference_update(
                map(Vec.as_tuple, Vec.iter_grid(pos_min, pos_max, 128)))

            self._clump_locs.append(
                Clump(
                    pos_min.x,
                    pos_min.y,
                    pos_min.z,
                    pos_max.x,
                    pos_max.y,
                    pos_max.z,
                    # We use this to reseed an RNG, giving us the same textures
                    # each time for the same clump.
                    clump_rand.getrandbits(32),
                ))
            if debug_visgroup is not None:
                # noinspection PyUnboundLocalVariable
                debug_brush: Solid = vmf.make_prism(
                    pos_min - 64,
                    pos_max + 64,
                    'tools/toolsskip',
                ).solid
                debug_brush.visgroup_ids.add(debug_visgroup.id)
                debug_brush.vis_shown = False
                vmf.add_brush(debug_brush)

        LOGGER.info(
            '{}.{}.{}: {} Clumps for {} tiles',
            self.category.name,
            self.orient.name,
            self.portal.name,
            len(self._clump_locs),
            len(tiles),
        )