Ejemplo n.º 1
0
    def _make_straight(
        self,
        vmf: VMF,
        segment: Segment,
        start: Vec,
        end: Vec,
        mat: AntTex,
    ) -> None:
        """Construct a straight antline between two points.

        The two points will be the end of the antlines.
        """
        offset = start - end
        forward = offset.norm()
        side = Vec.cross(segment.normal, forward).norm()

        length = offset.mag()

        self._make_overlay(
            vmf,
            segment,
            (start + end) / 2,
            length * forward,
            16 * side,
            mat,
        )
Ejemplo n.º 2
0
def place_sign(
    vmf: VMF,
    faces: Iterable[Side],
    sign: Sign,
    pos: Vec,
    normal: Vec,
    forward: Vec,
    rotate: bool = True,
) -> None:
    """Place the sign into the map."""

    if rotate and normal.z == 0:
        # On the wall, point upward.
        forward = Vec(0, 0, 1)

    texture = sign.overlay
    if texture.startswith('<') and texture.endswith('>'):
        texture = vbsp.get_tex(texture[1:-1])

    width, height = SIZES[sign.type]
    over = make_overlay(
        vmf,
        -normal,
        pos,
        uax=-width * Vec.cross(normal, forward).norm(),
        vax=-height * forward,
        material=texture,
        surfaces=faces,
    )

    over['startu'] = '1'
    over['endu'] = '0'

    vbsp.IGNORED_OVERLAYS.add(over)
Ejemplo n.º 3
0
def place_sign(
    vmf: VMF,
    faces: Iterable[Side],
    sign: Sign,
    pos: Vec,
    normal: Vec,
    forward: Vec,
    rotate: bool = True,
) -> Entity:
    """Place the sign into the map."""
    if rotate and abs(normal.z) < 0.1:
        # On the wall, point upward.
        forward = Vec(0, 0, 1)

    texture = sign.overlay
    if texture.startswith('<') and texture.endswith('>'):
        gen, tex_name = texturing.parse_name(texture[1:-1])
        texture = gen.get(pos, tex_name)

    width, height = SIZES[sign.type]
    over = make_overlay(
        vmf,
        -normal,
        pos,
        uax=-width * Vec.cross(normal, forward).norm(),
        vax=-height * forward,
        material=texture,
        surfaces=faces,
    )
    vbsp.IGNORED_OVERLAYS.add(over)

    over['startu'] = '1'
    over['endu'] = '0'

    return over
Ejemplo n.º 4
0
def compute_orients(nodes: Iterable[Node]) -> None:
    """Compute the appropriate orientation for each node."""
    # This is based on the info at:
    # https://janakiev.com/blog/framing-parametric-curves/
    tangents: Dict[Node, Vec] = {}
    all_nodes: Set[Node] = set()
    for node in nodes:
        if node.prev is node.next is None:
            continue
        node_prev = node.prev if node.prev is not None else node
        node_next = node.next if node.next is not None else node
        tangents[node] = (node_next.pos - node_prev.pos).norm()
        all_nodes.add(node)

    while all_nodes:
        node1 = all_nodes.pop()
        node1 = node1.find_start()
        tanj1 = tangents[node1]
        # Start with an arbitrary roll for the first orientation.
        node1.orient = Matrix.from_angle(tanj1.to_angle())
        while node1.next is not None:
            node2 = node1.next
            all_nodes.discard(node2)
            tanj1 = tangents[node1]
            tanj2 = tangents[node2]
            b = Vec.cross(tanj1, tanj2)
            if b.mag_sq() < 0.001:
                node2.orient = node1.orient.copy()
            else:
                b = b.norm()
                phi = math.acos(Vec.dot(tanj1, tanj2))
                up = node1.orient.up() @ Matrix.axis_angle(
                    b, math.degrees(phi))
                node2.orient = Matrix.from_basis(x=tanj2, z=up)
            node1 = node2
Ejemplo n.º 5
0
def join_markers(vmf: VMF,
                 mark_a: Marker,
                 mark_b: Marker,
                 is_start: bool = False) -> None:
    """Join two marker ents together with corners."""
    origin_a = Vec.from_str(mark_a.ent['origin'])
    origin_b = Vec.from_str(mark_b.ent['origin'])

    norm_a = Vec(-1, 0, 0).rotate_by_str(mark_a.ent['angles'])
    norm_b = Vec(-1, 0, 0).rotate_by_str(mark_b.ent['angles'])

    config = mark_a.conf

    if norm_a == norm_b:
        # Either straight-line, or s-bend.
        dist = (origin_a - origin_b).mag()

        if origin_a + (norm_a * dist) == origin_b:
            make_straight(
                vmf,
                origin_a,
                norm_a,
                dist,
                config,
                is_start,
            )
        # else: S-bend, we don't do the geometry for this..
        return

    if norm_a == -norm_b:
        # U-shape bend..
        make_ubend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            config,
            max_size=mark_a.size,
        )
        return

    # Lastly try a regular curve. Check they are on the same plane.
    side_dir = Vec.cross(norm_a, norm_b)
    side_off_a = side_dir.dot(origin_a)
    side_off_b = side_dir.dot(origin_b)
    if side_off_a == side_off_b:
        make_bend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            norm_b,
            config,
            max_size=mark_a.size,
        )
Ejemplo n.º 6
0
def test_gen_check(py_c_vec) -> None:
    """Do an exhaustive check on all rotation math using data from the engine."""
    Vec, Angle, Matrix, parse_vec_str = py_c_vec

    X = Vec(x=1)
    Y = Vec(y=1)
    Z = Vec(z=1)
    with open('rotation.txt') as f:
        for line_num, line in enumerate(f, start=1):
            if not line.startswith('|'):
                # Skip other junk in the log.
                continue

            (pit, yaw, roll, for_x, for_y, for_z, right_x, right_y, right_z,
             up_x, up_y, up_z) = map(float, line[1:].split())
            # The engine actually gave us a right vector, so we need to flip that.
            left_x, left_y, left_z = -right_x, -right_y, -right_z

            mat = Matrix.from_angle(Angle(pit, yaw, roll))

            # Then check rotating vectors works correctly.
            assert_vec(X @ mat, for_x, for_y, for_z)
            assert_vec(Y @ mat, left_x, left_y, left_z)
            assert_vec(Z @ mat, up_x, up_y, up_z)

            # Check the direct matrix values.
            assert_vec(mat.forward(), for_x, for_y, for_z)
            assert_vec(mat.left(), left_x, left_y, left_z)
            assert_vec(mat.up(), up_x, up_y, up_z)

            assert math.isclose(for_x, mat[0, 0], abs_tol=EPSILON)
            assert math.isclose(for_y, mat[0, 1], abs_tol=EPSILON)
            assert math.isclose(for_z, mat[0, 2], abs_tol=EPSILON)

            assert math.isclose(left_x, mat[1, 0], abs_tol=EPSILON)
            assert math.isclose(left_y, mat[1, 1], abs_tol=EPSILON)
            assert math.isclose(left_z, mat[1, 2], abs_tol=EPSILON)

            assert math.isclose(up_x, mat[2, 0], abs_tol=EPSILON)
            assert math.isclose(up_y, mat[2, 1], abs_tol=EPSILON)
            assert math.isclose(up_z, mat[2, 2], abs_tol=EPSILON)

            # Also test Matrix.from_basis().
            x = Vec(for_x, for_y, for_z)
            y = Vec(left_x, left_y, left_z)
            z = Vec(up_x, up_y, up_z)
            assert_rot(Matrix.from_basis(x=x, y=y, z=z), mat)
            assert_rot(Matrix.from_basis(x=x, y=y), mat)
            assert_rot(Matrix.from_basis(y=y, z=z), mat)
            assert_rot(Matrix.from_basis(x=x, z=z), mat)

            # Angle.from_basis() == Matrix.from_basis().to_angle().
            assert_ang(Angle.from_basis(x=x, y=y, z=z),
                       *Matrix.from_basis(x=x, y=y, z=z).to_angle())
            assert_ang(Angle.from_basis(x=x, y=y),
                       *Matrix.from_basis(x=x, y=y).to_angle())
            assert_ang(Angle.from_basis(y=y, z=z),
                       *Matrix.from_basis(y=y, z=z).to_angle())
            assert_ang(Angle.from_basis(x=x, z=z),
                       *Matrix.from_basis(x=x, z=z).to_angle())

            # And Vec.cross().
            assert_vec(Vec.cross(x, y), up_x, up_y, up_z, tol=1e-5)
            assert_vec(Vec.cross(y, z), for_x, for_y, for_z, tol=1e-5)
            assert_vec(Vec.cross(x, z), -left_x, -left_y, -left_z, tol=1e-5)

            assert_vec(Vec.cross(y, x), -up_x, -up_y, -up_z, tol=1e-5)
            assert_vec(Vec.cross(z, y), -for_x, -for_y, -for_z, tol=1e-5)
            assert_vec(Vec.cross(z, x), left_x, left_y, left_z, tol=1e-5)

            assert_vec(Vec.cross(x, x), 0, 0, 0)
            assert_vec(Vec.cross(y, y), 0, 0, 0)
            assert_vec(Vec.cross(z, z), 0, 0, 0)
Ejemplo n.º 7
0
def join_markers(vmf: VMF, mark_a: Marker, mark_b: Marker, is_start: bool=False) -> None:
    """Join two marker ents together with corners."""
    origin_a = Vec.from_str(mark_a.ent['origin'])
    origin_b = Vec.from_str(mark_b.ent['origin'])

    norm_a = mark_a.orient.forward()
    norm_b = mark_b.orient.forward()

    config = mark_a.conf

    LOGGER.debug(
        'Connect markers: {} @ {} -> {} @ {}, dot={}\n{}',
        origin_a, norm_a,
        origin_b, norm_b,
        Vec.dot(norm_a, norm_b),
        config,
    )

    if norm_a == norm_b:
        # Either straight-line, or s-bend.
        dist = round((origin_a - origin_b).mag())

        if origin_a + (norm_a * dist) == origin_b:
            make_straight(
                vmf,
                origin_a,
                norm_a,
                dist,
                config,
                is_start,
            )
        # else: S-bend, we don't do the geometry for this..
        return

    if norm_a == -norm_b:
        # U-shape bend..
        make_ubend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            config,
            max_size=mark_a.size,
        )
        return

    # Lastly try a regular curve. Check they are on the same plane.
    side_dir = Vec.cross(norm_a, norm_b)
    side_off_a = side_dir.dot(origin_a)
    side_off_b = side_dir.dot(origin_b)
    if abs(side_off_a - side_off_b) < 1e-6:
        make_bend(
            vmf,
            origin_a,
            origin_b,
            norm_a,
            norm_b,
            config,
            max_size=mark_a.size,
        )
    else:
        LOGGER.warning(
            'Cannot connect markers: {} @ {} -> {} @ {}\n '
            'Sides: {:.12f} {:.12f}\n{}',
            origin_a, norm_a,
            origin_b, norm_b,
            side_off_a, side_off_b,
            config,
        )
Ejemplo n.º 8
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