Exemplo n.º 1
0
def calc_connections(
    vmf: VMF,
    antlines: Dict[str, List[Antline]],
    shape_frame_tex: List[str],
    enable_shape_frame: bool,
    *,  # Don't mix up antlines!
    antline_wall: AntType,
    antline_floor: AntType,
) -> None:
    """Compute item connections from the map file.

    This also fixes cases where items have incorrect checkmark/timer signs.
    Instance Traits must have been calculated.
    It also applies frames to shape signage to distinguish repeats.
    """
    # First we want to match targetnames to item types.
    toggles = {}  # type: Dict[str, Entity]
    # Accumulate all the signs into groups, so the list should be 2-long:
    # sign_shapes[name, material][0/1]
    sign_shape_overlays = defaultdict(
        list)  # type: Dict[Tuple[str, str], List[Entity]]

    # Indicator panels
    panels = {}  # type: Dict[str, Entity]

    # We only need to pay attention for TBeams, other items we can
    # just detect any output.
    tbeam_polarity = {OutNames.IN_SEC_ACT, OutNames.IN_SEC_DEACT}
    # Also applies to other items, but not needed for this analysis.
    tbeam_io = {OutNames.IN_ACT, OutNames.IN_DEACT}

    for inst in vmf.by_class['func_instance']:
        inst_name = inst['targetname']
        # No connections, so nothing to worry about.
        if not inst_name:
            continue

        traits = instance_traits.get(inst)

        if 'indicator_toggle' in traits:
            toggles[inst_name] = inst
            # We do not use toggle instances.
            inst.remove()
        elif 'indicator_panel' in traits:
            panels[inst_name] = inst
        elif 'fizzler_model' in traits:
            # Ignore fizzler models - they shouldn't have the connections.
            # Just the base itself.
            pass
        else:
            # Normal item.
            item_id = instance_traits.get_item_id(inst)
            if item_id is None:
                LOGGER.warning('No item ID for "{}"!', inst)
                continue
            try:
                item_type = ITEM_TYPES[item_id.casefold()]
            except KeyError:
                LOGGER.warning('No item type for "{}"!', item_id)
                continue
            if item_type is None:
                # It exists, but has no I/O.
                continue

            # Pass in the defaults for antline styles.
            ITEMS[inst_name] = Item(
                inst,
                item_type,
                ant_floor_style=antline_floor,
                ant_wall_style=antline_wall,
            )

            # Strip off the original connection count variables, these are
            # invalid.
            if item_type.input_type is InputType.DUAL:
                del inst.fixup[consts.FixupVars.CONN_COUNT]
                del inst.fixup[consts.FixupVars.CONN_COUNT_TBEAM]

    for over in vmf.by_class['info_overlay']:
        name = over['targetname']
        mat = over['material']
        if mat in SIGN_ORDER_LOOKUP:
            sign_shape_overlays[name, mat.casefold()].append(over)

    # Name -> signs pairs
    sign_shapes = defaultdict(list)  # type: Dict[str, List[ShapeSignage]]
    # By material index, for group frames.
    sign_shape_by_index = defaultdict(
        list)  # type: Dict[int, List[ShapeSignage]]
    for (name, mat), sign_pair in sign_shape_overlays.items():
        # It's possible - but rare - for more than 2 to be in a pair.
        # We have to just treat them as all in their 'pair'.
        # Shouldn't be an issue, it'll be both from one item...
        shape = ShapeSignage(sign_pair)
        sign_shapes[name].append(shape)
        sign_shape_by_index[shape.index].append(shape)

    # Now build the connections and items.
    for item in ITEMS.values():
        input_items: List[Item] = []  # Instances we trigger
        inputs: Dict[str, List[Output]] = defaultdict(list)

        if item.inst.outputs and item.config is None:
            raise ValueError('No connections for item "{}", '
                             'but outputs in the map!'.format(
                                 instance_traits.get_item_id(item.inst)))

        for out in item.inst.outputs:
            inputs[out.target].append(out)

        # Remove the original outputs, we've consumed those already.
        item.inst.outputs.clear()

        # Pre-set the timer value, for items without antlines but with an output.
        if consts.FixupVars.TIM_DELAY in item.inst.fixup:
            if item.config.output_act or item.config.output_deact:
                item.timer = tim = item.inst.fixup.int(
                    consts.FixupVars.TIM_DELAY)
                if not (1 <= tim <= 30):
                    # These would be infinite.
                    item.timer = None

        for out_name in inputs:
            # Fizzler base -> model/brush outputs, ignore these (discard).
            # fizzler.py will regenerate as needed.
            if out_name.rstrip('0123456789').endswith(
                ('_modelStart', '_modelEnd', '_brush')):
                continue

            if out_name in toggles:
                inst_toggle = toggles[out_name]
                try:
                    item.antlines.update(
                        antlines[inst_toggle.fixup['indicator_name']])
                except KeyError:
                    pass
            elif out_name in panels:
                pan = panels[out_name]
                item.ind_panels.add(pan)
                if pan.fixup.bool(consts.FixupVars.TIM_ENABLED):
                    item.timer = tim = pan.fixup.int(
                        consts.FixupVars.TIM_DELAY)
                    if not (1 <= tim <= 30):
                        # These would be infinite.
                        item.timer = None
                else:
                    item.timer = None
            else:
                try:
                    inp_item = ITEMS[out_name]
                except KeyError:
                    raise ValueError(
                        '"{}" is not a known instance!'.format(out_name))
                else:
                    input_items.append(inp_item)
                    if inp_item.config is None:
                        raise ValueError('No connections for item "{}", '
                                         'but inputs in the map!'.format(
                                             instance_traits.get_item_id(
                                                 inp_item.inst)))

        for inp_item in input_items:
            # Default A/B type.
            conn_type = ConnType.DEFAULT
            in_outputs = inputs[inp_item.name]

            if inp_item.config.id == 'ITEM_TBEAM':
                # It's a funnel - we need to figure out if this is polarity,
                # or normal on/off.
                for out in in_outputs:
                    if out.input in tbeam_polarity:
                        conn_type = ConnType.TBEAM_DIR
                        break
                    elif out.input in tbeam_io:
                        conn_type = ConnType.TBEAM_IO
                        break
                else:
                    raise ValueError('Excursion Funnel "{}" has inputs, '
                                     'but no valid types!'.format(
                                         inp_item.name))

            conn = Connection(
                inp_item,
                item,
                conn_type,
                in_outputs,
            )
            conn.add()

    # Make signage frames
    shape_frame_tex = [mat for mat in shape_frame_tex if mat]
    if shape_frame_tex and enable_shape_frame:
        for shape_mat in sign_shape_by_index.values():
            # Sort so which gets what frame is consistent.
            shape_mat.sort()
            for index, shape in enumerate(shape_mat):
                shape.repeat_group = index
                if index == 0:
                    continue  # First, no frames..
                frame_mat = shape_frame_tex[(index - 1) % len(shape_frame_tex)]

                for overlay in shape:
                    frame = overlay.copy()
                    shape.overlay_frames.append(frame)
                    vmf.add_ent(frame)
                    frame['material'] = frame_mat
                    frame['renderorder'] = 1  # On top
Exemplo n.º 2
0
    def read_from_map(self, vmf: VMF, has_attr: dict[str, bool],
                      items: dict[str, editoritems.Item]) -> None:
        """Given the map file, set blocks."""
        from precomp.instance_traits import get_item_id
        from precomp import bottomlessPit

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

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

            pos = world_to_grid(Vec.from_str(str_pos))

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

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

        can_have_pit = bottomlessPit.pits_allowed()

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

            bbox_min, bbox_max = brush.get_bbox()

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

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

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

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

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

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

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

                continue

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

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

        LOGGER.info('Analysed map, filling air... ({} starting positions..)',
                    len(air_search_locs))
        self.fill_air(goo_search_locs + air_search_locs)
        LOGGER.info('Air filled!')