예제 #1
0
def test_bbox() -> None:
    """Test the bounding box behaviour against a brute-force loop."""
    rand = Random(1234)  # Ensure reproducibility.
    SIZE = 128.0
    # Build a set of points and keys.
    points = [(Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE),
                   rand.uniform(-SIZE, SIZE)),
               Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE),
                   rand.uniform(-SIZE, SIZE)),
               rand.getrandbits(64).to_bytes(8, 'little')) for _ in range(200)]
    tree = RTree()
    for a, b, data in points:
        tree.insert(a, b, data)

    # Pick a random bounding box.
    bb_min, bb_max = Vec.bbox(
        Vec(
            rand.uniform(-SIZE, SIZE),
            rand.uniform(-SIZE, SIZE),
            rand.uniform(-SIZE, SIZE),
        ),
        Vec(
            rand.uniform(-SIZE, SIZE),
            rand.uniform(-SIZE, SIZE),
            rand.uniform(-SIZE, SIZE),
        ))
    expected = [
        data for a, b, data in points
        if Vec.bbox_intersect(*Vec.bbox(a, b), bb_min, bb_max)
    ]
    found = set(tree.find_bbox(bb_min, bb_max))
    # Order is irrelevant, but duplicates must all match.
    assert sorted(expected) == sorted(found)
예제 #2
0
def build_itemclass_dict(prop_block: Property):
    """Load in the item ID database.

    This maps item IDs to their item class, and their embed locations.
    """
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class

    # Now load in the embed data.
    for prop in prop_block.find_children('ItemEmbeds'):
        if prop.name not in CLASS_FOR_ITEM:
            LOGGER.warning('Unknown item ID with embeds "{}"!', prop.real_name)

        vecs = EMBED_OFFSETS.setdefault(prop.name, [])
        if ':' in prop.value:
            first, last = prop.value.split(':')
            bbox_min, bbox_max = Vec.bbox(Vec.from_str(first),
                                          Vec.from_str(last))
            vecs.extend(Vec.iter_grid(bbox_min, bbox_max))
        else:
            vecs.append(Vec.from_str(prop.value))

    LOGGER.info(
        'Read {} item IDs, with {} embeds!',
        len(ITEMS_WITH_CLASS),
        len(EMBED_OFFSETS),
    )
예제 #3
0
def calc_fizzler_orient(fizzler: Fizzler):
    # Figure out how to compare for this fizzler.

    s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters))

    # If it's horizontal, signs should point to the center:
    if abs(s.z - l.z) == 2:
        return (
            'z',
            s.x + l.x / 2,
            s.y + l.y / 2,
            s.z + 1,
        )
    # For the vertical directions, we want to compare based on the line segment.
    if abs(s.x - l.x) == 2:  # Y direction
        return (
            'y',
            s.y,
            l.y,
            s.x + 1,
        )
    else:  # Extends in X direction
        return (
            'x',
            s.x,
            l.x,
            s.y + 1,
        )
예제 #4
0
파일: apTag.py 프로젝트: BenVlodgi/BEE2.4
def calc_fizzler_orient(fizzler: Fizzler):
    # Figure out how to compare for this fizzler.

    s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters))

    # If it's horizontal, signs should point to the center:
    if abs(s.z - l.z) == 2:
        return (
            'z',
            s.x + l.x / 2,
            s.y + l.y / 2,
            s.z + 1,
        )
    # For the vertical directions, we want to compare based on the line segment.
    if abs(s.x - l.x) == 2:  # Y direction
        return (
            'y',
            s.y,
            l.y,
            s.x + 1,
        )
    else:  # Extends in X direction
        return (
            'x',
            s.x,
            l.x,
            s.y + 1,
        )
예제 #5
0
def build_collision(qc: QC, prop: PropPos, ref_mesh: Mesh) -> Optional[Mesh]:
    """Get the correct collision mesh for this model."""
    if prop.solidity is CollType.NONE:  # Non-solid
        return None
    elif prop.solidity is CollType.VPHYS or prop.solidity is CollType.BSP:
        if qc.phy_smd is None:
            return None
        try:
            return _coll_cache[qc.phy_smd]
        except KeyError:
            LOGGER.info('Parsing coll "{}"', qc.phy_smd)
            with open(qc.phy_smd, 'rb') as fb:
                coll = Mesh.parse_smd(fb)

            rot = Matrix.from_yaw(90)
            for tri in coll.triangles:
                for vert in tri:
                    vert.pos @= rot
                    vert.norm @= rot

            _coll_cache[qc.phy_smd] = coll
            return coll
    # Else, it's one of the three bounding box types.
    # We don't really care about which.
    bbox_min, bbox_max = Vec.bbox(vert.pos for tri in ref_mesh.triangles
                                  for vert in tri)
    return Mesh.build_bbox('static_prop', 'phy', bbox_min, bbox_max)
예제 #6
0
def flag_blockpos_type(inst: Entity, flag: Property):
    """Determine the type of a grid position.

    If the value is single value, that should be the type.
    Otherwise, the value should be a block with 'offset' and 'type' values.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.
    If 'offset2' is also provided, all positions in the bounding box will
    be checked.

    The type should be a space-seperated list of locations:

    * `VOID` (Outside the map)
    * `SOLID` (Full wall cube)
    * `EMBED` (Hollow wall cube)
    * `AIR` (Inside the map, may be occupied by items)
    * `OCCUPIED` (Known to be occupied by items)
    * `PIT` (Bottomless pits, any)
        * `PIT_SINGLE` (one-high)
        * `PIT_TOP`
        * `PIT_MID`
        * `PIT_BOTTOM`
    * `GOO`
        * `GOO_SINGLE` (one-deep goo)
        * `GOO_TOP` (goo surface)
        * `GOO_MID`
        * `GOO_BOTTOM` (floor)
    """
    pos2 = None

    if flag.has_children():
        pos1 = resolve_offset(inst,
                              flag['offset', '0 0 0'],
                              scale=128,
                              zoff=-128)
        types = flag['type'].split()
        if 'offset2' in flag:
            pos2 = resolve_offset(inst, flag.value, scale=128, zoff=-128)
    else:
        types = flag.value.split()
        pos1 = Vec()

    if pos2 is not None:
        bbox = Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128)
    else:
        bbox = [pos1]

    for pos in bbox:
        block = brushLoc.POS['world':pos]
        for block_type in types:
            try:
                allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()]
            except KeyError:
                raise ValueError(
                    '"{}" is not a valid block type!'.format(block_type))
            if block in allowed:
                break  # To next position
        else:
            return False  # Didn't match any in this list.
    return True  # Matched all positions.
예제 #7
0
def make_straight(
        origin: Vec,
        normal: Vec,
        dist: int,
        config: dict,
        is_start=False,
    ):
    """Make a straight line of instances from one point to another."""

    # 32 added to the other directions, plus extended dist in the direction
    # of the normal - 1
    p1 = origin + (normal * ((dist // 128 * 128) - 96))
    # The starting brush needs to
    # stick out a bit further, to cover the
    # point_push entity.
    p2 = origin - (normal * (96 if is_start else 32))

    # bbox before +- 32 to ensure the above doesn't wipe it out
    p1, p2 = Vec.bbox(p1, p2)

    solid = vbsp.VMF.make_prism(
        # Expand to 64x64 in the other two directions
        p1 - 32, p2 + 32,
        mat='tools/toolstrigger',
    ).solid

    motion_trigger(solid.copy())

    push_trigger(origin, normal, [solid])

    angles = normal.to_angle()

    support_file = config['support']
    straight_file = config['straight']
    support_positions = (
        SUPPORT_POS[normal.as_tuple()]
        if support_file else
        []
    )

    for off in range(0, int(dist), 128):
        position = origin + off * normal
        vbsp.VMF.create_ent(
            classname='func_instance',
            origin=position,
            angles=angles,
            file=straight_file,
        )

        for supp_ang, supp_off in support_positions:
            if (position + supp_off).as_tuple() in SOLIDS:
                vbsp.VMF.create_ent(
                    classname='func_instance',
                    origin=position,
                    angles=supp_ang,
                    file=support_file,
                )
예제 #8
0
파일: vactubes.py 프로젝트: mariovct/BEE2.4
def make_straight(
    origin: Vec,
    normal: Vec,
    dist: int,
    config: dict,
    is_start=False,
):
    """Make a straight line of instances from one point to another."""

    # 32 added to the other directions, plus extended dist in the direction
    # of the normal - 1
    p1 = origin + (normal * ((dist // 128 * 128) - 96))
    # The starting brush needs to
    # stick out a bit further, to cover the
    # point_push entity.
    p2 = origin - (normal * (96 if is_start else 32))

    # bbox before +- 32 to ensure the above doesn't wipe it out
    p1, p2 = Vec.bbox(p1, p2)

    solid = vbsp.VMF.make_prism(
        # Expand to 64x64 in the other two directions
        p1 - 32,
        p2 + 32,
        mat='tools/toolstrigger',
    ).solid

    motion_trigger(solid.copy())

    push_trigger(origin, normal, [solid])

    angles = normal.to_angle()

    support_file = config['support']
    straight_file = config['straight']
    support_positions = (SUPPORT_POS[normal.as_tuple()]
                         if support_file else [])

    for off in range(0, int(dist), 128):
        position = origin + off * normal
        vbsp.VMF.create_ent(
            classname='func_instance',
            origin=position,
            angles=angles,
            file=straight_file,
        )

        for supp_ang, supp_off in support_positions:
            if (position + supp_off).as_tuple() in SOLIDS:
                vbsp.VMF.create_ent(
                    classname='func_instance',
                    origin=position,
                    angles=supp_ang,
                    file=support_file,
                )
예제 #9
0
def flag_blockpos_type(inst: Entity, flag: Property):
    """Determine the type of a grid position.

    If the value is single value, that should be the type.
    Otherwise, the value should be a block with 'offset' and 'type' values.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.
    If 'offset2' is also provided, all positions in the bounding box will
    be checked.

    The type should be a space-seperated list of locations:
    * `VOID` (Outside the map)
    * `SOLID` (Full wall cube)
    * `EMBED` (Hollow wall cube)
    * `AIR` (Inside the map, may be occupied by items)
    * `OCCUPIED` (Known to be occupied by items)
    * `PIT` (Bottomless pits, any)
      * `PIT_SINGLE` (one-high)
      * `PIT_TOP`
      * `PIT_MID`
      * `PIT_BOTTOM`
    * `GOO`
      * `GOO_SINGLE` (one-deep goo)
      * `GOO_TOP` (goo surface)
      * `GOO_MID`
      * `GOO_BOTTOM` (floor)
    """
    pos2 = None

    if flag.has_children():
        pos1 = resolve_offset(inst, flag['offset', '0 0 0'], scale=128, zoff=-128)
        types = flag['type'].split()
        if 'offset2' in flag:
            pos2 = resolve_offset(inst, flag.value, scale=128, zoff=-128)
    else:
        types = flag.value.split()
        pos1 = Vec()

    if pos2 is not None:
        bbox = Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128)
    else:
        bbox = [pos1]

    for pos in bbox:
        block = brushLoc.POS['world': pos]
        for block_type in types:
            try:
                allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()]
            except KeyError:
                raise ValueError('"{}" is not a valid block type!'.format(block_type))
            if block in allowed:
                break  # To next position
        else:
            return False  # Didn't match any in this list.
    return True  # Matched all positions.
예제 #10
0
def group_props_auto(
    prop_groups: Dict[Optional[tuple], List[StaticProp]],
    rejected: List[StaticProp],
    dist: float,
    min_cluster: int,
) -> Iterator[List[StaticProp]]:
    """Given the groups of props, automatically find close props to merge."""
    # Each of these groups cannot be merged with other ones.

    dist_sq = dist * dist
    large_dist_sq = 4 * dist_sq

    for group in prop_groups.values():
        # No point merging single/empty groups.
        if len(group) < 2:
            rejected.extend(group)
            continue

        todo = set(group)
        while todo:
            center = todo.pop()
            cluster = {center}

            for prop in todo:
                if (center.origin - prop.origin).mag_sq() <= large_dist_sq:
                    cluster.add(prop)
                    if len(cluster) > MAX_GROUP:
                        # Limit the number of maximum props that can be used.
                        break

            if len(cluster) < min_cluster:
                rejected.append(center)
                continue

            bbox_min, bbox_max = Vec.bbox(prop.origin for prop in cluster)
            center_pos = (bbox_min + bbox_max) / 2

            cluster_list = []

            for prop in cluster:
                prop_off = (center_pos - prop.origin).mag_sq()
                if prop_off <= dist_sq:
                    cluster_list.append((prop, prop_off))

            cluster_list.sort(key=lambda t: t[1])
            selected_props = [prop for prop, off in cluster_list[:MAX_GROUP]]
            todo.difference_update(selected_props)

            if len(selected_props) >= min_cluster:
                yield selected_props
            else:
                rejected.extend(selected_props)
예제 #11
0
def res_set_block(inst: Entity, res: Property) -> None:
    """Set a block to the given value, overwriting the existing value.

    - `type` is the type of block to set:
        * `VOID` (Outside the map)
        * `SOLID` (Full wall cube)
        * `EMBED` (Hollow wall cube)
        * `AIR` (Inside the map, may be occupied by items)
        * `OCCUPIED` (Known to be occupied by items)
        * `PIT_SINGLE` (one-high)
        * `PIT_TOP`
        * `PIT_MID`
        * `PIT_BOTTOM`
        * `GOO_SINGLE` (one-deep goo)
        * `GOO_TOP` (goo surface)
        * `GOO_MID`
        * `GOO_BOTTOM` (floor)
    - `offset` is in block increments, with `0 0 0` equal to the mounting surface.
    - If 'offset2' is also provided, all positions in the bounding box will be set.
    """
    try:
        new_vals = brushLoc.BLOCK_LOOKUP[res['type'].casefold()]
    except KeyError:
        raise ValueError('"{}" is not a valid block type!'.format(res['type']))

    try:
        [new_val] = new_vals
    except ValueError:
        # TODO: This could spread top/mid/bottom through the bbox...
        raise ValueError(
            f'Can\'t use compound block type "{res["type"]}", specify '
            "_SINGLE/TOP/MID/BOTTOM"
        )

    pos1 = resolve_offset(inst, res['offset', '0 0 0'], scale=128, zoff=-128)

    if 'offset2' in res:
        pos2 = resolve_offset(inst, res['offset2', '0 0 0'], scale=128, zoff=-128)
        for pos in Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128):
            brushLoc.POS['world': pos] = new_val
    else:
        brushLoc.POS['world': pos1] = new_val
예제 #12
0
def test_bbox_rotation(
    pitch: float, yaw: float, roll: float,
) -> None:
    """Test the rotation logic against the slow direct approach."""
    ang = Angle(pitch, yaw, roll)
    bb_start = BBox(100, 200, 300, 300, 450, 600, contents=CollideType.ANTLINES, tags='blah')
    # Directly compute, by rotating all the angles,
    points = [
        Vec(x, y, z)
        for x in [100, 300]
        for y in [200, 450]
        for z in [300, 600]
    ]
    result_ang = bb_start @ ang
    result_mat = bb_start @ Matrix.from_angle(ang)
    assert result_ang == result_mat

    bb_min, bb_max = Vec.bbox(
        point @ ang for point in points
    )
    assert_bbox(result_mat, round(bb_min, 0), round(bb_max, 0), CollideType.ANTLINES, {'blah'})
예제 #13
0
    def build_bbox(cls, root_bone: str, mat: str, bbox_min: Vec,
                   bbox_max: Vec) -> 'Mesh':
        """Construct a mesh for a bounding box."""
        mesh = cls.blank(root_bone)
        [root] = mesh.bones.values()
        links = [(root, 1.0)]

        bbox_min, bbox_max = Vec.bbox(bbox_min, bbox_max)

        for tri_def in cls._BBOX_MESH_DATA:
            tri = Triangle(
                mat, *[
                    Vertex(
                        Vec(
                            bbox_max.x if x > 0 else bbox_min.x,
                            bbox_max.y if y > 0 else bbox_min.y,
                            bbox_max.z if z > 0 else bbox_min.z,
                        ),
                        Vec(x, y, z).norm(), u, v, links.copy())
                    for x, y, z, u, v in tri_def
                ])
            mesh.triangles.append(tri)
        return mesh
예제 #14
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)
예제 #15
0
 def __init__(self, point1: Vec, point2: Vec) -> None:
     self.bbox_min, self.bbox_max = Vec.bbox(point1, point2)
예제 #16
0
파일: barriers.py 프로젝트: BEEmod/BEE2.4
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

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

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

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

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

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

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

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

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

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

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

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

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

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

    # Side -> u, v or None

    for group in groups:
        bbox_min, bbox_max = Vec.bbox(group)
        dimensions = bbox_max - bbox_min

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

        # Build min, max tuples for each axis in the other direction.
        # This tells us where the beams will be.
        beams: dict[float, tuple[float, float]] = {}

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

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

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

        detail = vmf.create_ent('func_detail')

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

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

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

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
예제 #17
0
def make_straight(
    vmf: VMF,
    origin: Vec,
    normal: Vec,
    dist: int,
    config: Config,
    is_start=False,
) -> None:
    """Make a straight line of instances from one point to another."""

    # 32 added to the other directions, plus extended dist in the direction
    # of the normal - 1
    p1 = origin + (normal * ((dist // 128 * 128) - 96))
    # The starting brush needs to
    # stick out a bit further, to cover the
    # point_push entity.
    p2 = origin - (normal * (96 if is_start else 32))

    # bbox before +- 32 to ensure the above doesn't wipe it out
    p1, p2 = Vec.bbox(p1, p2)

    solid = vmf.make_prism(
        # Expand to 64x64 in the other two directions
        p1 - 32,
        p2 + 32,
        mat='tools/toolstrigger',
    ).solid

    motion_trigger(vmf, solid.copy())

    push_trigger(vmf, origin, normal, [solid])

    angles = normal.to_angle()
    orient = Matrix.from_angle(angles)

    for off in range(0, int(dist), 128):
        position = origin + off * normal
        vmf.create_ent(
            classname='func_instance',
            origin=position,
            angles=orient.to_angle(),
            file=config.inst_straight,
        )

        for supp_dir in [
                orient.up(),
                orient.left(), -orient.left(), -orient.up()
        ]:
            try:
                tile = tiling.TILES[(position - 128 * supp_dir).as_tuple(),
                                    supp_dir.norm().as_tuple()]
            except KeyError:
                continue
            # Check all 4 center tiles are present.
            if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)):
                vmf.create_ent(
                    classname='func_instance',
                    origin=position,
                    angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(),
                    file=config.inst_support,
                )
예제 #18
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
예제 #19
0
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property):
    """Add an Aperture Tag Paint Gun activation fizzler.

    These fizzlers are created via signs, and work very specially.
    This must be before -250 so it runs before fizzlers and connections.
    """
    (
        sign_offset,
        fizz_conn_conf,
        inst_frame_double,
        inst_frame_single,
        blue_sign_on,
        blue_sign_off,
        oran_sign_on,
        oran_sign_off,
    ) = res.value  # type: int, Optional[connections.Config], str, str, str, str, str, str
    import vbsp
    if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']:
        # Abort - TAG fizzlers shouldn't appear in any other game!
        inst.remove()
        return

    fizzler = None
    fizzler_item = None

    # Look for the fizzler instance we want to replace.
    sign_item = connections.ITEMS[inst['targetname']]
    for conn in list(sign_item.outputs):
        if conn.to_item.name in FIZZLERS:
            if fizzler is None:
                fizzler = FIZZLERS[conn.to_item.name]
                fizzler_item = conn.to_item
            else:
                raise ValueError('Multiple fizzlers attached to a sign!')

        conn.remove()  # Regardless, remove the useless output.

    sign_item.delete_antlines()

    if fizzler is None:
        # No fizzler - remove this sign
        inst.remove()
        return

    if fizzler.fizz_type.id == TAG_FIZZ_ID:
        LOGGER.warning('Two tag signs attached to one fizzler...')
        inst.remove()
        return

    # Swap to the special Tag Fizzler type.
    fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID]

    # And also swap the connection's type.
    if fizz_conn_conf is not None:
        fizzler_item.config = fizz_conn_conf
        fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd
        fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd
        fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd
        fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd

    sign_loc = (
        # The actual location of the sign - on the wall
        Vec.from_str(inst['origin']) +
        Vec(0, 0, -64).rotate_by_str(inst['angles']))

    fizz_norm_axis = fizzler.normal().axis()

    # Now deal with the visual aspect:
    # Blue signs should be on top.

    blue_enabled = inst.fixup.bool('$start_enabled')
    oran_enabled = inst.fixup.bool('$start_reversed')
    # If True, single-color signs will also turn off the other color.
    # This also means we always show both signs.
    # If both are enabled or disabled, this has no effect.
    disable_other = (not inst.fixup.bool('$disable_autorespawn', True)
                     and blue_enabled != oran_enabled)
    # Delete fixups now, they aren't useful.
    inst.fixup.clear()

    if not blue_enabled and not oran_enabled:
        # Hide the sign in this case!
        inst.remove()

    inst_angle = srctools.parse_vec_str(inst['angles'])

    inst_normal = Vec(0, 0, 1).rotate(*inst_angle)
    loc = Vec.from_str(inst['origin'])

    if disable_other or (blue_enabled and oran_enabled):
        inst['file'] = inst_frame_double
        # On a wall, and pointing vertically
        if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z:
            # They're vertical, make sure blue's on top!
            blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset)
            oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset)
            # If orange is enabled, with two frames put that on top
            # instead since it's more important
            if disable_other and oran_enabled:
                blue_loc, oran_loc = oran_loc, blue_loc

        else:
            offset = Vec(0, sign_offset, 0).rotate(*inst_angle)
            blue_loc = loc + offset
            oran_loc = loc - offset
    else:
        inst['file'] = inst_frame_single
        # They're always centered
        blue_loc = loc
        oran_loc = loc

    if inst_normal.z != 0:
        # If on floors/ceilings, rotate to point at the fizzler!
        sign_floor_loc = sign_loc.copy()
        sign_floor_loc.z = 0  # We don't care about z-positions.

        s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters))

        if fizz_norm_axis == 'z':
            # For z-axis, just compare to the center point of the emitters.
            sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc
        else:
            # For the other two, we compare to the line,
            # or compare to the closest side (in line with the fizz)

            if fizz_norm_axis == 'x':  #  Extends in Y direction
                other_axis = 'y'
                side_min = s.y
                side_max = l.y
                normal = s.x
            else:  # Extends in X direction
                other_axis = 'x'
                side_min = s.x
                side_max = l.x
                normal = s.y

            # Right in line with the fizzler. Point at the closest emitter.
            if abs(sign_floor_loc[other_axis] - normal) < 32:
                # Compare to the closest side.
                sign_dir = min(
                    (sign_floor_loc - Vec.with_axes(
                        fizz_norm_axis,
                        side_min,
                        other_axis,
                        normal,
                    ), sign_floor_loc - Vec.with_axes(
                        fizz_norm_axis,
                        side_max,
                        other_axis,
                        normal,
                    )),
                    key=Vec.mag,
                )
            else:
                # Align just based on whether we're in front or behind.
                sign_dir = Vec.with_axes(
                    fizz_norm_axis,
                    normal - sign_floor_loc[fizz_norm_axis]).norm()

        sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x))
        # Round to nearest 90 degrees
        # Add 45 so the switchover point is at the diagonals
        sign_yaw = (sign_yaw + 45) // 90 * 90

        # Rotate to fit the instances - south is down
        sign_yaw = int(sign_yaw - 90) % 360

        if inst_normal.z > 0:
            sign_angle = '0 {} 0'.format(sign_yaw)
        elif inst_normal.z < 0:
            # Flip upside-down for ceilings
            sign_angle = '0 {} 180'.format(sign_yaw)
        else:
            raise AssertionError('Cannot be zero here!')
    else:
        # On a wall, face upright
        sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()]

    # If disable_other, we show off signs. Otherwise we don't use that sign.
    blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None
    oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None

    if blue_sign:
        vmf.create_ent(
            classname='func_instance',
            file=blue_sign,
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=blue_loc.join(' '),
        )

    if oran_sign:
        vmf.create_ent(
            classname='func_instance',
            file=oran_sign,
            targetname=inst['targetname'],
            angles=sign_angle,
            origin=oran_loc.join(' '),
        )

    # Now modify the fizzler...

    # Subtract the sign from the list of connections, but don't go below
    # zero
    fizzler.base_inst.fixup['$connectioncount'] = str(
        max(
            0,
            srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', ''])
            - 1))

    # Find the direction the fizzler normal is.
    # Signs will associate with the given side!

    bbox_min, bbox_max = fizzler.emitters[0]

    sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2

    # Figure out what the sides will set values to...
    pos_blue = False
    pos_oran = False
    neg_blue = False
    neg_oran = False

    if sign_loc[fizz_norm_axis] < sign_center:
        pos_blue = blue_enabled
        pos_oran = oran_enabled
    else:
        neg_blue = blue_enabled
        neg_oran = oran_enabled

    # If it activates the paint gun, use different textures
    fizzler.tag_on_pos = pos_blue or pos_oran
    fizzler.tag_on_neg = neg_blue or neg_oran

    # Now make the trigger ents. We special-case these since they need to swap
    # depending on the sign config and position.

    if vbsp.GAME_MODE == 'COOP':
        # We need ATLAS-specific triggers
        pos_trig = vmf.create_ent(classname='trigger_playerteam', )
        neg_trig = vmf.create_ent(classname='trigger_playerteam', )
        output = 'OnStartTouchBluePlayer'
    else:
        pos_trig = vmf.create_ent(classname='trigger_multiple', )
        neg_trig = vmf.create_ent(
            classname='trigger_multiple',
            spawnflags='1',
        )
        output = 'OnStartTouch'

    pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin']
    pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1'  # Clients Only

    pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos')
    neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg')

    pos_trig['startdisabled'] = neg_trig['startdisabled'] = (
        not fizzler.base_inst.fixup.bool('start_enabled'))

    pos_trig.outputs = [
        Output(output, neg_trig, 'Enable'),
        Output(output, pos_trig, 'Disable'),
    ]

    neg_trig.outputs = [
        Output(output, pos_trig, 'Enable'),
        Output(output, neg_trig, 'Disable'),
    ]

    voice_attr = vbsp.settings['has_attr']

    if blue_enabled or disable_other:
        # If this is blue/oran only, don't affect the other color
        neg_trig.outputs.append(
            Output(
                output,
                '@BlueIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(neg_blue),
            ))
        pos_trig.outputs.append(
            Output(
                output,
                '@BlueIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(pos_blue),
            ))
        if blue_enabled:
            # Add voice attributes - we have the gun and gel!
            voice_attr['bluegelgun'] = True
            voice_attr['bluegel'] = True
            voice_attr['bouncegun'] = True
            voice_attr['bouncegel'] = True

    if oran_enabled or disable_other:
        neg_trig.outputs.append(
            Output(
                output,
                '@OrangeIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(neg_oran),
            ))
        pos_trig.outputs.append(
            Output(
                output,
                '@OrangeIsEnabled',
                'SetValue',
                param=srctools.bool_as_int(pos_oran),
            ))
        if oran_enabled:
            voice_attr['orangegelgun'] = True
            voice_attr['orangegel'] = True
            voice_attr['speedgelgun'] = True
            voice_attr['speedgel'] = True

    if not oran_enabled and not blue_enabled:
        # If both are disabled, we must shutdown the gun when touching
        # either side - use neg_trig for that purpose!
        # We want to get rid of pos_trig to save ents
        vmf.remove_ent(pos_trig)
        neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off')
        neg_trig.outputs.clear()
        neg_trig.add_out(
            Output(output, '@BlueIsEnabled', 'SetValue', param='0'))
        neg_trig.add_out(
            Output(output, '@OrangeIsEnabled', 'SetValue', param='0'))

    # Make the triggers.
    for bbox_min, bbox_max in fizzler.emitters:
        bbox_min = bbox_min.copy() - 64 * fizzler.up_axis
        bbox_max = bbox_max.copy() + 64 * fizzler.up_axis

        # The triggers are 8 units thick, with a 32-unit gap in the middle
        neg_min, neg_max = Vec(bbox_min), Vec(bbox_max)
        neg_min[fizz_norm_axis] -= 24
        neg_max[fizz_norm_axis] -= 16

        pos_min, pos_max = Vec(bbox_min), Vec(bbox_max)
        pos_min[fizz_norm_axis] += 16
        pos_max[fizz_norm_axis] += 24

        if blue_enabled or oran_enabled:
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    neg_max,
                    mat='tools/toolstrigger',
                ).solid, )
            pos_trig.solids.append(
                vmf.make_prism(
                    pos_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid, )
        else:
            # If neither enabled, use one trigger
            neg_trig.solids.append(
                vmf.make_prism(
                    neg_min,
                    pos_max,
                    mat='tools/toolstrigger',
                ).solid, )
예제 #20
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.
    `default` is the ID of a fizzler type which should be used if no outputs
    are fired.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS[shape_name]

    for conn in shape_item.outputs:
        fizz_name = conn.inp.name
        try:
            fizz = fizzler.FIZZLERS[fizz_name]
        except KeyError:
            LOGGER.warning(
                'Reshaping fizzler with non-fizzler output! Ignoring!')
            continue
        break
    else:
        # No fizzler - create one.
        conn = None
        fizz_type = fizzler.FIZZ_TYPES[res['default']]
        base_inst = vmf.create_ent(
            targetname=shape_name,
            classname='func_instance',
            origin=shape_inst['origin'],
            file=fizz_type.inst[fizzler.FizzInst.BASE][0],
        )
        base_inst.fixup.update(shape_inst.fixup)
        fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler(
            fizz_type,
            Vec(),
            base_inst,
            [],
        )

    # Detach this connection and remove traces of it.
    if conn:
        conn.remove()
        if shape_item.ind_toggle:
            remove_ant_toggle(shape_item.ind_toggle)

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

    shape_angles = Vec.from_str(shape_inst['angles'])

    fizz.up_axis = res.vec('up_axis').rotate(*shape_angles)
    fizz.emitters.clear()

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
예제 #21
0
파일: fizzler.py 프로젝트: prrg/BEE2.4
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.
    `default` is the ID of a fizzler type which should be used if no outputs
    are fired.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS.pop(shape_name)

    shape_angles = Vec.from_str(shape_inst['angles'])
    up_axis = res.vec('up_axis').rotate(*shape_angles)

    for conn in shape_item.outputs:
        fizz_item = conn.to_item
        try:
            fizz = fizzler.FIZZLERS[fizz_item.name]
        except KeyError:
            LOGGER.warning(
                'Reshaping fizzler with non-fizzler output ({})! Ignoring!',
                fizz_item.name)
            continue
        fizz.emitters.clear()  # Remove old positions.
        fizz.up_axis = up_axis
        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'],
            file=resolve_inst('<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'],
            shape_item.ant_floor_style,
            shape_item.ant_wall_style,
        )
        connections.ITEMS[shape_name] = fizz_item

    # Detach this connection and remove traces of it.
    for conn in list(shape_item.outputs):
        conn.remove()
    for coll in [
            shape_item.antlines, shape_item.ind_panels, shape_item.shape_signs
    ]:
        for ent in coll:
            ent.remove()
        coll.clear()

    for inp in list(shape_item.inputs):
        inp.to_item = fizz_item

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

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
예제 #22
0
파일: marker.py 프로젝트: Thedoczek/BEE2.4
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
예제 #23
0
파일: fizzler.py 프로젝트: BenVlodgi/BEE2.4
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None:
    """Analyse fizzler instances to assign fizzler types.

    Instance traits are required.
    The model instances and brushes will be removed from the map.
    Needs connections to be parsed.
    """

    # Item ID and model skin -> fizzler type
    fizz_types = {}  # type: Dict[Tuple[str, int], FizzlerType]

    for fizz_type in FIZZ_TYPES.values():
        for item_id in fizz_type.item_ids:
            if ':' in item_id:
                item_id, barrier_type = item_id.split(':')
                if barrier_type == 'laserfield':
                    barrier_skin = 2
                elif barrier_type == 'fizzler':
                    barrier_skin = 0
                else:
                    LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id)
                    fizz_types[item_id, 0] = fizz_type
                    fizz_types[item_id, 2] = fizz_type
                    continue
                fizz_types[item_id, barrier_skin] = fizz_type
            else:
                fizz_types[item_id, 0] = fizz_type
                fizz_types[item_id, 2] = fizz_type

    fizz_bases = {}  # type: Dict[str, Entity]
    fizz_models = defaultdict(list)  # type: Dict[str, List[Entity]]

    # Position and normal -> name, for output relays.
    fizz_pos = {}  # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str]

    # First use traits to gather up all the instances.
    for inst in vmf.by_class['func_instance']:
        traits = instance_traits.get(inst)
        if 'fizzler' not in traits:
            continue

        name = inst['targetname']

        if 'fizzler_model' in traits:
            name = name.rsplit('_model', 1)[0]
            fizz_models[name].append(inst)
            inst.remove()
        elif 'fizzler_base' in traits:
            fizz_bases[name] = inst
        else:
            LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name)
            continue

        origin = Vec.from_str(inst['origin'])
        normal = Vec(z=1).rotate_by_str(inst['angles'])
        fizz_pos[origin.as_tuple(), normal.as_tuple()] = name

    for name, base_inst in fizz_bases.items():
        models = fizz_models[name]
        up_axis = Vec(y=1).rotate_by_str(base_inst['angles'])

        # If upside-down, make it face upright.
        if up_axis == (0, 0, -1):
            up_axis = Vec(z=1)

        base_inst.outputs.clear()

        # Now match the pairs of models to each other.
        # The length axis is the line between them.
        # We don't care about the instances after this, so don't keep track.
        length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis()

        emitters = []  # type: List[Tuple[Vec, Vec]]

        model_pairs = {}  # type: Dict[Tuple[float, float], Vec]

        model_skin = models[0].fixup.int('$skin')

        try:
            item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[base_inst['file'].casefold()]
            fizz_type = fizz_types[item_id, model_skin]
        except KeyError:
            LOGGER.warning('Fizzler types: {}', fizz_types.keys())
            raise ValueError('No fizzler type for "{}"!'.format(
                base_inst['file'],
            )) from None

        for attr_name in fizz_type.voice_attrs:
            voice_attrs[attr_name] = True

        for model in models:
            pos = Vec.from_str(model['origin'])
            try:
                other_pos = model_pairs.pop(pos.other_axes(length_axis))
            except KeyError:
                # No other position yet, we need to find that.
                model_pairs[pos.other_axes(length_axis)] = pos
                continue

            min_pos, max_pos = Vec.bbox(pos, other_pos)

            # Move positions to the wall surface.
            min_pos[length_axis] -= 64
            max_pos[length_axis] += 64
            emitters.append((min_pos, max_pos))

        FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters)

    # Delete all the old brushes associated with fizzlers
    for brush in (
        vmf.by_class['trigger_portal_cleanser'] |
        vmf.by_class['trigger_hurt'] |
        vmf.by_class['func_brush']
    ):
        name = brush['targetname']
        if not name:
            continue
        name = name.rsplit('_brush')[0]
        if name in FIZZLERS:
            brush.remove()

    # Check for fizzler output relays.
    relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>', silent=True)
    if not relay_file:
        # No relay item - deactivated most likely.
        return

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in relay_file:
            continue

        inst.remove()

        relay_item = connections.ITEMS[inst['targetname']]

        try:
            fizz_name = fizz_pos[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()
            ]
            fizz_item = connections.ITEMS[fizz_name]
        except KeyError:
            # Not placed on a fizzler, or a fizzler with no IO
            # - ignore, and destroy.
            for out in list(relay_item.outputs):
                out.remove()
            for out in list(relay_item.inputs):
                out.remove()
            del connections.ITEMS[relay_item.name]
            continue

        # Copy over fixup values
        fizz_item.inst.fixup.update(inst.fixup)

        # Copy over the timer delay set in the relay.
        fizz_item.timer = relay_item.timer
        # Transfer over antlines.
        fizz_item.antlines |= relay_item.antlines
        fizz_item.shape_signs += relay_item.shape_signs
        fizz_item.ind_panels |= relay_item.ind_panels

        # Remove the relay item so it doesn't get added to the map.
        del connections.ITEMS[relay_item.name]

        for conn in list(relay_item.outputs):
            conn.from_item = fizz_item
예제 #24
0
def add_glass_floorbeams(vmf: VMF, temp_name: str):
    """Add beams to separate large glass panels.

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

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

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

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

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

        normal = Vec(normal)

        if not normal.z:
            # Not walls.
            continue

        pos = Vec(origin) + normal * 62

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

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

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

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

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

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

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

    # Side -> u, v or None

    for group in groups:

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

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

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

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

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

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

        detail = vmf.create_ent('func_detail')

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

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

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

                new_beam = beam_template.copy(vmf_file=vmf)
                new_beam.localise(min_pos, rot)
                detail.solids.append(new_beam)
예제 #25
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)
예제 #26
0
def make_straight(
    vmf: VMF,
    origin: Vec,
    normal: Vec,
    dist: int,
    config: Config,
    is_start=False,
) -> None:
    """Make a straight line of instances from one point to another."""
    angles = round(normal, 6).to_angle()
    orient = Matrix.from_angle(angles)

    # The starting brush needs to stick out a bit further, to cover the
    # point_push entity.
    start_off = -96 if is_start else -64

    p1, p2 = Vec.bbox(
        origin + Vec(start_off, -config.trig_radius, -config.trig_radius) @ orient,
        origin + Vec(dist - 64, config.trig_radius, config.trig_radius) @ orient,
    )

    solid = vmf.make_prism(p1, p2, mat='tools/toolstrigger').solid

    motion_trigger(vmf, solid.copy())

    push_trigger(vmf, origin, normal, [solid])

    off = 0
    for seg_dist in utils.fit(dist, config.inst_straight_sizes):
        vmf.create_ent(
            classname='func_instance',
            origin=origin + off * orient.forward(),
            angles=angles,
            file=config.inst_straight[seg_dist],
        )
        off += seg_dist
    # Supports.
    if config.inst_support:
        for off in range(0, int(dist), 128):
            position = origin + off * normal
            placed_support = False
            for supp_dir in [
                orient.up(), orient.left(),
                -orient.left(), -orient.up()
            ]:
                try:
                    tile = tiling.TILES[
                        (position - 128 * supp_dir).as_tuple(),
                        supp_dir.norm().as_tuple()
                    ]
                except KeyError:
                    continue
                # Check all 4 center tiles are present.
                if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)):
                    vmf.create_ent(
                        classname='func_instance',
                        origin=position,
                        angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(),
                        file=config.inst_support,
                    )
                    placed_support = True
            if placed_support and config.inst_support_ring:
                vmf.create_ent(
                    classname='func_instance',
                    origin=position,
                    angles=angles,
                    file=config.inst_support_ring,
                )
예제 #27
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

    This is run once to affect all of an item.  
    Options:
    * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    * `markerItem`: The item's ID
    * `previewConf`: A item config which enables/disables the preview overlay.
    * `previewInst`: An instance to place at the marker location in preview mode.
        This should contain checkmarks to display the value when testing.
    * `previewMat`: If set, the material to use for an overlay func_brush.
        The brush will be parented to the trigger, so it vanishes once killed.
        It is also non-solid.
    * `previewScale`: The scale for the func_brush materials.
    * `previewActivate`, `previewDeactivate`: The VMF output to turn the
        previewInst on and off.

    * `triggerActivate, triggerDeactivate`: The `instance:name;Output`
        outputs used when the trigger turns on or off.

    * `coopVar`: The instance variable which enables detecting both Coop players.
        The trigger will be a trigger_playerteam.

    * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used
        when coopVar is enabled. These should be suitable for a logic_coop_manager.
    * `coopOnce`: If true, kill the manager after it first activates.

    * `keys`: A block of keyvalues for the trigger brush. Origin and targetname
        will be set automatically.
    * `localkeys`: The same as above, except values will be changed to use
        instance-local names.
    """
    marker = instanceLocs.resolve(res['markerInst'])

    marker_names = set()

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in marker:
            marker_names.add(inst['targetname'])
            # Unconditionally delete from the map, so it doesn't
            # appear even if placed wrongly.
            inst.remove()

    if not marker_names:  # No markers in the map - abort
        return RES_EXHAUSTED

    item_id = res['markerItem']

    # Synthesise the item type used for the final trigger.
    item_type_sp = connections.ItemType(
        id=item_id + ':TRIGGER',
        output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']),
        output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']),
    )

    # For Coop, we add a logic_coop_manager in the mix so both players can
    # be handled.
    try:
        coop_var = res['coopVar']
    except LookupError:
        coop_var = item_type_coop = None
        coop_only_once = False
    else:
        coop_only_once = res.bool('coopOnce')
        item_type_coop = connections.ItemType(
            id=item_id + ':TRIGGER_COOP',
            output_act=Output.parse_name(
                res['coopActivate', 'OnChangeToAllTrue']
            ),
            output_deact=Output.parse_name(
                res['coopDeactivate', 'OnChangeToAnyFalse']
            ),
        )

    # Display preview overlays if it's preview mode, and the config is true
    pre_act = pre_deact = None
    if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        preview_scale = res.float('previewScale', 0.25)
        # None if not found.
        with suppress(LookupError):
            pre_act = Output.parse(res.find_key('previewActivate'))
        with suppress(LookupError):
            pre_deact = Output.parse(res.find_key('previewDeactivate'))
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        preview_scale = 0.25

    # Now go through each brush.
    # We do while + pop to allow removing both names each loop through.
    todo_names = set(marker_names)
    while todo_names:
        targ = todo_names.pop()

        mark1 = connections.ITEMS.pop(targ)
        for conn in mark1.outputs:
            if conn.to_item.name in marker_names:
                mark2 = conn.to_item
                conn.remove()  # Delete this connection.
                todo_names.discard(mark2.name)
                del connections.ITEMS[mark2.name]
                break
        else:
            if not mark1.inputs:
                # If the item doesn't have any connections, 'connect'
                # it to itself so we'll generate a 1-block trigger.
                mark2 = mark1
            else:
                # It's a marker with an input, the other in the pair
                # will handle everything.
                # But reinstate it in ITEMS.
                connections.ITEMS[targ] = mark1
                continue

        inst1 = mark1.inst
        inst2 = mark2.inst

        is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and (
            inst1.fixup.bool(coop_var) or
            inst2.fixup.bool(coop_var)
        )

        bbox_min, bbox_max = Vec.bbox(
            Vec.from_str(inst1['origin']),
            Vec.from_str(inst2['origin'])
        )
        origin = (bbox_max + bbox_min) / 2

        # Extend to the edge of the blocks.
        bbox_min -= 64
        bbox_max += 64

        out_ent = trig_ent = vmf.create_ent(
            classname='trigger_multiple',  # Default
            targetname=targ,
            origin=origin,
            angles='0 0 0',
        )
        trig_ent.solids = [
            vmf.make_prism(
                bbox_min,
                bbox_max,
                mat=const.Tools.TRIGGER,
            ).solid,
        ]

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, inst, res)

        if is_coop:
            trig_ent['spawnflags'] = '1'  # Clients
            trig_ent['classname'] = 'trigger_playerteam'

            out_ent = manager = vmf.create_ent(
                classname='logic_coop_manager',
                targetname=conditions.local_name(inst, 'man'),
                origin=origin,
            )

            item = connections.Item(
                out_ent,
                item_type_coop,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

            if coop_only_once:
                # Kill all the ents when both players are present.
                manager.add_out(
                    Output('OnChangeToAllTrue', manager, 'Kill'),
                    Output('OnChangeToAllTrue', targ, 'Kill'),
                )
            trig_ent.add_out(
                Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'),
                Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'),
                Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'),
                Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'),
            )
        else:
            item = connections.Item(
                trig_ent,
                item_type_sp,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

        # Register, and copy over all the antlines.
        connections.ITEMS[item.name] = item
        item.ind_panels = mark1.ind_panels | mark2.ind_panels
        item.antlines = mark1.antlines | mark2.antlines
        item.shape_signs = mark1.shape_signs + mark2.shape_signs

        if preview_mat:
            preview_brush = vmf.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=origin,

                Solidity='1',  # Not solid
                drawinfastreflection='1',  # Draw in goo..

                # Disable shadows and lighting..
                disableflashlight='1',
                disablereceiveshadows='1',
                disableshadowdepth='1',
                disableshadows='1',
            )
            preview_brush.solids = [
                # Make it slightly smaller, so it doesn't z-fight with surfaces.
                vmf.make_prism(
                    bbox_min + 0.5,
                    bbox_max - 0.5,
                    mat=preview_mat,
                ).solid,
            ]
            for face in preview_brush.sides():
                face.scale = preview_scale

        if preview_inst_file:
            pre_inst = vmf.create_ent(
                classname='func_instance',
                targetname=targ + '_preview',
                file=preview_inst_file,
                # Put it at the second marker, since that's usually
                # closest to antlines if present.
                origin=inst2['origin'],
            )

            if pre_act is not None:
                out = pre_act.copy()
                out.inst_out, out.output = item.output_act()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)
            if pre_deact is not None:
                out = pre_deact.copy()
                out.inst_out, out.output = item.output_deact()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)

        for conn in mark1.outputs | mark2.outputs:
            conn.from_item = item

    return RES_EXHAUSTED
예제 #28
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 = conditions.add_inst(
            vmf,
            targetname=shape_name,
            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)
예제 #29
0
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None:
    """Analyse fizzler instances to assign fizzler types.

    Instance traits are required.
    The model instances and brushes will be removed from the map.
    Needs connections to be parsed.
    """

    # Item ID and model skin -> fizzler type
    fizz_types = {}  # type: Dict[Tuple[str, int], FizzlerType]

    for fizz_type in FIZZ_TYPES.values():
        for item_id in fizz_type.item_ids:
            if ':' in item_id:
                item_id, barrier_type = item_id.split(':')
                if barrier_type == 'laserfield':
                    barrier_skin = 2
                elif barrier_type == 'fizzler':
                    barrier_skin = 0
                else:
                    LOGGER.error('Invalid barrier type ({}) for "{}"!',
                                 barrier_type, item_id)
                    fizz_types[item_id, 0] = fizz_type
                    fizz_types[item_id, 2] = fizz_type
                    continue
                fizz_types[item_id, barrier_skin] = fizz_type
            else:
                fizz_types[item_id, 0] = fizz_type
                fizz_types[item_id, 2] = fizz_type

    fizz_bases = {}  # type: Dict[str, Entity]
    fizz_models = defaultdict(list)  # type: Dict[str, List[Entity]]

    # Position and normal -> name, for output relays.
    fizz_pos = {
    }  # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str]

    # First use traits to gather up all the instances.
    for inst in vmf.by_class['func_instance']:
        traits = instance_traits.get(inst)
        if 'fizzler' not in traits:
            continue

        name = inst['targetname']

        if 'fizzler_model' in traits:
            name = name.rsplit('_model', 1)[0]
            fizz_models[name].append(inst)
            inst.remove()
        elif 'fizzler_base' in traits:
            fizz_bases[name] = inst
        else:
            LOGGER.warning('Fizzler "{}" has non-base, non-model instance?',
                           name)
            continue

        origin = Vec.from_str(inst['origin'])
        normal = Vec(z=1).rotate_by_str(inst['angles'])
        fizz_pos[origin.as_tuple(), normal.as_tuple()] = name

    for name, base_inst in fizz_bases.items():
        models = fizz_models[name]
        up_axis = Vec(y=1).rotate_by_str(base_inst['angles'])

        # If upside-down, make it face upright.
        if up_axis == (0, 0, -1):
            up_axis = Vec(z=1)

        base_inst.outputs.clear()

        # Now match the pairs of models to each other.
        # The length axis is the line between them.
        # We don't care about the instances after this, so don't keep track.
        length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis()

        emitters = []  # type: List[Tuple[Vec, Vec]]

        model_pairs = {}  # type: Dict[Tuple[float, float], Vec]

        model_skin = models[0].fixup.int('$skin')

        try:
            item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[
                base_inst['file'].casefold()]
            fizz_type = fizz_types[item_id, model_skin]
        except KeyError:
            LOGGER.warning('Fizzler types: {}', fizz_types.keys())
            raise ValueError('No fizzler type for "{}"!'.format(
                base_inst['file'], )) from None

        for attr_name in fizz_type.voice_attrs:
            voice_attrs[attr_name] = True

        for model in models:
            pos = Vec.from_str(model['origin'])
            try:
                other_pos = model_pairs.pop(pos.other_axes(length_axis))
            except KeyError:
                # No other position yet, we need to find that.
                model_pairs[pos.other_axes(length_axis)] = pos
                continue

            min_pos, max_pos = Vec.bbox(pos, other_pos)

            # Move positions to the wall surface.
            min_pos[length_axis] -= 64
            max_pos[length_axis] += 64
            emitters.append((min_pos, max_pos))

        FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters)

    # Delete all the old brushes associated with fizzlers
    for brush in (vmf.by_class['trigger_portal_cleanser']
                  | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush']):
        name = brush['targetname']
        if not name:
            continue
        name = name.rsplit('_brush')[0]
        if name in FIZZLERS:
            brush.remove()

    # Check for fizzler output relays.
    relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>',
                                      silent=True)
    if not relay_file:
        # No relay item - deactivated most likely.
        return

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in relay_file:
            continue

        inst.remove()

        relay_item = connections.ITEMS[inst['targetname']]

        try:
            fizz_name = fizz_pos[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()]
            fizz_item = connections.ITEMS[fizz_name]
        except KeyError:
            # Not placed on a fizzler, or a fizzler with no IO
            # - ignore, and destroy.
            for out in list(relay_item.outputs):
                out.remove()
            for out in list(relay_item.inputs):
                out.remove()
            del connections.ITEMS[relay_item.name]
            continue

        # Copy over fixup values
        fizz_item.inst.fixup.update(inst.fixup)

        # Copy over the timer delay set in the relay.
        fizz_item.timer = relay_item.timer
        # Transfer over antlines.
        fizz_item.antlines |= relay_item.antlines
        fizz_item.shape_signs += relay_item.shape_signs
        fizz_item.ind_panels |= relay_item.ind_panels

        # Remove the relay item so it doesn't get added to the map.
        del connections.ITEMS[relay_item.name]

        for conn in list(relay_item.outputs):
            conn.from_item = fizz_item
def res_resizeable_trigger(res: Property):
    """Replace two markers with a trigger brush.

    This is run once to affect all of an item.
    Options:
    'markerInst': <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    'markerItem': The item's ID
    'previewVar': A stylevar which enables/disables the preview overlay.
    'previewinst': An instance to place at the marker location in preview mode.
        This should contain checkmarks to display the value when testing.
    'previewMat': If set, the material to use for an overlay func_brush.
        The brush will be parented to the trigger, so it vanishes once killed.
        It is also non-solid.
    'previewScale': The scale for the func_brush materials.
    'previewActivate', 'previewDeactivate': The 'instance:name;Input' value
        to turn the previewInst on and off.

    'triggerActivate, triggerDeactivate': The outputs used when the trigger
        turns on or off.

    'coopVar': The instance variable which enables detecting both Coop players.
        The trigger will be a trigger_playerteam.

    'coopActivate, coopDeactivate': The outputs used when coopVar is enabled.
        These should be suitable for a logic_coop_manager.
    'coopOnce': If true, kill the manager after it first activates.

    'keys': A block of keyvalues for the trigger brush. Origin and targetname
        will be set automatically.
    'localkeys': The same as above, except values will be changed to use
        instance-local names.
    """
    marker = resolve_inst(res['markerInst'])

    markers = {}
    for inst in vbsp.VMF.by_class['func_instance']:
        if inst['file'].casefold() in marker:
            markers[inst['targetname']] = inst

    if not markers:  # No markers in the map - abort
        return RES_EXHAUSTED

    trig_act = res['triggerActivate', 'OnStartTouchAll']
    trig_deact = res['triggerDeactivate','OnEndTouchAll']

    coop_var = res['coopVar', None]
    coop_act = res['coopActivate', 'OnChangeToAllTrue']
    coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse']
    coop_only_once = res.bool('coopOnce')

    marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()]
    mark_act_name, mark_act_out = marker_connection.out_act
    mark_deact_name, mark_deact_out = marker_connection.out_deact
    del marker_connection

    preview_var = res['previewVar', ''].casefold()

    # Display preview overlays if it's preview mode, and the style var is true
    # or does not exist
    if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        pre_act_name, pre_act_inp = Output.parse_name(
            res['previewActivate', ''])
        pre_deact_name, pre_deact_inp = Output.parse_name(
            res['previewDeactivate', ''])
        preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25)
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        pre_act_name = pre_deact_name = None
        pre_act_inp = pre_deact_inp = ''
        preview_scale = 0.25

    # Now convert each brush
    # Use list() to freeze it, allowing us to delete from the dict
    for targ, inst in list(markers.items()):  # type: str, VLib.Entity
        for out in inst.output_targets():
            if out in markers:
                other = markers[out]  # type: Entity
                del markers[out]  # Don't let it get repeated
                break
        else:
            if inst.fixup['$connectioncount'] == '0':
                # If the item doesn't have any connections, 'connect'
                # it to itself so we'll generate a 1-block trigger.
                other = inst
            else:
                continue  # It's a marker with an input, the other in the pair
                # will handle everything.

        for ent in {inst, other}:
            # Only do once if inst == other
            ent.remove()

        is_coop = vbsp.GAME_MODE == 'COOP' and (
            inst.fixup.bool(coop_var) or
            other.fixup.bool(coop_var)
        )

        bbox_min, bbox_max = Vec.bbox(
            Vec.from_str(inst['origin']),
            Vec.from_str(other['origin'])
        )

        # Extend to the edge of the blocks.
        bbox_min -= 64
        bbox_max += 64

        out_ent = trig_ent = vbsp.VMF.create_ent(
            classname='trigger_multiple',  # Default
            # Use the 1st instance's name - that way other inputs control the
            # trigger itself.
            targetname=targ,
            origin=inst['origin'],
            angles='0 0 0',
        )
        trig_ent.solids = [
            vbsp.VMF.make_prism(
                bbox_min,
                bbox_max,
                mat=const.Tools.TRIGGER,
            ).solid,
        ]

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, inst, res)

        if is_coop:
            trig_ent['spawnflags'] = '1'  # Clients
            trig_ent['classname'] = 'trigger_playerteam'

            out_ent_name = conditions.local_name(inst, 'man')
            out_ent = vbsp.VMF.create_ent(
                classname='logic_coop_manager',
                targetname=out_ent_name,
                origin=inst['origin']
            )
            if coop_only_once:
                # Kill all the ents when both players are present.
                out_ent.add_out(
                    Output('OnChangeToAllTrue', out_ent_name, 'Kill'),
                    Output('OnChangeToAllTrue', targ, 'Kill'),
                )
            trig_ent.add_out(
                Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'),
                Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'),
                Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'),
                Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'),
            )
            act_out = coop_act
            deact_out = coop_deact
        else:
            act_out = trig_act
            deact_out = trig_deact

        if preview_mat:
            preview_brush = vbsp.VMF.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=inst['origin'],

                Solidity='1',  # Not solid
                drawinfastreflection='1',  # Draw in goo..

                # Disable shadows and lighting..
                disableflashlight='1',
                disablereceiveshadows='1',
                disableshadowdepth='1',
                disableshadows='1',
            )
            preview_brush.solids = [
                # Make it slightly smaller, so it doesn't z-fight with surfaces.
                vbsp.VMF.make_prism(
                    bbox_min + 0.5,
                    bbox_max - 0.5,
                    mat=preview_mat,
                ).solid,
            ]
            for face in preview_brush.sides():
                face.scale = preview_scale

        if preview_inst_file:
            vbsp.VMF.create_ent(
                classname='func_instance',
                targetname=targ + '_preview',
                file=preview_inst_file,
                # Put it at the second marker, since that's usually
                # closest to antlines if present.
                origin=other['origin'],
            )

            if pre_act_name and trig_act:
                out_ent.add_out(Output(
                    trig_act,
                    targ + '_preview',
                    inst_in=pre_act_name,
                    inp=pre_act_inp,
                ))
            if pre_deact_name and trig_deact:
                out_ent.add_out(Output(
                    trig_deact,
                    targ + '_preview',
                    inst_in=pre_deact_name,
                    inp=pre_deact_inp,
                ))

        # Now copy over the outputs from the markers, making it work.
        for out in inst.outputs + other.outputs:
            # Skip the output joining the two markers together.
            if out.target == other['targetname']:
                continue

            if out.inst_out == mark_act_name and out.output == mark_act_out:
                ent_out = act_out
            elif out.inst_out == mark_deact_name and out.output == mark_deact_out:
                ent_out = deact_out
            else:
                continue  # Skip this output - it's somehow invalid for this item.

            if not ent_out:
                continue  # Allow setting the output to "" to skip

            out_ent.add_out(Output(
                ent_out,
                out.target,
                inst_in=out.inst_in,
                inp=out.input,
                param=out.params,
                delay=out.delay,
                times=out.times,
            ))

    return RES_EXHAUSTED
예제 #31
0
def improve_item(item: Property) -> None:
    """Improve editoritems formats in various ways.

    This operates inplace.
    """
    # OccupiedVoxels does not allow specifying 'volume' regions like
    # EmbeddedVoxel. Implement that.

    # First for 32^2 cube sections.
    for voxel_part in item.find_all("Exporting", "OccupiedVoxels",
                                    "SurfaceVolume"):
        if 'subpos1' not in voxel_part or 'subpos2' not in voxel_part:
            LOGGER.warning(
                'Item {} has invalid OccupiedVoxels part '
                '(needs SubPos1 and SubPos2)!',
                item['type'],
            )
            continue
        voxel_part.name = "Voxel"
        pos_1 = None
        voxel_subprops = list(voxel_part)
        voxel_part.clear()
        for prop in voxel_subprops:
            if prop.name not in ('subpos', 'subpos1', 'subpos2'):
                voxel_part.append(prop)
                continue
            pos_2 = Vec.from_str(prop.value)
            if pos_1 is None:
                pos_1 = pos_2
                continue

            bbox_min, bbox_max = Vec.bbox(pos_1, pos_2)
            pos_1 = None
            for pos in Vec.iter_grid(bbox_min, bbox_max):
                voxel_part.append(
                    Property("Surface", [
                        Property("Pos", str(pos)),
                    ]))
        if pos_1 is not None:
            LOGGER.warning(
                'Item {} has only half of SubPos bbox!',
                item['type'],
            )

    # Full blocks
    for occu_voxels in item.find_all("Exporting", "OccupiedVoxels"):
        for voxel_part in list(occu_voxels.find_all("Volume")):
            del occu_voxels['Volume']

            if 'pos1' not in voxel_part or 'pos2' not in voxel_part:
                LOGGER.warning(
                    'Item {} has invalid OccupiedVoxels part '
                    '(needs Pos1 and Pos2)!', item['type'])
                continue
            voxel_part.name = "Voxel"
            bbox_min, bbox_max = Vec.bbox(
                voxel_part.vec('pos1'),
                voxel_part.vec('pos2'),
            )
            del voxel_part['pos1']
            del voxel_part['pos2']
            for pos in Vec.iter_grid(bbox_min, bbox_max):
                new_part = voxel_part.copy()
                new_part['Pos'] = str(pos)
                occu_voxels.append(new_part)
예제 #32
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

    This is run once to affect all of an item.  
    Options:
    * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    * `markerItem`: The item's ID
    * `previewConf`: A item config which enables/disables the preview overlay.
    * `previewInst`: An instance to place at the marker location in preview mode.
        This should contain checkmarks to display the value when testing.
    * `previewMat`: If set, the material to use for an overlay func_brush.
        The brush will be parented to the trigger, so it vanishes once killed.
        It is also non-solid.
    * `previewScale`: The scale for the func_brush materials.
    * `previewActivate`, `previewDeactivate`: The VMF output to turn the
        previewInst on and off.

    * `triggerActivate, triggerDeactivate`: The `instance:name;Output`
        outputs used when the trigger turns on or off.

    * `coopVar`: The instance variable which enables detecting both Coop players.
        The trigger will be a trigger_playerteam.

    * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used
        when coopVar is enabled. These should be suitable for a logic_coop_manager.
    * `coopOnce`: If true, kill the manager after it first activates.

    * `keys`: A block of keyvalues for the trigger brush. Origin and targetname
        will be set automatically.
    * `localkeys`: The same as above, except values will be changed to use
        instance-local names.
    """
    marker = instanceLocs.resolve(res['markerInst'])

    marker_names = set()

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in marker:
            marker_names.add(inst['targetname'])
            # Unconditionally delete from the map, so it doesn't
            # appear even if placed wrongly.
            inst.remove()

    if not marker_names:  # No markers in the map - abort
        return RES_EXHAUSTED

    item_id = res['markerItem']

    # Synthesise the item type used for the final trigger.
    item_type_sp = connections.ItemType(
        id=item_id + ':TRIGGER',
        output_act=Output.parse_name(res['triggerActivate',
                                         'OnStartTouchAll']),
        output_deact=Output.parse_name(res['triggerDeactivate',
                                           'OnEndTouchAll']),
    )

    # For Coop, we add a logic_coop_manager in the mix so both players can
    # be handled.
    try:
        coop_var = res['coopVar']
    except LookupError:
        coop_var = item_type_coop = None
        coop_only_once = False
    else:
        coop_only_once = res.bool('coopOnce')
        item_type_coop = connections.ItemType(
            id=item_id + ':TRIGGER_COOP',
            output_act=Output.parse_name(res['coopActivate',
                                             'OnChangeToAllTrue']),
            output_deact=Output.parse_name(res['coopDeactivate',
                                               'OnChangeToAnyFalse']),
        )

    # Display preview overlays if it's preview mode, and the config is true
    pre_act = pre_deact = None
    if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''],
                                                     False):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        preview_scale = res.float('previewScale', 0.25)
        # None if not found.
        with suppress(LookupError):
            pre_act = Output.parse(res.find_key('previewActivate'))
        with suppress(LookupError):
            pre_deact = Output.parse(res.find_key('previewDeactivate'))
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        preview_scale = 0.25

    # Now go through each brush.
    # We do while + pop to allow removing both names each loop through.
    todo_names = set(marker_names)
    while todo_names:
        targ = todo_names.pop()

        mark1 = connections.ITEMS.pop(targ)
        for conn in mark1.outputs:
            if conn.to_item.name in marker_names:
                mark2 = conn.to_item
                conn.remove()  # Delete this connection.
                todo_names.discard(mark2.name)
                del connections.ITEMS[mark2.name]
                break
        else:
            if not mark1.inputs:
                # If the item doesn't have any connections, 'connect'
                # it to itself so we'll generate a 1-block trigger.
                mark2 = mark1
            else:
                # It's a marker with an input, the other in the pair
                # will handle everything.
                # But reinstate it in ITEMS.
                connections.ITEMS[targ] = mark1
                continue

        inst1 = mark1.inst
        inst2 = mark2.inst

        is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and (
            inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var))

        bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']),
                                      Vec.from_str(inst2['origin']))
        origin = (bbox_max + bbox_min) / 2

        # Extend to the edge of the blocks.
        bbox_min -= 64
        bbox_max += 64

        out_ent = trig_ent = vmf.create_ent(
            classname='trigger_multiple',  # Default
            targetname=targ,
            origin=origin,
            angles='0 0 0',
        )
        trig_ent.solids = [
            vmf.make_prism(
                bbox_min,
                bbox_max,
                mat=const.Tools.TRIGGER,
            ).solid,
        ]

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, inst, res)

        if is_coop:
            trig_ent['spawnflags'] = '1'  # Clients
            trig_ent['classname'] = 'trigger_playerteam'

            out_ent = manager = vmf.create_ent(
                classname='logic_coop_manager',
                targetname=conditions.local_name(inst, 'man'),
                origin=origin,
            )

            item = connections.Item(
                out_ent,
                item_type_coop,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

            if coop_only_once:
                # Kill all the ents when both players are present.
                manager.add_out(
                    Output('OnChangeToAllTrue', manager, 'Kill'),
                    Output('OnChangeToAllTrue', targ, 'Kill'),
                )
            trig_ent.add_out(
                Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'),
                Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'),
                Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'),
                Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'),
            )
        else:
            item = connections.Item(
                trig_ent,
                item_type_sp,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

        # Register, and copy over all the antlines.
        connections.ITEMS[item.name] = item
        item.ind_panels = mark1.ind_panels | mark2.ind_panels
        item.antlines = mark1.antlines | mark2.antlines
        item.shape_signs = mark1.shape_signs + mark2.shape_signs

        if preview_mat:
            preview_brush = vmf.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=origin,
                Solidity='1',  # Not solid
                drawinfastreflection='1',  # Draw in goo..

                # Disable shadows and lighting..
                disableflashlight='1',
                disablereceiveshadows='1',
                disableshadowdepth='1',
                disableshadows='1',
            )
            preview_brush.solids = [
                # Make it slightly smaller, so it doesn't z-fight with surfaces.
                vmf.make_prism(
                    bbox_min + 0.5,
                    bbox_max - 0.5,
                    mat=preview_mat,
                ).solid,
            ]
            for face in preview_brush.sides():
                face.scale = preview_scale

        if preview_inst_file:
            pre_inst = vmf.create_ent(
                classname='func_instance',
                targetname=targ + '_preview',
                file=preview_inst_file,
                # Put it at the second marker, since that's usually
                # closest to antlines if present.
                origin=inst2['origin'],
            )

            if pre_act is not None:
                out = pre_act.copy()
                out.inst_out, out.output = item.output_act()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)
            if pre_deact is not None:
                out = pre_deact.copy()
                out.inst_out, out.output = item.output_deact()
                out.target = conditions.local_name(pre_inst, out.target)
                out_ent.add_out(out)

        for conn in mark1.outputs | mark2.outputs:
            conn.from_item = item

    return RES_EXHAUSTED
예제 #33
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)
예제 #34
0
def brush_at_loc(
    inst: Entity,
    props: Property,
) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]:
    """Common code for posIsSolid and ReadSurfType.

    This returns the average tiletype, if both colors were found,
    and a set of all types found.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])

    # Allow using pos1 instead, to match pos2.
    pos = props.vec('pos1' if 'pos1' in props else 'pos')
    pos.z -= 64  # Subtract so origin is the floor-position

    pos.localise(origin, angles)

    norm = props.vec('dir', 0, 0, 1).rotate(*angles)

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

    result_var = props['setVar', '']
    # RemoveBrush is the pre-tiling name.
    should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False))

    tile_types: Set[tiling.TileType] = set()
    both_colors = False

    if 'pos2' in props:
        pos2 = props.vec('pos2')
        pos2.z -= 64  # Subtract so origin is the floor-position
        pos2.localise(origin, angles)

        bbox_min, bbox_max = Vec.bbox(pos, pos2)

        white_count = black_count = 0

        for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
            try:
                tiledef, u, v = tiling.find_tile(pos, norm)
            except KeyError:
                continue

            tile_type = tiledef[u, v]
            tile_types.add(tile_type)
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
            if tile_type.is_tile:
                if tile_type.color is tiling.Portalable.WHITE:
                    white_count += 1
                else:
                    black_count += 1

        both_colors = white_count > 0 and black_count > 0

        if white_count == black_count == 0:
            tile_type = tiling.TileType.VOID
            tile_types.add(tiling.TileType.VOID)
        elif white_count > black_count:
            tile_type = tiling.TileType.WHITE
        else:
            tile_type = tiling.TileType.BLACK
    else:
        # Single tile.
        try:
            tiledef, u, v = tiling.find_tile(pos, norm)
        except KeyError:
            tile_type = tiling.TileType.VOID
        else:
            tile_type = tiledef[u, v]
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
        tile_types.add(tile_type)

    if result_var:
        if tile_type.is_tile:
            # Don't distinguish between 4x4, goo sides
            inst.fixup[result_var] = tile_type.color.value
        elif tile_type is tiling.TileType.VOID:
            inst.fixup[result_var] = 'none'
        else:
            inst.fixup[result_var] = tile_type.name.casefold()

    return tile_type, both_colors, tile_types
예제 #35
0
    def export(self, vmf: VMF, *, wall_conf: AntType, floor_conf: AntType) -> None:
        """Add the antlines into the map."""

        # First, do some optimisation. If corners aren't defined, try and
        # optimise those antlines out by merging the straight segment
        # before/after it into the corners.

        collapse_line: list[Segment | None]
        if not wall_conf.tex_corner or not floor_conf.tex_corner:
            collapse_line = list(self.line)
            for i, seg in enumerate(collapse_line):
                if seg is None or seg.type is not SegType.STRAIGHT:
                    continue
                if (floor_conf if seg.on_floor else wall_conf).tex_corner:
                    continue
                for corner_ind in [i-1, i+1]:
                    if i == -1:
                        continue
                    try:
                        corner = collapse_line[corner_ind]
                    except IndexError:
                        # Each end of the list.
                        continue

                    if (
                        corner is not None and
                        corner.type is SegType.CORNER and
                        corner.normal == seg.normal
                    ):
                        corner_pos = corner.start
                        if (seg.start - corner_pos).mag_sq() == 8 ** 2:
                            # The line segment is at the border between them,
                            # the corner is at the center. So move double the
                            # distance towards the corner, so it reaches to the
                            # other side of the corner and replaces it.
                            seg.start += 2 * (corner_pos - seg.start)
                            # Remove corner by setting to None, so we aren't
                            # resizing the list constantly.
                            collapse_line[corner_ind] = None
                            # Now merge together the tiledefs.
                            seg.tiles.update(corner.tiles)
                        elif (seg.end - corner_pos).mag_sq() == 8 ** 2:
                            seg.end += 2 * (corner_pos - seg.end)
                            collapse_line[corner_ind] = None
                            seg.tiles.update(corner.tiles)

            self.line[:] = [seg for seg in collapse_line if seg is not None]
            LOGGER.info('Collapsed {} antline corners', collapse_line.count(None))

        for seg in self.line:
            conf = floor_conf if seg.on_floor else wall_conf
            # Check tiledefs in the voxels, and assign just in case.
            # antline corner items don't have them defined, and some embedfaces don't work
            # properly. But we keep any segments actually defined also.
            mins, maxs = Vec.bbox(seg.start, seg.end)
            norm_axis = seg.normal.axis()
            u_axis, v_axis = Vec.INV_AXIS[norm_axis]
            for pos in Vec.iter_line(mins, maxs, 128):
                pos[u_axis] = pos[u_axis] // 128 * 128 + 64
                pos[v_axis] = pos[v_axis] // 128 * 128 + 64
                pos -= 64 * seg.normal
                try:
                    tile = tiling.TILES[pos.as_tuple(), seg.normal.as_tuple()]
                except KeyError:
                    pass
                else:
                    seg.tiles.add(tile)

            rng = rand.seed(b'antline', seg.start, seg.end)
            if seg.type is SegType.CORNER:
                mat: AntTex
                if rng.randrange(100) < conf.broken_chance:
                    mat = rng.choice(conf.broken_corner or conf.broken_straight)
                else:
                    mat = rng.choice(conf.tex_corner or conf.tex_straight)

                # Because we can, apply a random rotation to mix up the texture.
                orient = Matrix.from_angle(seg.normal.to_angle(
                    rng.choice((0.0, 90.0, 180.0, 270.0))
                ))
                self._make_overlay(
                    vmf,
                    seg,
                    seg.start,
                    16.0 * orient.left(),
                    16.0 * orient.up(),
                    mat,
                )
            else:  # Straight
                # TODO: Break up these segments.
                for a, b, is_broken in seg.broken_iter(conf.broken_chance):
                    if is_broken:
                        mat = rng.choice(conf.broken_straight)
                    else:
                        mat = rng.choice(conf.tex_straight)
                    self._make_straight(
                        vmf,
                        seg,
                        a,
                        b,
                        mat,
                    )