Beispiel #1
0
def add_locking(item: Item) -> None:
    """Create IO to control buttons from the target item.

    This allows items to customise how buttons behave.
    """
    if item.config.output_lock is None and item.config.output_unlock is None:
        return
    if item.config.input_type is InputType.DUAL:
        LOGGER.warning(
            'Item type ({}) with locking IO, but dual inputs. '
            'Locking functionality is ignored!',
            item.config.id
        )
        return

    # If more than one, it's not logical to lock the button.
    try:
        [lock_conn] = item.inputs
    except ValueError:
        return

    lock_button = lock_conn.from_item

    if item.config.inf_lock_only and lock_button.timer is not None:
        return

    # Check the button doesn't also activate other things -
    # we need exclusive control.
    # Also the button actually needs to be lockable.
    if len(lock_button.outputs) != 1 or not lock_button.config.lock_cmd:
        return

    instance_traits.get(item.inst).add('locking_targ')
    instance_traits.get(lock_button.inst).add('locking_btn')

    # Force the item to not have a timer.
    for pan in item.ind_panels:
        pan.remove()
    item.ind_panels.clear()

    for output, input_cmds in [
        (item.config.output_lock, lock_button.config.lock_cmd),
        (item.config.output_unlock, lock_button.config.unlock_cmd)
    ]:
        if not output:
            continue

        for cmd in input_cmds:
            if cmd.target:
                target = conditions.local_name(lock_button.inst, cmd.target)
            else:
                target = lock_button.inst
            item.add_io_command(
                output,
                target,
                cmd.input,
                cmd.params,
                delay=cmd.delay,
                times=cmd.times,
            )
Beispiel #2
0
def flag_has_trait(inst: Entity, flag: Property) -> bool:
    """Check if the instance has a specific 'trait', which is set by code.

    Current traits:

    * `white`, `black`: If editoritems indicates the colour of the item.
    * `arrival_departure_transition`: `arrival_departure_transition_ents`.
    * `barrier`: Glass/grating instances:
        * `barrier_128`: Segment instance.
        * `barrier_frame`: Any frame part.
            * `frame_convex_corner`: Convex corner (unused).
            * `frame_short`: Shortened frame to fit a corner.
            * `frame_straight`: Straight frame section.
            * `frame_corner`: Frame corner section.
            * `frame_left`: Left half of the frame.
            * `frame_right`: Right half of the frame.
    * `floor_button`: ItemButtonFloor type item:
        * `btn_ball`: Button Type = Sphere.
        * `btn_cube`: Button Type = Cube
        * `weighted`: Button Type = Weighted
    * `dropperless`: A dropperless Cube:
        * `cube_standard`: Normal Cube.
        * `cube_companion`: Companion Cube.
        * `cube_ball`: Edgeless Safety Cube.
        * `cube_reflect`: Discouragment Redirection Cube.
        * `cube_franken`: FrankenTurret.
    * `preplaced`: The various pre-existing instances:
        * `coop_corridor`: A Coop exit Corridor.
        * `sp_corridor`: SP entry or exit corridor.
        * `corridor_frame`: White/black door frame.
        * `corridor_1`-`7`: The specified entry/exit corridor.
        * `elevator`: An elevator instance.
        * `entry_elevator`: Entry Elevator.
        * `exit_elevator`: Exit Elevator.
        * `entry_corridor`: Entry SP Corridor.
        * `exit_corridor`: Exit SP/Coop Corridor.
    * `fizzler`: A fizzler item:
        * `fizzler_base`: Logic instance.
        * `fizzler_model`: Model instance.
        * `cust_shape`: Set if the fizzler has been moved to a custom position
          by ReshapeFizzler.
    * `locking_targ`: Target of a locking pedestal button.
    * `locking_btn`: Locking pedestal button.
    * `paint_dropper`: Gel Dropper:
        * `paint_dropper_bomb`: Bomb-type dropper.
        * `paint_dropper_sprayer`: Sprayer-type dropper.
    * `panel_angled`: Angled Panel-type item.
    * `track_platform`: Track Platform-style item:
        * `plat_bottom`: Bottom frame.
        * `plat_bottom_grate`: Grating.
        * `plat_middle`: Middle frame.
        * `plat_single`: One-long frame.
        * `plat_top`: Top frame.
        * `plat_non_osc`: Non-oscillating platform.
        * `plat_osc`: Oscillating platform.
    * `tbeam_emitter`: Funnel emitter.
    * `tbeam_frame`: Funnel frame.
    """
    return flag.value.casefold() in instance_traits.get(inst)
Beispiel #3
0
def set_random_seed(inst: Entity, seed: str) -> None:
    """Compute and set a random seed for a specific entity."""
    from precomp import instance_traits

    name = inst['targetname']
    # The global instances like elevators always get the same name, or
    # none at all so we cannot use those for the seed. Instead use the global
    # seed.
    if name == '' or 'preplaced' in instance_traits.get(inst):
        import vbsp
        random.seed('{}{}{}{}'.format(
            vbsp.MAP_RAND_SEED, seed, inst['origin'], inst['angles'],
        ))
    else:
        # We still need to use angles and origin, since things like
        # fizzlers might not get unique names.
        random.seed('{}{}{}{}'.format(
            inst['targetname'], seed, inst['origin'], inst['angles']
        ))
Beispiel #4
0
    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        for picker_name, picker_var in picker_vars:
            picker_val = temp_data.picker_results.get(picker_name, None)
            if picker_val is not None:
                inst.fixup[picker_var] = picker_val.value
            else:
                inst.fixup[picker_var] = ''
Beispiel #5
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
Beispiel #6
0
 def traits(self) -> Set[str]:
     """Return the set of instance traits for the item."""
     return instance_traits.get(self.inst)
Beispiel #7
0
def res_import_template(vmf: VMF, inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

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

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

    temp_id = inst.fixup.substitute(orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):

        def visgroup_func(group):
            """Use all the groups."""
            yield from group

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

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

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

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

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

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

    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles', '0 0 0'])
    temp_data = template_brush.import_template(
        vmf,
        template,
        origin,
        angles,
        targetname=inst['targetname', ''],
        force_type=force_type,
        visgroup_choose=visgroup_func,
        add_to_map=True,
        additional_visgroups=visgroups,
        bind_tile_pos=bind_tile_pos,
    )

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

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

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

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

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