Exemple #1
0
 def plane_normal(self) -> Vec | None:
     """If a plane, returns the normal axis."""
     if self.min_x == self.max_x:
         return Vec(1.0, 0.0, 0.0)
     elif self.min_y == self.max_y:
         return Vec(0.0, 1.0, 0.0)
     elif self.min_z == self.max_z:
         return Vec(0.0, 0.0, 1.0)
     return None
Exemple #2
0
    def rotate(self,
               angles: Union[Angle, Matrix],
               origin: Optional[Vec] = None) -> 'ScalingTemplate':
        """Rotate this template, and return a new template with those angles."""
        new_axis = {}
        if origin is None:
            origin = Vec()

        for norm, (mat, axis_u, axis_v, rot) in self._axes.items():
            axis_u = axis_u.localise(origin, angles)
            axis_v = axis_v.localise(origin, angles)
            v_norm = Vec(norm) @ angles
            new_axis[v_norm.as_tuple()] = mat, axis_u, axis_v, rot

        return ScalingTemplate(self.id, new_axis)
Exemple #3
0
 def size(self) -> Vec:
     """Return the size of the bounding box, as a Vector."""
     return Vec(
         self.max_x - self.min_x,
         self.max_y - self.min_y,
         self.max_z - self.min_z,
     )
Exemple #4
0
 def center(self) -> Vec:
     """Return the center of the bounding box, as a Vector."""
     return Vec(
         (self.min_x + self.max_x) / 2.0,
         (self.min_y + self.max_y) / 2.0,
         (self.min_z + self.max_z) / 2.0,
     )
Exemple #5
0
    def from_ent(cls, ent: Entity) -> Iterator[BBox]:
        """Parse keyvalues on a VMF entity. One bounding box is produced for each brush."""
        coll = CollideType.NOTHING
        for key, value in ent.keys.items():
            if key.casefold().startswith('coll_') and conv_bool(value):
                coll_name = key[5:].upper()
                try:
                    coll |= CollideType[coll_name]
                except KeyError:
                    LOGGER.warning('Invalid collide type: "{}"!', key)
        tags = frozenset(ent['tags'].split())

        for solid in ent.solids:
            mins, maxes = solid.get_bbox()
            non_skip_faces = [
                face
                for face in solid
                if face.mat != consts.Tools.SKIP
            ]
            try:
                # Only one non-skip face, "flatten" along its plane.
                face: Side
                [face] = non_skip_faces
            except ValueError:
                pass  # Regular bbox.
            else:
                plane_norm = face.normal()
                plane_point = face.planes[0]
                for point in [mins, maxes]:
                    # Get the offset from the plane, then subtract to force it onto the plane.
                    point -= plane_norm * Vec.dot(point - plane_point, plane_norm)

            yield cls(mins, maxes, contents=coll, tags=tags)
Exemple #6
0
def res_add_placement_helper(inst: Entity, res: Property):
    """Add a placement helper to a specific tile.

    `Offset` and `normal` specify the position and direction out of the surface
    the helper should be added to. If `upDir` is specified, this is the
    direction of the top of the portal.
    """
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    pos = conditions.resolve_offset(inst, res['offset', '0 0 0'], zoff=-64)
    normal = res.vec('normal', 0, 0, 1) @ orient

    up_dir: Vec | None
    try:
        up_dir = Vec.from_str(res['upDir']) @ orient
    except LookupError:
        up_dir = None

    try:
        tile = tiling.TILES[(pos - 64 * normal).as_tuple(), normal.as_tuple()]
    except KeyError:
        LOGGER.warning('No tile at {} @ {}', pos, normal)
        return

    tile.add_portal_helper(up_dir)
Exemple #7
0
def res_checkpoint_trigger(inst: Entity, res: Property) -> None:
    """Generate a trigger underneath coop checkpoint items.

    """

    if vbsp.GAME_MODE == 'SP':
        # We can't have a respawn dropper in singleplayer.
        # Not generating the trigger means it's not going to
        # do anything.
        return

    pos = brushLoc.POS.raycast_world(
        Vec.from_str(inst['origin']),
        direction=Vec(0, 0, -1),
    )
    bbox_min = pos - (192, 192, 64)
    bbox_max = pos + (192, 192, 64)

    # Find triggers already placed next to ours, and
    # merge with them if that's the case
    for offset in CHECKPOINT_NEIGHBOURS:
        near_pos = pos + offset
        try:
            trig = CHECKPOINT_TRIG[near_pos.as_tuple()]
            break
        except KeyError:
            pass
    else:
        # None found, make one.
        trig = inst.map.create_ent(
            classname='trigger_playerteam',
            origin=pos,
        )
        trig.solids = []
        CHECKPOINT_TRIG[pos.as_tuple()] = trig

    trig.solids.append(
        inst.map.make_prism(
            bbox_min,
            bbox_max,
            mat=consts.Tools.TRIGGER,
        ).solid)

    for prop in res:
        out = Output.parse(prop)
        out.target = conditions.local_name(inst, out.target)
        trig.add_out(out)
Exemple #8
0
    def vec(self, key: str, x=0.0, y=0.0, z=0.0) -> _Vec:
        """Return the given property, converted to a vector.

        If multiple keys with the same name are present, this will use the
        last only.
        """
        if not self.has_children():
            raise ValueError("{!r} has no children!".format(self))
        try:
            return _Vec.from_str(self._get_value(key), x, y, z)
        except LookupError:  # key not present, defaults.
            return _Vec(x, y, z)
Exemple #9
0
 def __iter__(self) -> Iterator[Tuple[Vec, Vec, ValueT]]:
     """Iterating over the tree returns each bbox and value associated with it."""
     for holder in self._by_id.values():
         mins = Vec(holder.min_x, holder.min_y, holder.min_z)
         maxes = Vec(holder.max_x, holder.max_y, holder.max_z)
         for val in holder.values:
             yield mins.copy(), maxes.copy(), val
Exemple #10
0
def res_antigel(inst: Entity) -> None:
    """Implement the Antigel marker."""
    inst.remove()
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    pos = round(origin - 128 * orient.up(), 6)
    norm = round(orient.up(), 6)
    try:
        tiling.TILES[pos.as_tuple(), norm.as_tuple()].is_antigel = True
    except KeyError:
        LOGGER.warning('No tile to set antigel at {}, {}', pos, norm)
    texturing.ANTIGEL_LOCS.add((origin // 128).as_tuple())
Exemple #11
0
 def remove(self, p1: Vec, p2: Vec, value: ValueT) -> None:
     """Remove the specified value from the tree."""
     mins, maxs = Vec.bbox(p1, p2)
     coords = (mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z)
     try:
         holder = self._by_coord[coords]
         holder.values.remove(value)
     except LookupError:
         raise KeyError(mins, maxs, value) from None
     # Removed, check to see if the holder is empty, and we can discard.
     if not holder.values:
         del self._by_id[id(holder)]
         del self._by_coord[coords]
         self.tree.delete(id(holder), coords)
Exemple #12
0
 def __iter__(self) -> Iterator[Vec]:
     yield from [
         Vec(-1, 0, 0),
         Vec(1, 0, 0),
         Vec(0, -1, 0),
         Vec(0, 1, 0),
         Vec(0, 0, -1),
         Vec(0, 0, 1),
     ]
Exemple #13
0
 def insert(self, p1: Vec, p2: Vec, value: ValueT) -> None:
     """Add the specified value occupying the specified region."""
     mins, maxs = Vec.bbox(p1, p2)
     coords = (mins.x, mins.y, mins.z, maxs.x, maxs.y, maxs.z)
     try:
         holder = self._by_coord[coords]
     except KeyError:
         # Make one.
         holder = ValueHolder([value], *coords)
         self._by_id[id(holder)] = self._by_coord[coords] = holder
         self.tree.insert(id(holder), coords)
     else:
         # Append if not already present.
         if value not in holder.values:
             holder.values.append(value)
Exemple #14
0
def _fill_norm_rotations() -> dict[tuple[tuple[float, float, float], tuple[
    float, float, float]], Matrix, ]:
    """Given a norm->norm rotation, return the angles producing that."""
    rotations = {}
    for norm_ax in 'xyz':
        for norm_mag in [-1, +1]:
            norm = Vec.with_axes(norm_ax, norm_mag)
            for angle_ax in ('pitch', 'yaw', 'roll'):
                for angle_mag in (-90, 90):
                    angle = Matrix.from_angle(
                        Angle.with_axes(angle_ax, angle_mag))
                    new_norm = norm @ angle
                    if new_norm != norm:
                        rotations[norm.as_tuple(), new_norm.as_tuple()] = angle
            # Assign a null rotation as well.
            rotations[norm.as_tuple(), norm.as_tuple()] = Matrix()
            rotations[norm.as_tuple(), (-norm).as_tuple()] = Matrix()
    return rotations
Exemple #15
0
def res_transfer_bullseye(inst: Entity, props: Property):
    """Transfer catapult targets and placement helpers from one tile to another."""
    start_pos = conditions.resolve_offset(inst, props['start_pos', ''])
    end_pos = conditions.resolve_offset(inst, props['end_pos', ''])
    angles = Angle.from_str(inst['angles'])
    start_norm = props.vec('start_norm', 0, 0, 1) @ angles
    end_norm = props.vec('end_norm', 0, 0, 1) @ angles

    try:
        start_tile = tiling.TILES[(start_pos - 64 * start_norm).as_tuple(),
                                  start_norm.as_tuple()]
    except KeyError:
        LOGGER.warning(
            '"{}": Cannot find tile to transfer from at {}, {}!'.format(
                inst['targetname'], start_pos, start_norm))
        return

    end_tile = tiling.TileDef.ensure(
        end_pos - 64 * end_norm,
        end_norm,
    )
    # Now transfer the stuff.
    if start_tile.has_oriented_portal_helper:
        # We need to rotate this.
        orient = start_tile.portal_helper_orient.copy()
        # If it's directly opposite, just mirror - we have no clue what the
        # intent is.
        if Vec.dot(start_norm, end_norm) != -1.0:
            # Use the dict to compute the rotation to apply.
            orient @= NORM_ROTATIONS[start_norm.as_tuple(),
                                     end_norm.as_tuple()]
        end_tile.add_portal_helper(orient)
    elif start_tile.has_portal_helper:
        # Non-oriented, don't orient.
        end_tile.add_portal_helper()
    start_tile.remove_portal_helper(all=True)

    if start_tile.bullseye_count:
        end_tile.bullseye_count = start_tile.bullseye_count
        start_tile.bullseye_count = 0
        # Then transfer the targets across.
        for plate in faithplate.PLATES.values():
            if getattr(plate, 'target', None) is start_tile:
                plate.target = end_tile
Exemple #16
0
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None:
    """Implements SetPanelOptions and CreatePanel."""
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))
    normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6)
    origin = Vec.from_str(inst['origin'])
    uaxis, vaxis = Vec.INV_AXIS[normal.axis()]

    points: set[tuple[float, float, float]] = set()

    if 'point' in props:
        for prop in props.find_all('point'):
            points.add(
                conditions.resolve_offset(inst, prop.value,
                                          zoff=-64).as_tuple())
    elif 'pos1' in props and 'pos2' in props:
        pos1, pos2 = Vec.bbox(
            conditions.resolve_offset(inst,
                                      props['pos1', '-48 -48 0'],
                                      zoff=-64),
            conditions.resolve_offset(inst, props['pos2', '48 48 0'],
                                      zoff=-64),
        )
        points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32)))
    else:
        # Default to the full tile.
        points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple()
                       for u in [-48.0, -16.0, 16.0, 48.0]
                       for v in [-48.0, -16.0, 16.0, 48.0]})

    tiles_to_uv: dict[tiling.TileDef, set[tuple[int, int]]] = defaultdict(set)
    for pos in points:
        try:
            tile, u, v = tiling.find_tile(Vec(pos), normal, force=create)
        except KeyError:
            continue
        tiles_to_uv[tile].add((u, v))

    if not tiles_to_uv:
        LOGGER.warning('"{}": No tiles found for panels!', inst['targetname'])
        return

    # If bevels is provided, parse out the overall world positions.
    bevel_world: set[tuple[int, int]] | None
    try:
        bevel_prop = props.find_key('bevel')
    except NoKeyError:
        bevel_world = None
    else:
        bevel_world = set()
        if bevel_prop.has_children():
            # Individually specifying offsets.
            for bevel_str in bevel_prop.as_array():
                bevel_point = Vec.from_str(bevel_str) @ orient + origin
                bevel_world.add(
                    (int(bevel_point[uaxis]), int(bevel_point[vaxis])))
        elif srctools.conv_bool(bevel_prop.value):
            # Fill the bounding box.
            bbox_min, bbox_max = Vec.bbox(map(Vec, points))
            off = Vec.with_axes(uaxis, 32, vaxis, 32)
            bbox_min -= off
            bbox_max += off
            for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
                if pos.as_tuple() not in points:
                    bevel_world.add((pos[uaxis], pos[vaxis]))
        # else: No bevels.
    panels: list[tiling.Panel] = []

    for tile, uvs in tiles_to_uv.items():
        if create:
            panel = tiling.Panel(
                None,
                inst,
                tiling.PanelType.NORMAL,
                thickness=4,
                bevels=(),
            )
            panel.points = uvs
            tile.panels.append(panel)
        else:
            for panel in tile.panels:
                if panel.same_item(inst) and panel.points == uvs:
                    break
            else:
                LOGGER.warning('No panel to modify found for "{}"!',
                               inst['targetname'])
                continue
        panels.append(panel)

        pan_type = '<nothing?>'
        try:
            pan_type = conditions.resolve_value(inst, props['type'])
            panel.pan_type = tiling.PanelType(pan_type.lower())
        except LookupError:
            pass
        except ValueError:
            raise ValueError('Unknown panel type "{}"!'.format(pan_type))

        if 'thickness' in props:
            panel.thickness = srctools.conv_int(
                conditions.resolve_value(inst, props['thickness']))
            if panel.thickness not in (2, 4, 8):
                raise ValueError(
                    '"{}": Invalid panel thickess {}!\n'
                    'Must be 2, 4 or 8.',
                    inst['targetname'],
                    panel.thickness,
                )

        if bevel_world is not None:
            panel.bevels.clear()
            for u, v in bevel_world:
                # Convert from world points to UV positions.
                u = (u - tile.pos[uaxis] + 48) // 32
                v = (v - tile.pos[vaxis] + 48) // 32
                # Cull outside here, we wont't use them.
                if -1 <= u <= 4 and -1 <= v <= 4:
                    panel.bevels.add((u, v))

        if 'offset' in props:
            panel.offset = conditions.resolve_offset(inst, props['offset'])
            panel.offset -= Vec.from_str(inst['origin'])
        if 'template' in props:
            # We only want the template inserted once. So remove it from all but one.
            if len(panels) == 1:
                panel.template = inst.fixup.substitute(props['template'])
            else:
                panel.template = ''
        if 'nodraw' in props:
            panel.nodraw = srctools.conv_bool(
                inst.fixup.substitute(props['nodraw'], allow_invert=True))
        if 'seal' in props:
            panel.seal = srctools.conv_bool(
                inst.fixup.substitute(props['seal'], allow_invert=True))
        if 'move_bullseye' in props:
            panel.steals_bullseye = srctools.conv_bool(
                inst.fixup.substitute(props['move_bullseye'],
                                      allow_invert=True))
    if 'keys' in props or 'localkeys' in props:
        # First grab the existing ent, so we can edit it.
        # These should all have the same value, unless they were independently
        # edited with mismatching point sets. In that case destroy all those existing ones.
        existing_ents: set[Entity
                           | None] = {panel.brush_ent
                                      for panel in panels}
        try:
            [brush_ent] = existing_ents
        except ValueError:
            LOGGER.warning(
                'Multiple independent panels for "{}" were made, then the '
                'brush entity was edited as a group! Discarding '
                'individual ents...', inst['targetname'])
            for brush_ent in existing_ents:
                if brush_ent is not None and brush_ent in vmf.entities:
                    brush_ent.remove()
            brush_ent = None

        if brush_ent is None:
            brush_ent = vmf.create_ent('')

        old_pos = brush_ent.keys.pop('origin', None)

        conditions.set_ent_keys(brush_ent, inst, props)
        if not brush_ent['classname']:
            if create:  # This doesn't make sense, you could just omit the prop.
                LOGGER.warning(
                    'No classname provided for panel "{}"!',
                    inst['targetname'],
                )
            # Make it a world brush.
            brush_ent.remove()
            brush_ent = None
        else:
            # We want to do some post-processing.
            # Localise any origin value.
            if 'origin' in brush_ent.keys:
                pos = Vec.from_str(brush_ent['origin'])
                pos.localise(
                    Vec.from_str(inst['origin']),
                    Angle.from_str(inst['angles']),
                )
                brush_ent['origin'] = pos
            elif old_pos is not None:
                brush_ent['origin'] = old_pos

            # If it's func_detail, clear out all the keys.
            # Particularly `origin`, but the others are useless too.
            if brush_ent['classname'] == 'func_detail':
                brush_ent.clear_keys()
                brush_ent['classname'] = 'func_detail'
        for panel in panels:
            panel.brush_ent = brush_ent
Exemple #17
0
def load_templates() -> None:
    """Load in the template file, used for import_template()."""
    with open(TEMPLATE_LOCATION) as file:
        props = Property.parse(file, TEMPLATE_LOCATION)
    vmf = srctools.VMF.parse(props, preserve_ids=True)

    def make_subdict() -> Dict[str, list]:
        return defaultdict(list)

    # detail_ents[temp_id][visgroup]
    detail_ents = defaultdict(
        make_subdict)  # type: Dict[str, Dict[str, List[Solid]]]
    world_ents = defaultdict(
        make_subdict)  # type: Dict[str, Dict[str, List[Solid]]]
    overlay_ents = defaultdict(
        make_subdict)  # type: Dict[str, Dict[str, List[Entity]]]
    conf_ents = {}

    color_pickers = defaultdict(list)  # type: Dict[str, List[ColorPicker]]
    tile_setters = defaultdict(list)  # type: Dict[str, List[TileSetter]]

    for ent in vmf.by_class['bee2_template_world']:
        world_ents[ent['template_id'].casefold()][
            ent['visgroup'].casefold()].extend(ent.solids)

    for ent in vmf.by_class['bee2_template_detail']:
        detail_ents[ent['template_id'].casefold()][
            ent['visgroup'].casefold()].extend(ent.solids)

    for ent in vmf.by_class['bee2_template_overlay']:
        overlay_ents[ent['template_id'].casefold()][
            ent['visgroup'].casefold()].append(ent)

    for ent in vmf.by_class['bee2_template_conf']:
        conf_ents[ent['template_id'].casefold()] = ent

    for ent in vmf.by_class['bee2_template_scaling']:
        temp = ScalingTemplate.parse(ent)
        _TEMPLATES[temp.id.casefold()] = temp

    for ent in vmf.by_class['bee2_template_colorpicker']:
        # Parse the colorpicker data.
        temp_id = ent['template_id'].casefold()
        try:
            priority = Decimal(ent['priority'])
        except ValueError:
            LOGGER.warning(
                'Bad priority for colorpicker in "{}" template!',
                temp_id.upper(),
            )
            priority = Decimal(0)

        try:
            remove_after = AfterPickMode(ent['remove_brush', '0'])
        except ValueError:
            LOGGER.warning(
                'Bad remove-brush mode for colorpicker in "{}" template!',
                temp_id.upper(),
            )
            remove_after = AfterPickMode.NONE

        color_pickers[temp_id].append(
            ColorPicker(
                priority,
                name=ent['targetname'],
                visgroups=set(ent['visgroups'].split(' ')) - {''},
                offset=Vec.from_str(ent['origin']),
                normal=Vec(x=1) @ Angle.from_str(ent['angles']),
                sides=ent['faces'].split(' '),
                grid_snap=srctools.conv_bool(ent['grid_snap']),
                after=remove_after,
                use_pattern=srctools.conv_bool(ent['use_pattern']),
                force_tex_white=ent['tex_white'],
                force_tex_black=ent['tex_black'],
            ))

    for ent in vmf.by_class['bee2_template_tilesetter']:
        # Parse the tile setter data.
        temp_id = ent['template_id'].casefold()
        tile_type = TILE_SETTER_SKINS[srctools.conv_int(ent['skin'])]
        color = ent['color']
        if color == 'tile':
            try:
                color = tile_type.color
            except ValueError:
                # Non-tile types.
                color = None
        elif color == 'invert':
            color = 'INVERT'
        elif color == 'match':
            color = None
        elif color != 'copy':
            raise ValueError('Invalid TileSetter color '
                             '"{}" for "{}"'.format(color, temp_id))

        tile_setters[temp_id].append(
            TileSetter(
                offset=Vec.from_str(ent['origin']),
                normal=Vec(z=1) @ Angle.from_str(ent['angles']),
                visgroups=set(ent['visgroups'].split(' ')) - {''},
                color=color,
                tile_type=tile_type,
                picker_name=ent['color_picker'],
                force=srctools.conv_bool(ent['force']),
            ))

    temp_ids = set(conf_ents).union(
        detail_ents,
        world_ents,
        overlay_ents,
        color_pickers,
        tile_setters,
    )

    for temp_id in temp_ids:
        try:
            conf = conf_ents[temp_id]
        except KeyError:
            overlay_faces = []  # type: List[str]
            skip_faces = []  # type: List[str]
            vertical_faces = []  # type: List[str]
            realign_faces = []  # type: List[str]
        else:
            vertical_faces = conf['vertical_faces'].split()
            realign_faces = conf['realign_faces'].split()
            overlay_faces = conf['overlay_faces'].split()
            skip_faces = conf['skip_faces'].split()

        _TEMPLATES[temp_id.casefold()] = Template(
            temp_id,
            world_ents[temp_id],
            detail_ents[temp_id],
            overlay_ents[temp_id],
            skip_faces,
            realign_faces,
            overlay_faces,
            vertical_faces,
            color_pickers[temp_id],
            tile_setters[temp_id],
        )
Exemple #18
0
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property):
    """Properly setup rotating brush entities to match the instance.

    This uses the orientation of the instance to determine the correct
    spawnflags to make it rotate in the correct direction.

    This can either modify an existing entity (which may be in an instance),
    or generate a new one. The generated brush will be 2x2x2 units large,
    and always set to be non-solid.

    For both modes:
    - `Axis`: specifies the rotation axis local to the instance.
    - `Reversed`: If set, flips the direction around.
    - `Classname`: Specifies which entity, since the spawnflags required varies.

    For application to an existing entity:
    - `ModifyTarget`: The local name of the entity to modify.

    For brush generation mode:

    - `Pos` and `name` are local to the
      instance, and will set the `origin` and `targetname` respectively.
    - `Keys` are any other keyvalues to be be set.
    - `Flags` sets additional spawnflags. Multiple values may be
       separated by `+`, and will be added together.
    - `Classname` specifies which entity will be created, as well as
       which other values will be set to specify the correct orientation.
    - `AddOut` is used to add outputs to the generated entity. It takes
       the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If
       `Inst_targ` is defined, it will be used with the input to construct
       an instance proxy input. If `OnceOnly` is set, the output will be
       deleted when fired.

    Permitted entities:

       * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating)
       * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot)
       * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button)
       * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating)
       * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button)
    """
    des_axis = res['axis', 'z'].casefold()
    reverse = res.bool('reversed')
    door_type = res['classname', 'func_door_rotating']
    orient = Matrix.from_angle(Angle.from_str(ent['angles']))

    axis = round(Vec.with_axes(des_axis, 1) @ orient, 6)

    if axis.x > 0 or axis.y > 0 or axis.z > 0:
        # If it points forward, we need to reverse the rotating door
        reverse = not reverse
    axis = abs(axis)

    try:
        flag_values = FLAG_ROTATING[door_type]
    except KeyError:
        LOGGER.warning('Unknown rotating brush type "{}"!', door_type)
        return

    name = res['ModifyTarget', '']
    door_ent: Entity | None
    if name:
        name = conditions.local_name(ent, name)
        setter_loc = ent['origin']
        door_ent = None
        spawnflags = 0
    else:
        # Generate a brush.
        name = conditions.local_name(ent, res['name', ''])

        pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0'])
        pos += Vec.from_str(ent['origin', '0 0 0'])
        setter_loc = str(pos)

        door_ent = vmf.create_ent(
            classname=door_type,
            targetname=name,
            origin=pos.join(' '),
        )
        # Extra stuff to apply to the flags (USE, toggle, etc)
        spawnflags = sum(
            map(
                # Add together multiple values
                srctools.conv_int,
                res['flags', '0'].split('+')
                # Make the door always non-solid!
            )) | flag_values.get('solid_flags', 0)

        conditions.set_ent_keys(door_ent, ent, res)

        for output in res.find_all('AddOut'):
            door_ent.add_out(
                Output(
                    out=output['Output', 'OnUse'],
                    inp=output['Input', 'Use'],
                    targ=output['Target', ''],
                    inst_in=output['Inst_targ', None],
                    param=output['Param', ''],
                    delay=srctools.conv_float(output['Delay', '']),
                    times=(1 if srctools.conv_bool(output['OnceOnly',
                                                          False]) else -1),
                ))

        # Generate brush
        door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid]

    # Add or remove flags as needed
    for flag, value in zip(
        ('x', 'y', 'z', 'rev'),
        [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse],
    ):
        if flag not in flag_values:
            continue
        if door_ent is not None:
            if value:
                spawnflags |= flag_values[flag]
            else:
                spawnflags &= ~flag_values[flag]
        else:  # Place a KV setter to set this.
            vmf.create_ent(
                'comp_kv_setter',
                origin=setter_loc,
                target=name,
                mode='flags',
                kv_name=flag_values[flag],
                kv_value_global=value,
            )
    if door_ent is not None:
        door_ent['spawnflags'] = spawnflags

    # This ent uses a keyvalue for reversing...
    if door_type == 'momentary_rot_button':
        vmf.create_ent(
            'comp_kv_setter',
            origin=setter_loc,
            target=name,
            mode='kv',
            kv_name='StartDirection',
            kv_value_global='1' if reverse else '-1',
        )
Exemple #19
0
    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        for picker_name, picker_var in picker_vars:
            picker_val = temp_data.picker_results.get(picker_name, None)
            if picker_val is not None:
                inst.fixup[picker_var] = picker_val.value
            else:
                inst.fixup[picker_var] = ''
Exemple #20
0
def res_import_template(vmf: VMF, coll: Collisions, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    sense_offset = res.vec('senseOffset')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return place_template
Exemple #21
0
def res_add_brush(vmf: VMF, inst: Entity, res: Property) -> None:
    """Spawn in a brush at the indicated points.

    - `point1` and `point2` are locations local to the instance, with `0 0 0`
      as the floor-position.
    - `type` is either `black` or `white`.
    - detail should be set to `1/0`. If true the brush will be a
      func_detail instead of a world brush.

    The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor
    textures as needed.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])

    point1 = Vec.from_str(res['point1'])
    point2 = Vec.from_str(res['point2'])

    point1.z -= 64  # Offset to the location of the floor
    point2.z -= 64

    # Rotate to match the instance
    point1 = point1 @ angles + origin
    point2 = point2 @ angles + origin

    try:
        tex_type = texturing.Portalable(res['type', 'black'])
    except ValueError:
        LOGGER.warning(
            'AddBrush: "{}" is not a valid brush '
            'color! (white or black)',
            res['type'],
        )
        tex_type = texturing.Portalable.BLACK

    dim = round(point2 - point1, 6)
    dim.max(-dim)

    # Figure out what grid size and scale is needed
    # Check the dimensions in two axes to figure out the largest
    # tile size that can fit in it.
    tile_grids = {
        'x': tiling.TileSize.TILE_4x4,
        'y': tiling.TileSize.TILE_4x4,
        'z': tiling.TileSize.TILE_4x4,
    }

    for axis in 'xyz':
        u, v = Vec.INV_AXIS[axis]
        max_size = min(dim[u], dim[v])
        if max_size % 128 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_1x1
        elif dim[u] % 64 == 0 and dim[v] % 128 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_2x1
        elif max_size % 64 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_2x2
        else:
            tile_grids[axis] = tiling.TileSize.TILE_4x4

    solids = vmf.make_prism(point1, point2)

    solids.north.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.N),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['y'])
    solids.south.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.S),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['y'])
    solids.east.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.E),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['x'])
    solids.west.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.W),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['x'])
    solids.top.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.T),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['z'])
    solids.bottom.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.B),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['z'])

    if res.bool('detail'):
        # Add the brush to a func_detail entity
        vmf.create_ent(classname='func_detail').solids = [solids.solid]
    else:
        # Add to the world
        vmf.add_brush(solids.solid)
Exemple #22
0
def res_set_texture(inst: Entity, res: Property):
    """Set the tile at a particular place to use a specific texture.

    This can only be set for an entire voxel side at once.

    `pos` is the position, relative to the instance (0 0 0 is the floor-surface).
    `dir` is the normal of the texture (pointing out)
    If `gridPos` is true, the position will be snapped so it aligns with
     the 128 brushes (Useful with fizzler/light strip items).

    `tex` is the texture to use.

    If `template` is set, the template should be an axis aligned cube. This
    will be rotated by the instance angles, and then the face with the same
    orientation will be applied to the face (with the rotation and texture).
    """
    angles = Angle.from_str(inst['angles'])
    origin = Vec.from_str(inst['origin'])

    pos = Vec.from_str(res['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos.localise(origin, angles)

    norm = round(Vec.from_str(res['dir', '0 0 1']) @ angles, 6)

    if srctools.conv_bool(res['gridpos', '0']):
        for axis in 'xyz':
            # Don't realign things in the normal's axis -
            # those are already fine.
            if not norm[axis]:
                pos[axis] //= 128
                pos[axis] *= 128
                pos[axis] += 64

    try:
        # The user expects the tile to be at it's surface pos, not the
        # position of the voxel.
        tile = tiling.TILES[(pos - 64 * norm).as_tuple(), norm.as_tuple()]
    except KeyError:
        LOGGER.warning(
            '"{}": Could not find tile at {} with orient {}!',
            inst['targetname'],
            pos,
            norm,
        )
        return

    temp_id = inst.fixup.substitute(res['template', ''])
    if temp_id:
        temp = template_brush.get_scaling_template(temp_id).rotate(
            angles, origin)
    else:
        temp = template_brush.ScalingTemplate.world()

    tex = inst.fixup.substitute(res['tex', ''])

    if tex.startswith('<') and tex.endswith('>'):
        LOGGER.warning(
            'Special <lookups> for AlterTexture are '
            'no longer usable! ("{}")', tex)
    elif tex.startswith('[') and tex.endswith(']'):
        gen, name = texturing.parse_name(tex[1:-1])
        tex = gen.get(pos - 64 * norm, name)

    tile.override = (tex, temp)
Exemple #23
0
 def mins(self) -> Vec:
     """Return the minimums, as a Vector."""
     return Vec(self.min_x, self.min_y, self.min_z)
Exemple #24
0
def res_set_tile(inst: Entity, res: Property) -> None:
    """Set 4x4 parts of a tile to the given values.

    `Offset` defines the position of the upper-left tile in the grid.
    Each `Tile` section defines a row of the positions to edit like so:
        "Tile" "bbbb"
        "Tile" "b..b"
        "Tile" "b..b"
        "Tile" "bbbb"
    If `Force` is true, the specified tiles will override any existing ones
    and create the tile if necessary.
    Otherwise they will be merged in - white/black tiles will not replace
    tiles set to nodraw or void for example.
    `chance`, if specified allows producing irregular tiles by randomly not
    changing the tile.

    If you need less regular placement (other orientation, precise positions)
    use a bee2_template_tilesetter in a template.

    Allowed tile characters:
    - `W`: White tile.
    - `w`: White 4x4 only tile.
    - `B`: Black tile.
    - `b`: Black 4x4 only tile.
    - `g`: The side/bottom of goo pits.
    - `n`: Nodraw surface.
    - `i`: Invert the tile surface, if black/white.
    - `1`: Convert to a 1x1 only tile, if a black/white tile.
    - `4`: Convert to a 4x4 only tile, if a black/white tile.
    - `.`: Void (remove the tile in this position).
    - `_` or ` `: Placeholder (don't modify this space).
    - `x`: Cutout Tile (Broken)
    - `o`: Cutout Tile (Partial)
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin

    norm = round(orient.up(), 6)

    force_tile = res.bool('force')

    tiles: list[str] = [
        row.value for row in res if row.name in ('tile', 'tiles')
    ]
    if not tiles:
        raise ValueError('No "tile" parameters in SetTile!')

    chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0)
    if chance < 100.0:
        rng = rand.seed(b'tile', inst, res['seed', ''])
    else:
        rng = None

    for y, row in enumerate(tiles):
        for x, val in enumerate(row):
            if val in '_ ':
                continue

            if rng is not None and rng.uniform(0, 100) > chance:
                continue

            pos = Vec(32 * x, -32 * y, 0) @ orient + offset

            if val == '4':
                size = tiling.TileSize.TILE_4x4
            elif val == '1':
                size = tiling.TileSize.TILE_1x1
            elif val == 'i':
                size = None
            else:
                try:
                    new_tile = tiling.TILETYPE_FROM_CHAR[val]
                except KeyError:
                    LOGGER.warning('Unknown tiletype "{}"!', val)
                else:
                    tiling.edit_quarter_tile(pos, norm, new_tile, force_tile)
                continue

            # Edit the existing tile.
            try:
                tile, u, v = tiling.find_tile(pos, norm, force_tile)
            except KeyError:
                LOGGER.warning(
                    'Expected tile, but none found: {}, {}',
                    pos,
                    norm,
                )
                continue

            if size is None:
                # Invert the tile.
                tile[u, v] = tile[u, v].inverted
                continue

            # Unless forcing is enabled don't alter the size of GOO_SIDE.
            if tile[u, v].is_tile and tile[u,
                                           v] is not tiling.TileType.GOO_SIDE:
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tile[u, v].color)
            elif force_tile:
                # If forcing, make it black. Otherwise no need to change.
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tiling.Portalable.BLACK)
Exemple #25
0
# Position -> entity
# We merge ones within 3 blocks of our item.
CHECKPOINT_TRIG: dict[tuple[float, float, float], Entity] = {}

# Approximately a 3-distance from
# the center.
#   x
#  xxx
# xx xx
#  xxx
#   x
CHECKPOINT_NEIGHBOURS = list(
    Vec.iter_grid(
        Vec(-128, -128, 0),
        Vec(128, 128, 0),
        stride=128,
    ))
CHECKPOINT_NEIGHBOURS.extend([
    Vec(-256, 0, 0),
    Vec(256, 0, 0),
    Vec(0, -256, 0),
    Vec(0, 256, 0),
])
# Don't include ourself..
CHECKPOINT_NEIGHBOURS.remove(Vec(0, 0, 0))


@conditions.make_result('CheckpointTrigger')
def res_checkpoint_trigger(inst: Entity, res: Property) -> None:
    """Generate a trigger underneath coop checkpoint items.
Exemple #26
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
Exemple #27
0
 def find_bbox(self, p1: Vec, p2: Vec) -> Iterator[ValueT]:
     """Find all values intersecting the given bounding box."""
     mins, maxs = Vec.bbox(p1, p2)
     for holder_id in self.tree.intersection((*mins, *maxs)):
         yield from self._by_id[holder_id].values
Exemple #28
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,
Exemple #29
0
def _parse_template(loc: UnparsedTemplate) -> Template:
    """Parse a template VMF."""
    filesys: FileSystem
    if os.path.isdir(loc.pak_path):
        filesys = RawFileSystem(loc.pak_path)
    else:
        ext = os.path.splitext(loc.pak_path)[1].casefold()
        if ext in ('.bee_pack', '.zip'):
            filesys = ZipFileSystem(loc.pak_path)
        elif ext == '.vpk':
            filesys = VPKFileSystem(loc.pak_path)
        else:
            raise ValueError(f'Unknown filesystem type for "{loc.pak_path}"!')

    with filesys[loc.path].open_str() as f:
        props = Property.parse(f, f'{loc.pak_path}:{loc.path}')
    vmf = srctools.VMF.parse(props, preserve_ids=True)
    del props, filesys, f  # Discard all this data.

    # visgroup -> list of brushes/overlays
    detail_ents: dict[str, list[Solid]] = defaultdict(list)
    world_ents: dict[str, list[Solid]] = defaultdict(list)
    overlay_ents: dict[str, list[Entity]] = defaultdict(list)

    color_pickers: list[ColorPicker] = []
    tile_setters: list[TileSetter] = []
    voxel_setters: list[VoxelSetter] = []

    conf_ents = vmf.by_class['bee2_template_conf']
    if len(conf_ents) > 1:
        raise ValueError(
            f'Multiple configuration entities in template "{loc.id}"!')
    elif not conf_ents:
        raise ValueError(f'No configration entity for template "{loc.id}"!')
    else:
        [conf] = conf_ents

    if conf['template_id'].upper() != loc.id:
        raise ValueError(
            f'Mismatch in template IDs for {conf["template_id"]} and {loc.id}')

    def yield_world_detail() -> Iterator[tuple[list[Solid], bool, set[str]]]:
        """Yield all world/detail solids in the map.

        This also indicates if it's a func_detail, and the visgroup IDs.
        (Those are stored in the ent for detail, and the solid for world.)
        """
        for brush in vmf.brushes:
            yield [brush], False, brush.visgroup_ids
        for detail in vmf.by_class['func_detail']:
            yield detail.solids, True, detail.visgroup_ids

    force = conf['temp_type']
    force_is_detail: Optional[bool]
    if force.casefold() == 'detail':
        force_is_detail = True
    elif force.casefold() == 'world':
        force_is_detail = False
    else:
        force_is_detail = None

    visgroup_names = {vis.id: vis.name.casefold() for vis in vmf.vis_tree}
    conf_auto_visgroup = 1 if srctools.conv_bool(
        conf['detail_auto_visgroup']) else 0

    if not srctools.conv_bool(conf['discard_brushes']):
        for brushes, is_detail, vis_ids in yield_world_detail():
            visgroups = list(map(visgroup_names.__getitem__, vis_ids))
            if len(visgroups) > 1:
                raise ValueError('Template "{}" has brush with two '
                                 'visgroups! ({})'.format(
                                     loc.id, ', '.join(visgroups)))
            # No visgroup = ''
            visgroup = visgroups[0] if visgroups else ''

            # Auto-visgroup puts func_detail ents in unique visgroups.
            if is_detail and not visgroup and conf_auto_visgroup:
                visgroup = '__auto_group_{}__'.format(conf_auto_visgroup)
                # Reuse as the unique index, >0 are True too..
                conf_auto_visgroup += 1

            # Check this after auto-visgroup, so world/detail can be used to
            # opt into the grouping, then overridden to be the same.
            if force_is_detail is not None:
                is_detail = force_is_detail

            if is_detail:
                detail_ents[visgroup].extend(brushes)
            else:
                world_ents[visgroup].extend(brushes)

    for ent in vmf.by_class['info_overlay']:
        visgroups = list(map(visgroup_names.__getitem__, ent.visgroup_ids))
        if len(visgroups) > 1:
            raise ValueError('Template "{}" has overlay with two '
                             'visgroups! ({})'.format(loc.id,
                                                      ', '.join(visgroups)))
        # No visgroup = ''
        overlay_ents[visgroups[0] if visgroups else ''].append(ent)

    for ent in vmf.by_class['bee2_template_colorpicker']:
        # Parse the colorpicker data.
        try:
            priority = Decimal(ent['priority'])
        except ArithmeticError:
            LOGGER.warning(
                'Bad priority for colorpicker in "{}" template!',
                loc.id,
            )
            priority = Decimal(0)

        try:
            remove_after = AfterPickMode(ent['remove_brush', '0'])
        except ValueError:
            LOGGER.warning(
                'Bad remove-brush mode for colorpicker in "{}" template!',
                loc.id,
            )
            remove_after = AfterPickMode.NONE

        color_pickers.append(
            ColorPicker(
                priority=priority,
                name=ent['targetname'],
                visgroups=set(map(visgroup_names.__getitem__,
                                  ent.visgroup_ids)),
                offset=Vec.from_str(ent['origin']),
                normal=Vec(x=1) @ Angle.from_str(ent['angles']),
                sides=ent['faces'].split(' '),
                grid_snap=srctools.conv_bool(ent['grid_snap']),
                after=remove_after,
                use_pattern=srctools.conv_bool(ent['use_pattern']),
                force_tex_white=ent['tex_white'],
                force_tex_black=ent['tex_black'],
            ))

    for ent in vmf.by_class['bee2_template_voxelsetter']:
        tile_type = TILE_SETTER_SKINS[srctools.conv_int(ent['skin'])]

        voxel_setters.append(
            VoxelSetter(
                offset=Vec.from_str(ent['origin']),
                normal=Vec(z=1) @ Angle.from_str(ent['angles']),
                visgroups=set(map(visgroup_names.__getitem__,
                                  ent.visgroup_ids)),
                tile_type=tile_type,
                force=srctools.conv_bool(ent['force']),
            ))

    for ent in vmf.by_class['bee2_template_tilesetter']:
        tile_type = TILE_SETTER_SKINS[srctools.conv_int(ent['skin'])]
        color = ent['color']
        if color == 'tile':
            try:
                color = tile_type.color
            except ValueError:
                # Non-tile types.
                color = None
        elif color == 'invert':
            color = 'INVERT'
        elif color == 'match':
            color = None
        elif color != 'copy':
            raise ValueError('Invalid TileSetter color '
                             '"{}" for "{}"'.format(color, loc.id))

        tile_setters.append(
            TileSetter(
                offset=Vec.from_str(ent['origin']),
                normal=Vec(z=1) @ Angle.from_str(ent['angles']),
                visgroups=set(map(visgroup_names.__getitem__,
                                  ent.visgroup_ids)),
                color=color,
                tile_type=tile_type,
                picker_name=ent['color_picker'],
                force=srctools.conv_bool(ent['force']),
            ))

    coll: list[CollisionDef] = []
    for ent in vmf.by_class['bee2_collision_bbox']:
        visgroups = set(map(visgroup_names.__getitem__, ent.visgroup_ids))
        for bbox in collisions.BBox.from_ent(ent):
            coll.append(CollisionDef(bbox, visgroups))

    return Template(
        loc.id,
        set(visgroup_names.values()),
        world_ents,
        detail_ents,
        overlay_ents,
        conf['skip_faces'].split(),
        conf['realign_faces'].split(),
        conf['overlay_faces'].split(),
        conf['vertical_faces'].split(),
        color_pickers,
        tile_setters,
        voxel_setters,
        coll,
    )
Exemple #30
0
 def maxes(self) -> Vec:
     """Return the minimums, as a Vector."""
     return Vec(self.max_x, self.max_y, self.max_z)