Example #1
0
def combine_group(
    compiler: ModelCompiler,
    props: List[StaticProp],
    lookup_model: Callable[[str], Tuple[QC, Model]],
) -> StaticProp:
    """Merge the given props together, compiling a model if required."""

    # We want to allow multiple props to reuse the same model.
    # To do this try and match prop groups to each other, by "unifying"
    # them into a consistent orientation.
    #
    # If there are matches in different orientations, they're most likely
    # 90 degree or other rotations in the yaw axis. So we compute the average,
    # and subtract that out.

    avg_pos = Vec()
    avg_yaw = 0.0

    visleafs = set()  # type: Set[int]

    for prop in props:
        avg_pos += prop.origin
        avg_yaw += prop.angles.yaw
        visleafs.update(prop.visleafs)

    # Snap to nearest 15 degrees to keep the models themselves not
    # strangely rotated.
    avg_yaw = round(avg_yaw / (15 * len(props))) * 15.0
    avg_pos /= len(props)
    yaw_rot = Matrix.from_yaw(-avg_yaw)

    prop_pos = set()
    for prop in props:
        origin = round((prop.origin - avg_pos) @ yaw_rot, 7)
        angles = round(Vec(prop.angles), 7)
        angles.y -= avg_yaw
        try:
            coll = CollType(prop.solidity)
        except ValueError:
            raise ValueError('Unknown prop_static collision type '
                             '{} for "{}" at {}!'.format(
                                 prop.solidity,
                                 prop.model,
                                 prop.origin,
                             ))
        prop_pos.add(
            PropPos(
                origin.x,
                origin.y,
                origin.z,
                angles.x,
                angles.y,
                angles.z,
                prop.model,
                prop.skin,
                prop.scaling,
                coll,
            ))
    # We don't want to build collisions if it's not used.
    has_coll = any(pos.solidity is not CollType.NONE for pos in prop_pos)
    mdl_name, result = compiler.get_model(
        (frozenset(prop_pos), has_coll),
        compile_func,
        lookup_model,
    )

    # Many of these we require to be the same, so we can read them
    # from any of the component props.
    return StaticProp(
        model=mdl_name,
        origin=avg_pos,
        angles=Angle(0, avg_yaw - 90, 0),
        scaling=1.0,
        visleafs=sorted(visleafs),
        solidity=(CollType.VPHYS if has_coll else CollType.NONE).value,
        flags=props[0].flags,
        lighting_origin=avg_pos,
        tint=props[0].tint,
        renderfx=props[0].renderfx,
    )
Example #2
0
def group_props_ent(
    prop_groups: Dict[Optional[tuple], List[StaticProp]],
    rejected: List[StaticProp],
    get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]],
    bbox_ents: List[Entity],
    min_cluster: int,
) -> Iterator[List[StaticProp]]:
    """Given the groups of props, merge props according to the provided ents."""
    # Ents with group names. We have to split those by filter too.
    grouped_sets: Dict[Tuple[str, FrozenSet[str]], CombineVolume] = {}
    # Skinset filter -> volumes that match.
    sets_by_skin: Dict[FrozenSet[str], List[CombineVolume]] = defaultdict(list)

    empty_fs = frozenset('')

    for ent in bbox_ents:
        origin = Vec.from_str(ent['origin'])

        skinset = empty_fs
        mdl_name = ent['prop']
        if mdl_name:
            qc, mdl = get_model(mdl_name)
            if mdl is not None:
                skinset = frozenset({
                    tex.casefold().replace('\\', '/')
                    for tex in mdl.iter_textures([conv_int(ent['skin'])])
                })

        # Compute 6 planes to use for collision detection.
        mat = Matrix.from_angle(Angle.from_str(ent['angles']))
        mins, maxes = Vec.bbox(
            Vec.from_str(ent['mins']),
            Vec.from_str(ent['maxs']),
        )
        size = maxes - mins
        # Enlarge slightly to ensure it never has a zero area.
        # Otherwise the normal could potentially be invalid.
        mins -= 0.05
        maxes += 0.05

        # Group name
        group_name = ent['name']

        if group_name:
            try:
                combine_set = grouped_sets[group_name, skinset]
            except KeyError:
                combine_set = grouped_sets[group_name,
                                           skinset] = CombineVolume(
                                               group_name, skinset, origin)
                sets_by_skin[skinset].append(combine_set)
        else:
            combine_set = CombineVolume(group_name, skinset, origin)
            sets_by_skin[skinset].append(combine_set)

        combine_set.volume += size.x * size.y * size.z
        # For each direction, compute a position on the plane and
        # the normal vector.
        combine_set.collision.append([(
            origin + Vec.with_axes(axis, offset) @ mat,
            Vec.with_axes(axis, norm) @ mat,
        ) for offset, norm in zip([mins, maxes], (-1, 1))
                                      for axis in ('x', 'y', 'z')])

    # We want to apply a ordering to groups, so smaller ones apply first, and
    # filtered ones override all others.
    for group_list in sets_by_skin.values():
        group_list.sort(key=operator.attrgetter('volume'))
    # Groups with no filter have no skins in the group.
    unfiltered_group = sets_by_skin.get(frozenset(), [])

    # Each of these groups cannot be merged with other ones.
    for group_key, group in prop_groups.items():
        if group_key is None:
            continue

        # No point merging single/empty groups.
        group_skinset = group_key[0]
        if len(group) < min_cluster:
            rejected.extend(group)
            group.clear()
            continue

        for combine_set in itertools.chain(sets_by_skin.get(group_skinset, ()),
                                           unfiltered_group):
            found = []
            for prop in list(group):
                if combine_set.contains(prop.origin):
                    found.append(prop)
                    combine_set.used = True

            actual = set(found).intersection(group)
            if len(actual) >= min_cluster:
                yield list(actual)
                for prop in actual:
                    group.remove(prop)

    # Finally, reject all the ones not in a bbox.
    for group in prop_groups.values():
        rejected.extend(group)
    # And log unused groups
    for combine_set_list in sets_by_skin.values():
        for combine_set in combine_set_list:
            if not combine_set.used:
                LOGGER.warning('Unused comp_propcombine_set {}',
                               combine_set.desc)
Example #3
0
def parse_antlines(vmf: VMF) -> tuple[
    dict[str, list[Antline]],
    dict[int, list[Segment]]
]:
    """Convert overlays in the map into Antline objects.

    This returns two dicts. The first maps targetnames to lists of antlines.
    The second maps solid IDs to segments, for assigning TileDefs to them.
    """
    # We want to reconstruct the shape of the antline path.
    # To do that we find the neighbouring points for each overlay.

    LOGGER.info('Parsing antlines...')

    # segment -> found neighbours of it.
    overlay_joins: defaultdict[Segment, set[Segment]] = defaultdict(set)

    segment_to_name: dict[Segment, str] = {}

    # Points on antlines where two can connect. For corners that's each side,
    # for straight it's each end. Combine that with the targetname
    # so we only join related antlines.
    join_points: dict[tuple[str, float, float, float], Segment] = {}

    mat_straight = consts.Antlines.STRAIGHT
    mat_corner = consts.Antlines.CORNER

    side_to_seg: dict[int, list[Segment]] = {}
    antlines: dict[str, list[Antline]] = {}

    for over in vmf.by_class['info_overlay']:
        mat = over['material']
        origin = Vec.from_str(over['basisorigin'])
        normal = Vec.from_str(over['basisnormal'])
        orient = Matrix.from_angle(Angle.from_str(over['angles']))

        if mat == mat_corner:
            seg_type = SegType.CORNER
            start = end = origin

            # One on each side - we know the size.
            points = [
                origin + orient.left(-8.0),
                origin + orient.left(+8.0),
                origin + orient.forward(-8.0),
                origin + orient.forward(+8.0),
            ]
        elif mat == mat_straight:
            seg_type = SegType.STRAIGHT

            # We want to determine the length first.
            long_axis = orient.left()
            side_axis = orient.forward()

            # The order of these isn't correct, but we need the neighbours to
            # fix that.
            start, end = overlay_bounds(over)
            # For whatever reason, Valve sometimes generates antlines which are
            # shortened by 1 unit. So snap those to grid.
            start = round(start / 16, 0) * 16
            end = round(end / 16, 0) * 16

            if math.isclose(Vec.dot(end - start, long_axis), 16.0):
                # Special case.
                # 1-wide antlines don't have the correct
                # rotation, pointing always in the U axis.
                # So we need to figure that out to get the correct links.
                # For now just create the segment with dummy values.
                start = end = origin
                points = []
            else:
                offset: Vec = round(abs(8 * side_axis), 0)
                start += offset
                end -= offset

                points = [start, end]
        else:
            # It's not an antline.
            continue

        seg = Segment(seg_type, normal, start, end)
        segment_to_name[seg] = over_name = over['targetname']

        for side_id in over['sides'].split():
            side_to_seg.setdefault(int(side_id), []).append(seg)

        for point in points:
            # Lookup the point to see if we've already checked it.
            # If not, write us into that spot.
            neighbour = join_points.setdefault(
                (over_name, ) + point.as_tuple(),
                seg,
            )
            if neighbour is seg:
                # None found
                continue
            overlay_joins[neighbour].add(seg)
            overlay_joins[seg].add(neighbour)

        # Remove original from the map.
        over.remove()

    # Now fix the square straight segments.
    for seg, over_name in segment_to_name.items():
        if seg.type is SegType.STRAIGHT and seg.start == seg.end:
            fix_single_straight(seg, over_name, join_points, overlay_joins)

    # Now, finally compute each continuous section.
    for start_seg, over_name in segment_to_name.items():
        try:
            neighbours = overlay_joins[start_seg]
        except KeyError:
            continue  # Done already.

        if len(neighbours) != 1:
            continue
        # Found a start point!
        segments = [start_seg]

        for segment in segments:
            neighbours = overlay_joins.pop(segment)
            # Except KeyError: this segment's already done??
            for neighbour in neighbours:
                if neighbour not in segments:
                    segments.append(neighbour)

        antlines.setdefault(over_name, []).append(Antline(over_name, segments))

    LOGGER.info('Done! ({} antlines)'.format(sum(map(len, antlines.values()))))
    return antlines, side_to_seg
Example #4
0
def res_antlaser(vmf: VMF, res: Property) -> object:
    """The condition to generate AntLasers and Antline Corners.

    This is executed once to modify all instances.
    """
    conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>',
                                            silent=True)
    conf_inst_laser = instanceLocs.resolve(res['instance'])
    conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64)
    conf_las_start = Vec(z=res.float('LasStart') - 64)
    conf_rope_off = res.vec('RopePos')
    conf_toggle_targ = res['toggleTarg', '']

    beam_conf = res.find_key('BeamKeys', or_blank=True)
    glow_conf = res.find_key('GlowKeys', or_blank=True)
    cable_conf = res.find_key('CableKeys', or_blank=True)

    if beam_conf:
        # Grab a copy of the beam spawnflags so we can set our own options.
        conf_beam_flags = beam_conf.int('spawnflags')
        # Mask out certain flags.
        conf_beam_flags &= (
            0
            | 1  # Start On
            | 2  # Toggle
            | 4  # Random Strike
            | 8  # Ring
            | 16  # StartSparks
            | 32  # EndSparks
            | 64  # Decal End
            #| 128  # Shade Start
            #| 256  # Shade End
            #| 512  # Taper Out
        )
    else:
        conf_beam_flags = 0

    conf_outputs = [
        Output.parse(prop) for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes: dict[str, Node] = {}

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()
        name = inst['targetname']
        if filename in conf_inst_laser:
            node_type = NodeType.LASER
        elif filename in conf_inst_corner:
            node_type = NodeType.CORNER
        else:
            continue

        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            item = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None
        pos = Vec.from_str(inst['origin'])
        orient = Matrix.from_angle(Angle.from_str(inst['angles']))
        if node_type is NodeType.CORNER:
            timer_delay = item.inst.fixup.int('$timer_delay')
            # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not
            # being selectable issue.
            pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos
        nodes[name] = Node(node_type, inst, item, pos, orient)

    if not nodes:
        # None at all.
        return conditions.RES_EXHAUSTED

    # Now find every connected group, recording inputs, outputs and links.
    todo = set(nodes.values())

    groups: list[Group] = []

    while todo:
        start = todo.pop()
        # Synthesise the Item used for logic.
        # We use a random info_target to manage the IO data.
        group = Group(start, start.type)
        groups.append(group)
        for node in group.nodes:
            # If this node has no non-node outputs, destroy the antlines.
            has_output = False
            node.is_grouped = True

            for conn in list(node.item.outputs):
                neighbour = conn.to_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({node, neigh_node}))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                node.item.transfer_antlines(group.item)
            else:
                node.item.delete_antlines()

            # Do the same for inputs, so we can catch that.
            for conn in list(node.item.inputs):
                neighbour = conn.from_item
                neigh_node = nodes.get(neighbour.name, None)
                todo.discard(neigh_node)
                if neigh_node is None or neigh_node.type is not node.type:
                    # Not a node or different item type, it must therefore
                    # be a target of our logic.
                    conn.to_item = group.item
                    node.had_input = True
                    continue
                elif not neigh_node.is_grouped:
                    # Another node.
                    group.nodes.append(neigh_node)
                # else: True, node already added.

                # For nodes, connect link.
                conn.remove()
                group.links.add(frozenset({neigh_node, node}))

    # Now every node is in a group. Generate the actual entities.
    for group in groups:
        # We generate two ent types. For each marker, we add a sprite
        # and a beam pointing at it. Then for each connection
        # another beam.

        # Choose a random item name to use for our group.
        base_name = group.nodes[0].item.name

        out_enable = [Output('', '', 'FireUser2')]
        out_disable = [Output('', '', 'FireUser1')]
        if group.type is NodeType.LASER:
            for output in conf_outputs:
                if output.output.casefold() == 'onenabled':
                    out_enable.append(output.copy())
                else:
                    out_disable.append(output.copy())

        group.item.enable_cmd = tuple(out_enable)
        group.item.disable_cmd = tuple(out_disable)

        if group.type is NodeType.LASER and conf_toggle_targ:
            # Make the group info_target into a texturetoggle.
            toggle = group.item.inst
            toggle['classname'] = 'env_texturetoggle'
            toggle['target'] = conditions.local_name(group.nodes[0].inst,
                                                     conf_toggle_targ)

        # Node -> index for targetnames.
        indexes: dict[Node, int] = {}

        # For antline corners, the antline segments.
        segments: list[antlines.Segment] = []

        # frozenset[Node] unpacking isn't clear.
        node_a: Node
        node_b: Node

        if group.type is NodeType.CORNER:
            for node_a, node_b in group.links:
                # Place a straight antline between each connected node.
                # If on the same plane, we only need one. If not, we need to
                # do one for each plane it's in.
                offset = node_b.pos - node_a.pos
                up_a = node_a.orient.up()
                up_b = node_b.orient.up()
                plane_a = Vec.dot(node_a.pos, up_a)
                plane_b = Vec.dot(node_b.pos, up_b)
                if Vec.dot(up_a, up_b) > 0.9:
                    if abs(plane_a - plane_b) > 1e-6:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are on different planes',
                            node_a.item.name,
                            node_b.item.name,
                        )
                        continue
                    u = node_a.orient.left()
                    v = node_a.orient.forward()
                    # Which are we aligned to?
                    if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot(
                            offset, v)) < 1e-6:
                        forward = offset.norm()
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * forward,
                            node_b.pos - 8.0 * forward,
                        )
                    else:
                        LOGGER.warning(
                            'Antline corners "{}" - "{}" '
                            'are not directly aligned',
                            node_a.item.name,
                            node_b.item.name,
                        )
                else:
                    # We expect them be aligned to each other.
                    side = Vec.cross(up_a, up_b)
                    if abs(Vec.dot(side, offset)) < 1e-6:
                        mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b
                        mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a
                        if mid1 != mid2:
                            LOGGER.warning(
                                'Midpoint mismatch: {} != {} for "{}" - "{}"',
                                mid1,
                                mid2,
                                node_a.item.name,
                                node_b.item.name,
                            )
                        group.add_ant_straight(
                            up_a,
                            node_a.pos + 8.0 * (mid1 - node_a.pos).norm(),
                            mid1,
                        )
                        group.add_ant_straight(
                            up_b,
                            node_b.pos + 8.0 * (mid2 - node_b.pos).norm(),
                            mid2,
                        )

        # For cables, it's a bit trickier than the beams.
        # The cable ent itself is the one which decides what it links to,
        # so we need to potentially make endpoint cables at locations with
        # only "incoming" lines.
        # So this dict is either a targetname to indicate cables with an
        # outgoing connection, or the entity for endpoints without an outgoing
        # connection.
        cable_points: dict[Node, Union[Entity, str]] = {}

        for i, node in enumerate(group.nodes, start=1):
            indexes[node] = i
            node.item.name = base_name

            if group.type is NodeType.CORNER:
                node.inst.remove()
                # Figure out whether we want a corner at this point, or
                # just a regular dot. If a non-node input was provided it's
                # always a corner. Otherwise it's one if there's an L, T or X
                # junction.
                use_corner = True
                norm = node.orient.up().as_tuple()
                if not node.had_input:
                    neighbors = [
                        mag * direction for direction in [
                            node.orient.forward(),
                            node.orient.left(),
                        ] for mag in [-8.0, 8.0]
                        if ((node.pos + mag * direction).as_tuple(),
                            norm) in group.ant_seg
                    ]
                    if len(neighbors) == 2:
                        [off1, off2] = neighbors
                        if Vec.dot(off1, off2) < -0.99:
                            # ---o---, merge together. The endpoints we want
                            # are the other ends of the two segments.
                            group.add_ant_straight(
                                node.orient.up(),
                                group.rem_ant_straight(norm, node.pos + off1),
                                group.rem_ant_straight(norm, node.pos + off2),
                            )
                            use_corner = False
                    elif len(neighbors) == 1:
                        # o-----, merge.
                        [offset] = neighbors
                        group.add_ant_straight(
                            node.orient.up(),
                            group.rem_ant_straight(norm, node.pos + offset),
                            node.pos - offset,
                        )
                        use_corner = False
                if use_corner:
                    segments.append(
                        antlines.Segment(
                            antlines.SegType.CORNER,
                            round(node.orient.up(), 3),
                            Vec(node.pos),
                            Vec(node.pos),
                        ))
            elif group.type is NodeType.LASER:
                sprite_pos = node.pos + conf_glow_height @ node.orient

                if glow_conf:
                    # First add the sprite at the right height.
                    sprite = vmf.create_ent('env_sprite')
                    for prop in glow_conf:
                        sprite[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    sprite['origin'] = sprite_pos
                    sprite['targetname'] = NAME_SPR(base_name, i)
                elif beam_conf:
                    # If beams but not sprites, we need a target.
                    vmf.create_ent(
                        'info_target',
                        origin=sprite_pos,
                        targetname=NAME_SPR(base_name, i),
                    )

                if beam_conf:
                    # Now the beam going from below up to the sprite.
                    beam_pos = node.pos + conf_las_start @ node.orient
                    beam = vmf.create_ent('env_beam')
                    for prop in beam_conf:
                        beam[prop.name] = conditions.resolve_value(
                            node.inst, prop.value)

                    beam['origin'] = beam['targetpoint'] = beam_pos
                    beam['targetname'] = NAME_BEAM_LOW(base_name, i)
                    beam['LightningStart'] = beam['targetname']
                    beam['LightningEnd'] = NAME_SPR(base_name, i)
                    beam['spawnflags'] = conf_beam_flags | 128  # Shade Start

        segments += set(group.ant_seg.values())
        if group.type is NodeType.CORNER and segments:
            group.item.antlines.add(
                antlines.Antline(group.item.name + '_antline', segments))

        if group.type is NodeType.LASER and beam_conf:
            for i, (node_a, node_b) in enumerate(group.links):
                beam = vmf.create_ent('env_beam')
                conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys')
                beam['origin'] = beam['targetpoint'] = node_a.pos
                beam['targetname'] = NAME_BEAM_CONN(base_name, i)
                beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a])
                beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b])
                beam['spawnflags'] = conf_beam_flags

        if group.type is NodeType.LASER and cable_conf:
            build_cables(
                vmf,
                group,
                cable_points,
                base_name,
                beam_conf,
                conf_rope_off,
            )

    return conditions.RES_EXHAUSTED
Example #5
0
    def read_from_map(self, vmf: VMF, has_attr: dict[str, bool],
                      items: dict[str, editoritems.Item]) -> None:
        """Given the map file, set blocks."""
        from precomp.instance_traits import get_item_id
        from precomp import bottomlessPit

        # Starting points to fill air and goo.
        # We want to fill goo first...
        air_search_locs: list[tuple[Vec, bool]] = []
        goo_search_locs: list[tuple[Vec, bool]] = []

        for ent in vmf.entities:
            str_pos = ent['origin', None]
            if str_pos is None:
                continue

            pos = world_to_grid(Vec.from_str(str_pos))

            # Exclude entities outside the main area - elevators mainly.
            # The border should never be set to air!
            if not ((0, 0, 0) <= pos <= (25, 25, 25)):
                continue

            # We need to manually set EmbeddedVoxel locations.
            # These might not be detected for items where there's a block
            # which is entirely empty - corridors and obs rooms for example.
            # We also need to check occupy locations, so that it can seed search
            # locs.
            item_id = get_item_id(ent)
            seeded = False
            if item_id:
                try:
                    item = items[item_id.casefold()]
                except KeyError:
                    pass
                else:
                    orient = Matrix.from_angle(Angle.from_str(ent['angles']))
                    for local_pos in item.embed_voxels:
                        # Offset down because 0 0 0 is the floor voxel.
                        world_pos = (Vec(local_pos) - (0, 0, 1)) @ orient + pos
                        self[round(world_pos, 0)] = Block.EMBED
                    for occu in item.occupy_voxels:
                        world_pos = Vec(occu.pos) @ orient + pos
                        air_search_locs.append((round(world_pos, 0), False))
                        seeded = True
            if not seeded:
                # Assume origin is its location.
                air_search_locs.append((pos.copy(), False))

        can_have_pit = bottomlessPit.pits_allowed()

        for brush in vmf.brushes[:]:
            tex = {face.mat.casefold() for face in brush.sides}

            bbox_min, bbox_max = brush.get_bbox()

            if ('nature/toxicslime_a2_bridge_intro' in tex
                    or 'nature/toxicslime_puzzlemaker_cheap' in tex):
                # It's goo!

                x = bbox_min.x + 64
                y = bbox_min.y + 64

                g_x = x // 128
                g_y = y // 128

                is_pit = can_have_pit and bottomlessPit.is_pit(
                    bbox_min, bbox_max)

                # If goo is multi-level, we want to record all pos!
                z_pos = range(int(bbox_min.z) + 64, int(bbox_max.z), 128)
                top_ind = len(z_pos) - 1
                for ind, z in enumerate(z_pos):
                    g_z = z // 128
                    self[g_x, g_y, g_z] = Block.from_pitgoo_attr(
                        is_pit,
                        is_top=(ind == top_ind),
                        is_bottom=(ind == 0),
                    )
                    # If goo has totally submerged tunnels, they are not filled.
                    # Add each horizontal neighbour to the search list.
                    # If not found they'll be ignored.
                    goo_search_locs += [
                        (Vec(g_x - 1, g_y, g_z), True),
                        (Vec(g_x + 1, g_y, g_z), True),
                        (Vec(g_x, g_y + 1, g_z), True),
                        (Vec(g_x, g_y - 1, g_z), True),
                    ]

                # Remove the brush, since we're not going to use it.
                vmf.remove_brush(brush)

                # Indicate that this map contains goo/pits
                if is_pit:
                    has_attr[VOICE_ATTR_PIT] = True
                else:
                    has_attr[VOICE_ATTR_GOO] = True

                continue

            pos = world_to_grid(brush.get_origin(bbox_min, bbox_max))

            if bbox_max - bbox_min == (128, 128, 128):
                # Full block..
                self[pos] = Block.SOLID
            else:
                # Must be an embbedvoxel block
                self[pos] = Block.EMBED

        LOGGER.info('Analysed map, filling air... ({} starting positions..)',
                    len(air_search_locs))
        self.fill_air(goo_search_locs + air_search_locs)
        LOGGER.info('Air filled!')
Example #6
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: Optional[Set[Tuple[int, int]]]
    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 = conditions.resolve_value(
                    inst, props['template'])
            else:
                panel.template = ''
        if 'nodraw' in props:
            panel.nodraw = srctools.conv_bool(
                conditions.resolve_value(inst, props['nodraw']))
        if 'seal' in props:
            panel.seal = srctools.conv_bool(
                conditions.resolve_value(inst, props['seal']))
        if 'move_bullseye' in props:
            panel.steals_bullseye = srctools.conv_bool(
                conditions.resolve_value(inst, props['move_bullseye']))
    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[Optional[Entity]] = {
            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']),
                    Vec.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
Example #7
0
"""Generate models for all the different fizzler sizes."""
from typing import NamedTuple, List, Tuple, Dict
from srctools.smd import Mesh, Bone, BoneFrame, Triangle, Vertex
from srctools import VMF, Vec, Angle
import os
import subprocess
from math import radians
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

# Name to use for the root bone.
ROOT_NAME = 'root'
CACHE = {}

STYLES = [
    ('clean', Angle(0, 0, 0), 'fizzler/fizz_effect_portal'),
    ('retro', Angle(0, 90, 0), 'fizzler/fizz_effect_retro_portal'),
]

P2_LOC = os.environ.get('PORTAL_2_LOC')
print('Portal 2: ', P2_LOC)


class Shape:
    """A fizzler size, plus the block geo."""
    def __init__(self, name: str, block: str,
                 points: List[Tuple[Vec, Angle]]) -> None:
        self.name = name
        self.points = points
        self.block_fname = block + '.smd'

    def __repr__(self) -> str:
Example #8
0
def group_props_ent(
    prop_groups: Dict[Optional[tuple], List[StaticProp]],
    rejected: List[StaticProp],
    get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]],
    bbox_ents: List[Entity],
    min_cluster: int,
) -> Iterator[List[StaticProp]]:
    """Given the groups of props, merge props according to the provided ents."""
    # (name, skinset) -> list of boxes, constructed as 6 (pos, norm) tuples.
    combine_sets = defaultdict(
        list
    )  # type: Dict[Tuple[str, FrozenSet[str]], List[List[Tuple[Vec, Vec]]]]

    empty_fs = frozenset('')

    for ent in bbox_ents:
        # Either provided name, or unique value.
        name = ent['name'] or format(int(ent['hammerid']), 'X')
        origin = Vec.from_str(ent['origin'])

        skinset = empty_fs

        mdl_name = ent['prop']
        if mdl_name:
            qc, mdl = get_model(mdl_name)
            if mdl is not None:
                skinset = frozenset({
                    tex.casefold().replace('\\', '/')
                    for tex in mdl.iter_textures([conv_int(ent['skin'])])
                })

        # Compute 6 planes to use for collision detection.
        mat = Matrix.from_angle(Angle.from_str(ent['angles']))
        mins, maxes = Vec.bbox(
            Vec.from_str(ent['mins']),
            Vec.from_str(ent['maxs']),
        )
        # Enlarge slightly to ensure it never has a zero area.
        # Otherwise the normal could potentially be invalid.
        mins -= 0.05
        maxes += 0.05

        # For each direction, compute a position on the plane and
        # the normal vector.
        combine_sets[name, skinset].append([(
            origin + Vec.with_axes(axis, offset) @ mat,
            Vec.with_axes(axis, norm) @ mat,
        ) for offset, norm in zip([mins, maxes], (-1, 1))
                                            for axis in ('x', 'y', 'z')])

    # Each of these groups cannot be merged with other ones.
    for group_key, group in prop_groups.items():
        if group_key is None:
            continue

        # No point merging single/empty groups.
        group_skinset = group_key[0]
        if len(group) < min_cluster:
            rejected.extend(group)
            group.clear()
            continue

        for (name, skinset), boxes in combine_sets.items():
            if skinset and skinset != group_skinset:
                continue  # No match
            found = defaultdict(list)  # type: Dict[int, List[StaticProp]]
            for prop in list(group):
                for box in boxes:
                    if bsp_collision(prop.origin, box):
                        # Group by this box object's identity.
                        # That's a cheap way to keep each propcombine set
                        # grouped uniquely.
                        found[id(boxes)].append(prop)
                        break

            for subgroup in found.values():
                actual = set(subgroup).intersection(group)
                if len(actual) >= min_cluster:
                    yield list(actual)
                    for prop in actual:
                        group.remove(prop)

    # Finally, reject all the ones not in a bbox.
    for group in prop_groups.values():
        rejected.extend(group)
Example #9
0
 def _parse_value(value: str) -> Angle:
     return Angle.from_str(value)
Example #10
0
 def _init_orient(self) -> Matrix:
     """We need to rotate the orient, because items have forward as negative X."""
     rot = Matrix.from_angle(Angle.from_str(self.ent['angles']))
     return Matrix.from_yaw(180) @ rot
Example #11
0
def comp_prop_rope(ctx: Context) -> None:
    """Build static props for ropes."""
    compiler = ModelCompiler.from_ctx(ctx, 'ropes')
    # id -> node.
    all_nodes: MutableMapping[NodeID, NodeEnt] = {}
    # Given a targetname, all the nodes with that name.
    name_to_nodes: MutableMapping[str, List[NodeEnt]] = defaultdict(list)
    # Group name -> nodes with that group.
    group_to_node: Dict[str, List[NodeEnt]] = defaultdict(list)
    # Store the node/next-key pairs for linking after they're all parsed.
    temp_conns: List[Tuple[NodeEnt, str]] = []

    for ent in ctx.vmf.by_class['comp_prop_rope'] | ctx.vmf.by_class[
            'comp_prop_cable']:
        ent.remove()
        conf = Config.parse(ent)
        node = NodeEnt(
            Vec.from_str(ent['origin']),
            conf,
            NodeID(ent['hammerid']),
            ent['group'].casefold(),
        )
        all_nodes[node.id] = node

        if node.group:
            group_to_node[node.group].append(node)
        if ent['targetname']:
            name_to_nodes[ent['targetname'].casefold()].append(node)
        if ent['nextkey']:
            temp_conns.append((node, ent['nextkey'].casefold()))

    if not all_nodes:
        return
    LOGGER.info('{} rope nodes found.', len(all_nodes))

    connections_to: Dict[NodeID, List[NodeEnt]] = defaultdict(list)
    connections_from: Dict[NodeID, List[NodeEnt]] = defaultdict(list)

    for node, target in temp_conns:
        found: List[NodeEnt] = []
        if target.endswith('*'):
            search = target[:-1]
            for name, nodes in name_to_nodes.items():
                if name.startswith(search):
                    found.extend(nodes)
        else:
            found.extend(name_to_nodes.get(target, ()))
        found.sort()
        for dest in found:
            connections_from[node.id].append(dest)
            connections_to[dest.id].append(node)

    static_props = list(ctx.bsp.static_props())
    vis_tree_top = ctx.bsp.vis_tree()

    # To group nodes, take each group out, then search recursively through
    # all connections from it to other nodes.
    todo = set(all_nodes.values())
    with compiler:
        while todo:
            node = todo.pop()
            connections: Set[Tuple[NodeID, NodeID]] = set()
            # We need the set for fast is-in checks, and the list
            # so we can loop through while modifying it.
            nodes: Set[NodeEnt] = {node}
            unchecked: List[NodeEnt] = [node]
            while unchecked:
                node = unchecked.pop()
                # Three links to others - connections to/from, and groups.
                # We'll only ever follow a path once, so pop from the dicts.
                if node.group:
                    for subnode in group_to_node.pop(node.group, ()):
                        if subnode not in nodes:
                            nodes.add(subnode)
                            unchecked.append(subnode)
                for conn_node in connections_from.pop(node.id, ()):
                    connections.add((node.id, conn_node.id))
                    if conn_node not in nodes:
                        nodes.add(conn_node)
                        unchecked.append(conn_node)
                for conn_node in connections_to.pop(node.id, ()):
                    connections.add((conn_node.id, node.id))
                    if conn_node not in nodes:
                        nodes.add(conn_node)
                        unchecked.append(conn_node)
            todo -= nodes
            if len(nodes) == 1:
                LOGGER.warning(
                    'Node at {} has no connections to it! Skipping.', node.pos)
                continue

            bbox_min, bbox_max = Vec.bbox(node.pos for node in nodes)
            center = (bbox_min + bbox_max) / 2
            node = None
            for node in nodes:
                node.pos -= center

            model_name, coll_data = compiler.get_model(
                (frozenset(nodes), frozenset(connections)),
                build_rope,
                center,
            )

            # Use the node closest to the center. That way
            # it shouldn't be inside walls, and be about representative of
            # the whole model.
            light_origin = min(
                (point for point1, radius1, point2, radius2 in coll_data
                 for point in [point1, point2]),
                key=lambda pos: (pos - center).mag_sq())

            # Compute the flags. Just pick a random node, from above.
            conf = node.config
            flags = StaticPropFlags.NONE
            if conf.prop_light_bounce:
                flags |= StaticPropFlags.BOUNCED_LIGHTING
            if conf.prop_no_shadows:
                flags |= StaticPropFlags.NO_SHADOW
            if conf.prop_no_vert_light:
                flags |= StaticPropFlags.NO_PER_VERTEX_LIGHTING
            if conf.prop_no_self_shadow:
                flags |= StaticPropFlags.NO_SELF_SHADOWING

            static_props.append(
                StaticProp(
                    model=model_name,
                    origin=center,
                    angles=Angle(0, 270, 0),
                    scaling=1.0,
                    visleafs=compute_visleafs(coll_data, vis_tree_top),
                    solidity=0,
                    flags=flags,
                    tint=Vec(conf.prop_rendercolor),
                    renderfx=conf.prop_renderalpha,
                    lighting_origin=light_origin,
                    min_fade=conf.prop_fade_min_dist,
                    max_fade=conf.prop_fade_max_dist,
                    fade_scale=conf.prop_fade_scale,
                ))
    LOGGER.info('Built {} models.', len(all_nodes))
    ctx.bsp.write_static_props(static_props)
Example #12
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.

    * Each `segment` parameter should be a `x y z;x y z` pair of positions
    that represent the ends of the fizzler.
    * `up_axis` should be set to a normal vector pointing in the new 'upward'
    direction.
    * If none are connected, a regular fizzler will be synthesized.

    The following fixup vars will be set to allow the shape to match the fizzler:

    * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS.pop(shape_name)

    shape_orient = Matrix.from_angle(Angle.from_str(shape_inst['angles']))
    up_axis: Vec = round(res.vec('up_axis') @ shape_orient, 6)

    for conn in shape_item.outputs:
        fizz_item = conn.to_item
        try:
            fizz = fizzler.FIZZLERS[fizz_item.name]
        except KeyError:
            continue
        # Detach this connection and remove traces of it.
        conn.remove()

        fizz.emitters.clear()  # Remove old positions.
        fizz.up_axis = up_axis
        fizz.base_inst['origin'] = shape_inst['origin']
        fizz.base_inst['angles'] = shape_inst['angles']
        break
    else:
        # No fizzler, so generate a default.
        # We create the fizzler instance, Fizzler object, and Item object
        # matching it.
        # This is hardcoded to use regular Emancipation Fields.
        base_inst = vmf.create_ent(
            targetname=shape_name,
            classname='func_instance',
            origin=shape_inst['origin'],
            angles=shape_inst['angles'],
            file=resolve_one('<ITEM_BARRIER_HAZARD:fizz_base>'),
        )
        base_inst.fixup.update(shape_inst.fixup)
        fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler(
            fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'],
            up_axis,
            base_inst,
            [],
        )
        fizz_item = connections.Item(
            base_inst,
            connections.ITEM_TYPES['item_barrier_hazard'],
            ant_floor_style=shape_item.ant_floor_style,
            ant_wall_style=shape_item.ant_wall_style,
        )
        connections.ITEMS[shape_name] = fizz_item

    # Transfer the input/outputs from us to the fizzler.
    for inp in list(shape_item.inputs):
        inp.to_item = fizz_item
    for conn in list(shape_item.outputs):
        conn.from_item = fizz_item

    # If the fizzler has no outputs, then strip out antlines. Otherwise,
    # they need to be transferred across, so we can't tell safely.
    if fizz_item.output_act() is None and fizz_item.output_deact() is None:
        shape_item.delete_antlines()
    else:
        shape_item.transfer_antlines(fizz_item)

    fizz_base = fizz.base_inst
    fizz_base['origin'] = shape_inst['origin']
    origin = Vec.from_str(shape_inst['origin'])

    fizz.has_cust_position = True
    # Since the fizzler is moved elsewhere, it's the responsibility of
    # the new item to have holes.
    fizz.embedded = False
    # So tell it whether or not it needs to do so.
    shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1) @ shape_orient + origin,
            Vec.from_str(vec2) @ shape_orient + origin,
        )
        fizz.emitters.append(seg_min_max)
Example #13
0
 def add_point(self, pos: Vec) -> None:
     """Add the given point to the end of the animation."""
     self.mesh.animation[self.cur_frame] = [
         BoneFrame(self.move_bone, pos, Angle(next(self.rotator)))
     ]
     self.cur_frame += 1
Example #14
0
def res_signage(vmf: VMF, inst: Entity, res: Property):
    """Implement the Signage item."""
    sign: Optional[Sign]
    try:
        sign = (CONN_SIGNAGES if res.bool('connection') else
                SIGNAGES)[inst.fixup[consts.FixupVars.TIM_DELAY]]
    except KeyError:
        # Blank sign
        sign = None

    has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED)
    make_4x4 = res.bool('set4x4tile')

    sign_prim: Optional[Sign]
    sign_sec: Optional[Sign]

    if has_arrow:
        sign_prim = sign
        sign_sec = SIGNAGES['arrow']
    elif sign is not None:
        sign_prim = sign.primary or sign
        sign_sec = sign.secondary or None
    else:
        # Neither sign or arrow, delete this.
        inst.remove()
        return

    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    normal = -orient.up()
    forward = -orient.forward()

    prim_pos = Vec(0, -16, -64) @ orient + origin
    sec_pos = Vec(0, +16, -64) @ orient + origin

    template_id = res['template_id', '']

    if inst.fixup.bool(consts.FixupVars.ST_REVERSED):
        # Flip around.
        forward = -forward
        prim_visgroup = 'secondary'
        sec_visgroup = 'primary'
        prim_pos, sec_pos = sec_pos, prim_pos
    else:
        prim_visgroup = 'primary'
        sec_visgroup = 'secondary'

    if sign_prim and sign_sec:
        inst['file'] = fname = res['large_clip', '']
        inst['origin'] = (prim_pos + sec_pos) / 2
    else:
        inst['file'] = fname = res['small_clip', '']
        inst['origin'] = prim_pos if sign_prim else sec_pos
    conditions.ALL_INST.add(fname.casefold())

    brush_faces: List[Side] = []
    tiledef: Optional[tiling.TileDef] = None

    if template_id:
        if sign_prim and sign_sec:
            visgroup = [prim_visgroup, sec_visgroup]
        elif sign_prim:
            visgroup = [prim_visgroup]
        else:
            visgroup = [sec_visgroup]
        template = template_brush.import_template(
            vmf,
            template_id,
            origin,
            orient,
            force_type=template_brush.TEMP_TYPES.detail,
            additional_visgroups=visgroup,
        )

        for face in template.detail.sides():
            if face.normal() == normal:
                brush_faces.append(face)
    else:
        # Direct on the surface.
        # Find the grid pos first.
        grid_pos = (origin // 128) * 128 + 64
        try:
            tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(),
                                   (-normal).as_tuple()]
        except KeyError:
            LOGGER.warning(
                "Can't place signage at ({}) in ({}) direction!",
                origin,
                normal,
                exc_info=True,
            )
            return

    if sign_prim is not None:
        over = place_sign(
            vmf,
            brush_faces,
            sign_prim,
            prim_pos,
            normal,
            forward,
            rotate=True,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)
        if make_4x4:
            try:
                tile, u, v = tiling.find_tile(prim_pos, -normal)
            except KeyError:
                pass
            else:
                tile[u, v] = tile[u, v].as_4x4

    if sign_sec is not None:
        if has_arrow and res.bool('arrowDown'):
            # Arrow texture points down, need to flip it.
            forward = -forward

        over = place_sign(
            vmf,
            brush_faces,
            sign_sec,
            sec_pos,
            normal,
            forward,
            rotate=not has_arrow,
        )

        if tiledef is not None:
            tiledef.bind_overlay(over)
        if make_4x4:
            try:
                tile, u, v = tiling.find_tile(sec_pos, -normal)
            except KeyError:
                pass
            else:
                tile[u, v] = tile[u, v].as_4x4
Example #15
0
def generate(style: str, sty_ang: Angle, field_tex: str, shape: Shape) -> str:
    """Generate a specific model."""
    # Load the models we use.
    emitter = load_model(style + '_fizz_ref.smd')
    anim_open = load_model(style + '_explode_out.smd')
    anim_close = load_model(style + '_explode_in.smd')

    ref = Mesh.blank(ROOT_NAME)
    [root] = ref.bones.values()

    # First, copy in the static block geo.
    try:
        ref.append_model(load_model(shape.block_fname))
    except FileNotFoundError:
        print('No blocks for: ', shape.block_fname)

    # Rename the fizzler field material.
    for tri in ref.triangles:
        if tri.mat == 'field':
            tri.mat = field_tex

    new_bones: Dict[Tuple[float, float, float], Dict[Bone, Bone]] = {}
    ind = 1
    # Generate the duplicate sets of bones.
    for pos, angles in shape.points:
        angles = sty_ang @ angles
        if angles.as_tuple() in new_bones:
            continue

        bones: Dict[Bone, Bone]
        bones = new_bones[angles.as_tuple()] = {
            bone: Bone(f'{bone.name}_{ind}', None)
            for bone in emitter.bones.values()
        }
        local_root = bones[root] = Bone(f'{ROOT_NAME}_{ind}', root)
        ind += 1

        # Now they're all created, fix up the parents.
        for bone in emitter.bones.values():
            if bone.parent is not None:
                bones[bone].parent = bones[bone.parent]
            else:
                bones[bone].parent = local_root
        # Add the bones to the reference.
        for bone in bones.values():
            ref.bones[bone.name] = bone

        # And copy over their start pose.
        ref.animation[0].append(BoneFrame(local_root, Vec(), Angle()))
        for frame in emitter.animation[0]:
            ref.animation[0].append(
                BoneFrame(bones[frame.bone], frame.position, frame.rotation))

    # Now, place each emitter.
    for pos, angles in shape.points:
        angles = sty_ang @ angles
        bones = new_bones[angles.as_tuple()]
        for tri in emitter.triangles:
            ref.triangles.append(
                Triangle(
                    tri.mat, *[
                        Vertex((vert.pos @ sty_ang) + pos, vert.norm,
                               vert.tex_u, vert.tex_v,
                               [(bones[old], fact)
                                for old, fact in vert.links]) for vert in tri
                    ]))

    folder = f'generated/{style}/'

    make_anim(root, sty_ang, new_bones, f'{folder}{shape.name}_open',
              anim_open.animation)
    make_anim(root, sty_ang, new_bones, f'{folder}{shape.name}_close',
              anim_close.animation)
    make_anim(root, sty_ang, new_bones, f'{folder}{shape.name}_idle',
              {0: anim_open.animation[0]})

    print(f' - {folder}{shape.name}_ref...', flush=True)
    with open(f'{folder}{shape.name}_ref.smd', 'wb') as f:
        ref.export(f)

    qc_file = f'{folder}{shape.name}.qc'
    with open(qc_file, 'w') as f:
        f.write(
            f'$modelname "props_map_editor/BEE2/{style}/{shape.name}.mdl"\n')
        f.write('$BodyGroup "Body" {\n\tstudio "%_ref.smd"\n}\n'.replace(
            '%', shape.name))
        f.write(
            '$cdmaterials "models/props_map_editor/" "BEE2/models/props_map_editor/"\n'
        )
        f.write('''
$sequence "idle" "%_idle.smd"
$sequence "explodeOut" "%_open.smd"
$sequence "explodeIn" "%_close.smd"
'''.replace('%', shape.name))
    return qc_file
Example #16
0
def compile_func(
    lookup_model: Callable[[str], Tuple[QC, Model]],
    mdl_key: Tuple[Set[PropPos], bool],
    temp_folder: Path,
    mdl_name: str,
) -> None:
    """Build this merged model."""
    LOGGER.info('Compiling {}...', mdl_name)
    prop_pos, has_coll = mdl_key

    # Unify these properties.
    surfprops = set()  # type: Set[str]
    cdmats = set()  # type: Set[str]
    contents = set()  # type: Set[int]

    for prop in prop_pos:
        qc, mdl = lookup_model(prop.model)
        assert mdl is not None, prop.model
        surfprops.add(mdl.surfaceprop.casefold())
        cdmats.update(mdl.cdmaterials)
        contents.add(mdl.contents)

    if len(surfprops) > 1:
        raise ValueError('Multiple surfaceprops? Should be filtered out.')

    if len(contents) > 1:
        raise ValueError('Multiple contents? Should be filtered out.')

    [surfprop] = surfprops
    [phy_content_type] = contents

    ref_mesh = Mesh.blank('static_prop')
    coll_mesh = None  #  type: Optional[Mesh]

    for prop in prop_pos:
        qc, mdl = lookup_model(prop.model)
        try:
            child_ref = _mesh_cache[qc, prop.skin]
        except KeyError:
            LOGGER.info('Parsing ref "{}"', qc.ref_smd)
            with open(qc.ref_smd, 'rb') as fb:
                child_ref = Mesh.parse_smd(fb)

            if prop.skin != 0 and prop.skin < len(mdl.skins):
                # We need to rename the materials to match the skin.
                swap_skins = dict(zip(mdl.skins[0], mdl.skins[prop.skin]))
                for tri in child_ref.triangles:
                    tri.mat = swap_skins.get(tri.mat, tri.mat)

            # For some reason all the SMDs are rotated badly, but only
            # if we append them.
            rot = Matrix.from_yaw(90)
            for tri in child_ref.triangles:
                for vert in tri:
                    vert.pos @= rot
                    vert.norm @= rot

            _mesh_cache[qc, prop.skin] = child_ref

        child_coll = build_collision(qc, prop, child_ref)

        offset = Vec(prop.x, prop.y, prop.z)
        angles = Angle(prop.pit, prop.yaw, prop.rol)

        ref_mesh.append_model(child_ref, angles, offset,
                              prop.scale * qc.ref_scale)

        if has_coll and child_coll is not None:
            if coll_mesh is None:
                coll_mesh = Mesh.blank('static_prop')
            coll_mesh.append_model(child_coll, angles, offset,
                                   prop.scale * qc.phy_scale)

    with (temp_folder / 'reference.smd').open('wb') as fb:
        ref_mesh.export(fb)

    # Generate  a  blank animation.
    with (temp_folder / 'anim.smd').open('wb') as fb:
        Mesh.blank('static_prop').export(fb)

    if coll_mesh is not None:
        with (temp_folder / 'physics.smd').open('wb') as fb:
            coll_mesh.export(fb)

    with (temp_folder / 'model.qc').open('w') as f:
        f.write(
            QC_TEMPLATE.format(
                path=mdl_name,
                surf=surfprop,
                # For $contents, we need to decompose out each bit.
                # This is the same as BSP's flags in public/bsp_flags.h
                # However only a few types are allowable.
                contents=' '.join([
                    cont for mask, cont in [
                        (0x1, '"solid"'),
                        (0x8, '"grate"'),
                        (0x2000000, '"monster"'),
                        (0x20000000, '"ladder"'),
                    ] if mask & phy_content_type
                    # 0 needs to produce this value.
                ]) or '"notsolid"',
            ))

        for mat in sorted(cdmats):
            f.write('$cdmaterials "{}"\n'.format(mat))

        if coll_mesh is not None:
            f.write(QC_COLL_TEMPLATE)
Example #17
0
    def static_props(self) -> Iterator['StaticProp']:
        """Read in the Static Props lump."""
        # The version of the static prop format - different features.
        try:
            version = self.game_lumps[b'sprp'].version
        except KeyError:
            raise ValueError('No static prop lump!') from None

        if version > 11:
            raise ValueError('Unknown version ({})!'.format(version))
        if version < 4:
            # Predates HL2...
            raise ValueError('Static prop version {} is too old!')

        static_lump = BytesIO(self.game_lumps[b'sprp'].data)

        # Array of model filenames.
        model_dict = list(self._read_static_props_models(static_lump))

        [visleaf_count] = struct_read('<i', static_lump)
        visleaf_list = list(struct_read('H' * visleaf_count, static_lump))

        [prop_count] = struct_read('<i', static_lump)

        for i in range(prop_count):
            origin = Vec(struct_read('fff', static_lump))
            angles = Angle(struct_read('fff', static_lump))

            [model_ind] = struct_read('<H', static_lump)

            (
                first_leaf,
                leaf_count,
                solidity,
                flags,
                skin,
                min_fade,
                max_fade,
            ) = struct_read('<HHBBiff', static_lump)

            model_name = model_dict[model_ind]

            visleafs = visleaf_list[first_leaf:first_leaf + leaf_count]
            lighting_origin = Vec(struct_read('<fff', static_lump))

            if version >= 5:
                fade_scale = struct_read('<f', static_lump)[0]
            else:
                fade_scale = 1  # default

            if version in (6, 7):
                min_dx_level, max_dx_level = struct_read('<HH', static_lump)
            else:
                # Replaced by GPU & CPU in later versions.
                min_dx_level = max_dx_level = 0  # None

            if version >= 8:
                (
                    min_cpu_level,
                    max_cpu_level,
                    min_gpu_level,
                    max_gpu_level,
                ) = struct_read('BBBB', static_lump)
            else:
                # None
                min_cpu_level = max_cpu_level = 0
                min_gpu_level = max_gpu_level = 0

            if version >= 7:
                r, g, b, renderfx = struct_read('BBBB', static_lump)
                # Alpha isn't used.
                tint = Vec(r, g, b)
            else:
                # No tint.
                tint = Vec(255, 255, 255)
                renderfx = 255

            if version >= 11:
                # Unknown data, though it's float-like.
                unknown_1 = struct_read('<i', static_lump)

            if version >= 10:
                # Extra flags, post-CSGO.
                flags |= struct_read('<I', static_lump)[0] << 8

            flags = StaticPropFlags(flags)

            scaling = 1.0
            disable_on_xbox = False

            if version >= 11:
                # XBox support was removed. Instead this is the scaling factor.
                [scaling] = struct_read("<f", static_lump)
            elif version >= 9:
                # The single boolean byte also produces 3 pad bytes.
                [disable_on_xbox] = struct_read('<?xxx', static_lump)

            yield StaticProp(
                model_name,
                origin,
                angles,
                scaling,
                visleafs,
                solidity,
                flags,
                skin,
                min_fade,
                max_fade,
                lighting_origin,
                fade_scale,
                min_dx_level,
                max_dx_level,
                min_cpu_level,
                max_cpu_level,
                min_gpu_level,
                max_gpu_level,
                tint,
                renderfx,
                disable_on_xbox,
            )
Example #18
0
def test_construction(py_c_vec):
    """Check various parts of the constructor - Vec(), Vec.from_str()."""
    Vec, Angle, Matrix, parse_vec_str = py_c_vec
    
    for pit, yaw, rol in iter_vec(VALID_ZERONUMS):
        assert_ang(Angle(pit, yaw, rol), pit, yaw, rol)
        assert_ang(Angle(pit, yaw), pit, yaw, 0)
        assert_ang(Angle(pit), pit, 0, 0)
        assert_ang(Angle(), 0, 0, 0)

        assert_ang(Angle([pit, yaw, rol]), pit, yaw, rol)
        assert_ang(Angle([pit, yaw], roll=rol), pit, yaw, rol)
        assert_ang(Angle([pit], yaw=yaw, roll=rol), pit, yaw, rol)
        assert_ang(Angle([pit]), pit, 0, 0)
        assert_ang(Angle([pit, yaw]), pit, yaw, 0)
        assert_ang(Angle([pit, yaw, rol]), pit, yaw, rol)

        # Test this does nothing (except copy).
        ang = Angle(pit, yaw, rol)
        ang2 = Angle(ang)
        assert_ang(ang2, pit, yaw, rol)
        assert ang is not ang2

        ang3 = Angle.copy(ang)
        assert_ang(ang3, pit, yaw, rol)
        assert ang is not ang3

        # Test Angle.from_str()
        assert_ang(Angle.from_str('{} {} {}'.format(pit, yaw, rol)), pit, yaw, rol)
        assert_ang(Angle.from_str('<{} {} {}>'.format(pit, yaw, rol)), pit, yaw, rol)
        # {x y z}
        assert_ang(Angle.from_str('{{{} {} {}}}'.format(pit, yaw, rol)), pit, yaw, rol)
        assert_ang(Angle.from_str('({} {} {})'.format(pit, yaw, rol)), pit, yaw, rol)
        assert_ang(Angle.from_str('[{} {} {}]'.format(pit, yaw, rol)), pit, yaw, rol)

        # Test converting a converted Angle
        orig = Angle(pit, yaw, rol)
        new = Angle.from_str(Angle(pit, yaw, rol))
        assert_ang(new, pit, yaw, rol)
        assert orig is not new  # It must be a copy

        # Check as_tuple() makes an equivalent tuple
        tup = orig.as_tuple()

        # Flip to work arond the coercion.
        pit %= 360.0
        yaw %= 360.0
        rol %= 360.0

        assert isinstance(tup, tuple)
        assert (pit, yaw, rol) == tup
        assert hash((pit, yaw, rol)) == hash(tup)
        # Bypass subclass functions.
        assert tuple.__getitem__(tup, 0) == pit
        assert tuple.__getitem__(tup, 1) == yaw
        assert tuple.__getitem__(tup, 2) == rol

    # Check failures in Angle.from_str()
    # Note - does not pass through unchanged, they're converted to floats!
    for val in VALID_ZERONUMS:
        test_val = val % 360.0
        assert test_val == Angle.from_str('', pitch=val).pitch
        assert test_val == Angle.from_str('blah 4 2', yaw=val).yaw
        assert test_val == Angle.from_str('2 hi 2', pitch=val).pitch
        assert test_val == Angle.from_str('2 6 gh', roll=val).roll
        assert test_val == Angle.from_str('1.2 3.4', pitch=val).pitch
        assert test_val == Angle.from_str('34.5 38.4 -23 -38', roll=val).roll
Example #19
0
def flag_check_marker(inst: Entity, flag: Property) -> bool:
    """Check if markers are present at a position.

    Parameters:
        * `name`: The name to look for. This can contain one `*` to match prefixes/suffixes.
        * `nameVar`: If found, set this variable to the actual name.
        * `pos`: The position to check.
        * `pos2`: If specified, the position is a bounding box from 1 to 2.
        * `radius`: Check markers within this distance. If this is specified, `pos2` is not permitted.
        * `global`: If true, positions are an absolute position, ignoring this instance.
        * `removeFound`: If true, remove the found marker. If you don't need it, this will improve
          performance.
        * `copyto`: Copies fixup vars from the searching instance to the one which set the
          marker. The value is in the form `$src $dest`.
        * `copyfrom`: Copies fixup vars from the one that set the marker to the searching instance.
          The value is in the form `$src $dest`.
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    name = inst.fixup.substitute(flag['name']).casefold()
    if '*' in name:
        try:
            prefix, suffix = name.split('*')
        except ValueError:
            raise ValueError(f'Name "{name}" must only have 1 *!')

        def match(val: str) -> bool:
            """Match a prefix or suffix."""
            val = val.casefold()
            return val.startswith(prefix) and val.endswith(suffix)
    else:

        def match(val: str) -> bool:
            """Match an exact name."""
            return val.casefold() == name

    try:
        is_global = srctools.conv_bool(
            inst.fixup.substitute(flag['global'], allow_invert=True))
    except LookupError:
        is_global = False

    pos = Vec.from_str(inst.fixup.substitute(flag['pos']))
    if not is_global:
        pos = pos @ orient + origin

    radius: float | None
    if 'pos2' in flag:
        if 'radius' in flag:
            raise ValueError('Only one of pos2 or radius must be defined.')
        pos2 = Vec.from_str(inst.fixup.substitute(flag['pos2']))
        if not is_global:
            pos2 = pos2 @ orient + origin
        bb_min, bb_max = Vec.bbox(pos, pos2)
        radius = None
        LOGGER.debug('Searching for marker "{}" from ({})-({})', name, bb_min,
                     bb_max)
    elif 'radius' in flag:
        radius = abs(srctools.conv_float(inst.fixup.substitute(
            flag['radius'])))
        bb_min = pos - (radius + 1.0)
        bb_max = pos + (radius + 1.0)
        LOGGER.debug('Searching for marker "{}" at ({}), radius={}', name, pos,
                     radius)
    else:
        bb_min = pos - (1.0, 1.0, 1.0)
        bb_max = pos + (1.0, 1.0, 1.0)
        radius = 1e-6
        LOGGER.debug('Searching for marker "{}" at ({})', name, pos)

    for i, marker in enumerate(MARKERS):
        if not marker.pos.in_bbox(bb_min, bb_max):
            continue
        if radius is not None and (marker.pos - pos).mag() > radius:
            continue
        if not match(marker.name):
            continue
        # Matched.
        if 'nameVar' in flag:
            inst.fixup[flag['namevar']] = marker.name
        if srctools.conv_bool(
                inst.fixup.substitute(flag['removeFound'], allow_invert=True)):
            LOGGER.debug('Removing found marker {}', marker)
            del MARKERS[i]

        for prop in flag.find_all('copyto'):
            src, dest = prop.value.split(' ', 1)
            marker.inst.fixup[dest] = inst.fixup[src]
        for prop in flag.find_all('copyfrom'):
            src, dest = prop.value.split(' ', 1)
            inst.fixup[dest] = marker.inst.fixup[src]
        return True
    return False
Example #20
0
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]:
    """Add another instance on top of this one.

    If a single value, this sets only the filename.
    Values:

    - `file`: The filename.
    - `fixup_style`: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
    - `copy_fixup`: If true, all the `$replace` values from the original
            instance will be copied over.
    - `move_outputs`: If true, outputs will be moved to this instance.
    - `offset`: The offset (relative to the base) that the instance
        will be placed. Can be set to `<piston_top>` and
        `<piston_bottom>` to offset based on the configuration.
        `<piston_start>` will set it to the starting position, and
        `<piston_end>` will set it to the ending position of the Piston
        Platform's handles.
    - `rotation`: Rotate the instance by this amount.
    - `angles`: If set, overrides `rotation` and the instance angles entirely.
    - `fixup`/`localfixup`: Keyvalues in this block will be copied to the
            overlay entity.
        - If the value starts with `$`, the variable will be copied over.
        - If this is present, `copy_fixup` will be disabled.
    """

    if not res.has_children():
        # Use all the defaults.
        res = Property('AddOverlay', [
            Property('File', res.value)
        ])

    if 'angles' in res:
        angles = Angle.from_str(res['angles'])
        if 'rotation' in res:
            LOGGER.warning('"angles" option overrides "rotation"!')
    else:
        angles = Angle.from_str(res['rotation', '0 0 0'])
        angles @= Angle.from_str(inst['angles', '0 0 0'])

    orig_name = conditions.resolve_value(inst, res['file', ''])
    filename = instanceLocs.resolve_one(orig_name)

    if not filename:
        if not res.bool('silentLookup'):
            LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name)
        # Don't bother making a overlay which will be deleted.
        return None

    overlay_inst = conditions.add_inst(
        vmf,
        targetname=inst['targetname', ''],
        file=filename,
        angles=angles,
        origin=inst['origin'],
        fixup_style=res.int('fixup_style'),
    )
    # Don't run if the fixup block exists..
    if srctools.conv_bool(res['copy_fixup', '1']):
        if 'fixup' not in res and 'localfixup' not in res:
            # Copy the fixup values across from the original instance
            for fixup, value in inst.fixup.items():
                overlay_inst.fixup[fixup] = value

    conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup')

    if res.bool('move_outputs', False):
        overlay_inst.outputs = inst.outputs
        inst.outputs = []

    if 'offset' in res:
        overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset'])

    return overlay_inst
Example #21
0
def add_timer_relay(item: Item, has_sounds: bool) -> None:
    """Make a relay to play timer sounds, or fire once the outputs are done."""
    assert item.timer is not None

    rl_name = item.name + '_timer_rl'

    relay = item.inst.map.create_ent(
        'logic_relay',
        targetname=rl_name,
        startDisabled=0,
        spawnflags=0,
    )

    if item.config.timer_sound_pos:
        relay_loc = item.config.timer_sound_pos.copy()
        relay_loc.localise(
            Vec.from_str(item.inst['origin']),
            Angle.from_str(item.inst['angles']),
        )
        relay['origin'] = relay_loc
    else:
        relay['origin'] = item.inst['origin']

    for cmd in item.config.timer_done_cmd:
        if cmd:
            relay.add_out(Output(
                'OnTrigger',
                conditions.local_name(item.inst, cmd.target) or item.inst,
                conditions.resolve_value(item.inst, cmd.input),
                conditions.resolve_value(item.inst, cmd.params),
                inst_in=cmd.inst_in,
                delay=item.timer + cmd.delay,
                times=cmd.times,
            ))

    if item.config.timer_sound_pos is not None and has_sounds:
        timer_sound = options.get(str, 'timer_sound')
        timer_cc = options.get(str, 'timer_sound_cc')

        # The default sound has 'ticking' closed captions.
        # So reuse that if the style doesn't specify a different noise.
        # If explicitly set to '', we don't use this at all!
        if timer_cc is None and timer_sound != 'Portal.room1_TickTock':
            timer_cc = 'Portal.room1_TickTock'
        if timer_cc:
            timer_cc = 'cc_emit ' + timer_cc

        # Write out the VScript code to precache the sound, and play it on
        # demand.
        relay['vscript_init_code'] = (
            'function Precache() {'
            f'self.PrecacheSoundScript(`{timer_sound}`)'
            '}'
        )
        relay['vscript_init_code2'] = (
            'function snd() {'
            f'self.EmitSound(`{timer_sound}`)'
            '}'
        )
        packing.pack_files(item.inst.map, timer_sound, file_type='sound')

        for delay in range(item.timer):
            relay.add_out(Output(
                'OnTrigger',
                '!self',
                'CallScriptFunction',
                'snd',
                delay=delay,
            ))
            if timer_cc:
                relay.add_out(Output(
                    'OnTrigger',
                    '@command',
                    'Command',
                    timer_cc,
                    delay=delay,
                ))

    for outputs, cmd in [
        (item.timer_output_start(), 'Trigger'),
        (item.timer_output_stop(), 'CancelPending')
    ]:
        for output in outputs:
            item.add_io_command(output, rl_name, cmd)
Example #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)
Example #23
0
def make_bottomless_pit(vmf: VMF, max_height):
    """Generate bottomless pits."""
    import vbsp

    tele_ref = SETTINGS['tele_ref']
    tele_dest = SETTINGS['tele_dest']

    use_skybox = bool(SETTINGS['skybox'])

    if use_skybox:
        tele_off = Vec(
            x=SETTINGS['off_x'],
            y=SETTINGS['off_y'],
        )
    else:
        tele_off = Vec(0, 0, 0)

    # Controlled by the style, not skybox!
    blend_light = options.get(str, 'pit_blend_light')

    if use_skybox:
        # Add in the actual skybox edges and triggers.
        conditions.add_inst(
            vmf,
            file=SETTINGS['skybox'],
            targetname='skybox',
            origin=tele_off,
        )

        fog_opt = vbsp.settings['fog']

        # Now generate the sky_camera, with appropriate values.
        sky_camera = vmf.create_ent(
            classname='sky_camera',
            scale='1.0',
            origin=tele_off,
            angles=fog_opt['direction'],
            fogdir=fog_opt['direction'],
            fogcolor=fog_opt['primary'],
            fogstart=fog_opt['start'],
            fogend=fog_opt['end'],
            fogenable='1',
            heightFogStart=fog_opt['height_start'],
            heightFogDensity=fog_opt['height_density'],
            heightFogMaxDensity=fog_opt['height_max_density'],
        )

        if fog_opt['secondary']:
            # Only enable fog blending if a secondary color is enabled
            sky_camera['fogblend'] = '1'
            sky_camera['fogcolor2'] = fog_opt['secondary']
            sky_camera['use_angles'] = '1'
        else:
            sky_camera['fogblend'] = '0'
            sky_camera['use_angles'] = '0'

        if SETTINGS['skybox_ceil'] != '':
            # We dynamically add the ceiling so it resizes to match the map,
            # and lighting won't be too far away.
            conditions.add_inst(
                vmf,
                file=SETTINGS['skybox_ceil'],
                targetname='skybox',
                origin=tele_off + (0, 0, max_height),
            )

        if SETTINGS['targ'] != '':
            # Add in the teleport reference target.
            conditions.add_inst(
                vmf,
                file=SETTINGS['targ'],
                targetname='skybox',
                origin='0 0 0',
            )

    # First, remove all of Valve's triggers inside pits.
    for trig in vmf.by_class['trigger_multiple'] | vmf.by_class['trigger_hurt']:
        if brushLoc.POS['world':Vec.from_str(trig['origin'])].is_pit:
            trig.remove()

    # Potential locations of bordering brushes..
    wall_pos = set()

    side_dirs = [
        (0, -128, 0),  # N
        (0, +128, 0),  # S
        (-128, 0, 0),  # E
        (+128, 0, 0)  # W
    ]

    # Only use 1 entity for the teleport triggers. If multiple are used,
    # cubes can contact two at once and get teleported odd places.
    tele_trig = None
    hurt_trig = None

    for grid_pos, block_type in brushLoc.POS.items(
    ):  # type: Vec, brushLoc.Block
        pos = brushLoc.grid_to_world(grid_pos)
        if not block_type.is_pit:
            continue

        # Physics objects teleport when they hit the bottom of a pit.
        if block_type.is_bottom and use_skybox:
            if tele_trig is None:
                tele_trig = vmf.create_ent(
                    classname='trigger_teleport',
                    spawnflags='4106',  # Physics and npcs
                    landmark=tele_ref,
                    target=tele_dest,
                    origin=pos,
                )
            tele_trig.solids.append(
                vmf.make_prism(
                    pos + (-64, -64, -64),
                    pos + (64, 64, -8),
                    mat='tools/toolstrigger',
                ).solid, )

        # Players, however get hurt as soon as they enter - that way it's
        # harder to see that they don't teleport.
        if block_type.is_top:
            if hurt_trig is None:
                hurt_trig = vmf.create_ent(
                    classname='trigger_hurt',
                    damagetype=32,  # FALL
                    spawnflags=1,  # CLients
                    damage=100000,
                    nodmgforce=1,  # No physics force when hurt..
                    damagemodel=0,  # Always apply full damage.
                    origin=pos,  # We know this is not in the void..
                )
            hurt_trig.solids.append(
                vmf.make_prism(
                    Vec(pos.x - 64, pos.y - 64, -128),
                    pos + (64, 64, 48 if use_skybox else 16),
                    mat='tools/toolstrigger',
                ).solid, )

        if not block_type.is_bottom:
            continue
        # Everything else is only added to the bottom-most position.

        if use_skybox and blend_light:
            # Generate dim lights at the skybox location,
            # to blend the lighting together.
            light_pos = pos + (0, 0, -60)
            vmf.create_ent(
                classname='light',
                origin=light_pos,
                _light=blend_light,
                _fifty_percent_distance='256',
                _zero_percent_distance='512',
            )
            vmf.create_ent(
                classname='light',
                origin=light_pos + tele_off,
                _light=blend_light,
                _fifty_percent_distance='256',
                _zero_percent_distance='512',
            )

        wall_pos.update([(pos + off).as_tuple() for off in side_dirs])

    if hurt_trig is not None:
        hurt_trig.outputs.append(Output(
            'OnHurtPlayer',
            '@goo_fade',
            'Fade',
        ), )

    if not use_skybox:
        make_pit_shell(vmf)
        return

    # Now determine the position of side instances.
    # We use the utils.CONN_TYPES dict to determine instance positions
    # based on where nearby walls are.
    side_types = {
        utils.CONN_TYPES.side: PIT_INST['side'],  # o|
        utils.CONN_TYPES.corner: PIT_INST['corner'],  # _|
        utils.CONN_TYPES.straight: PIT_INST['side'],  # Add this twice for |o|
        utils.CONN_TYPES.triple: PIT_INST['triple'],  # U-shape
        utils.CONN_TYPES.all: PIT_INST['pillar'],  # [o]
    }

    LOGGER.info('Pit instances: {}', side_types)

    for pos in wall_pos:
        pos = Vec(pos)
        if not brushLoc.POS['world':pos].is_solid:
            # Not actually a wall here!
            continue

        # CONN_TYPES has n,s,e,w as keys - whether there's something in that direction.
        nsew = tuple(brushLoc.POS['world':pos + off].is_pit
                     for off in side_dirs)
        LOGGER.info('Pos: {}, NSEW: {}, lookup: {}', pos, nsew,
                    utils.CONN_LOOKUP[nsew])
        inst_type, angle = utils.CONN_LOOKUP[nsew]

        if inst_type is utils.CONN_TYPES.none:
            # Middle of the pit...
            continue

        rng = rand.seed(b'pit', pos.x, pos.y)
        file = rng.choice(side_types[inst_type])

        if file != '':
            conditions.add_inst(
                vmf,
                file=file,
                targetname='goo_side',
                origin=tele_off + pos,
                angles=angle,
            ).make_unique()

        # Straight uses two side-instances in parallel - "|o|"
        if inst_type is utils.CONN_TYPES.straight:
            file = rng.choice(side_types[inst_type])
            if file != '':
                conditions.add_inst(
                    vmf,
                    file=file,
                    targetname='goo_side',
                    origin=tele_off + pos,
                    # Reverse direction
                    angles=Angle.from_str(angle) + (0, 180, 0),
                ).make_unique()