Beispiel #1
0
def res_replace_instance(vmf: VMF, inst: Entity, res: Property):
    """Replace an instance with another entity.

    `keys` and `localkeys` defines the new keyvalues used.
    `targetname` and `angles` are preset, and `origin` will be used to offset
    the given amount from the current location.
    If `keep_instance` is true, the instance entity will be kept instead of
    removed.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])

    if not res.bool('keep_instance'):
        inst.remove()  # Do this first to free the ent ID, so the new ent has
        # the same one.

    # We copy to allow us to still access the $fixups and other values.
    new_ent = inst.copy(des_id=inst.id)
    new_ent.clear_keys()
    # Ensure there's a classname, just in case.
    new_ent['classname'] = 'info_null'

    vmf.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Beispiel #2
0
def res_create_entity(vmf: VMF, inst: Entity, res: Property):
    """Create an entity.

    * `keys` and `localkeys` defines the new keyvalues used.
    * `Origin` will be used to offset the given amount from the current location.
    """

    origin = Vec.from_str(inst['origin'])

    new_ent = vmf.create_ent(
        # Ensure there's a classname, just in case.
        classname='info_null')

    conditions.set_ent_keys(new_ent, inst, res)

    origin += Vec.from_str(new_ent['origin']).rotate_by_str(inst['angles'])

    new_ent['origin'] = origin
    new_ent['angles'] = inst['angles']
Beispiel #3
0
def res_create_entity(vmf: VMF, inst: Entity, res: Property):
    """Create an entity.

    * `keys` and `localkeys` defines the new keyvalues used.
    * `origin` and `angles` are local to the instance.
    """

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

    new_ent = vmf.create_ent(
        # Ensure there's these critical values.
        classname='info_null',
        origin='0 0 0',
        angles='0 0 0',
    )

    conditions.set_ent_keys(new_ent, inst, res)

    new_ent['origin'] = Vec.from_str(new_ent['origin']) @ orient + origin
    new_ent['angles'] = Angle.from_str(new_ent['angles']) @ orient
Beispiel #4
0
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property):
    """Properly setup rotating brush entities to match the instance.

    This uses the orientation of the instance to determine the correct
    spawnflags to make it rotate in the correct direction.

    This can either modify an existing entity (which may be in an instance),
    or generate a new one. The generated brush will be 2x2x2 units large,
    and always set to be non-solid.

    For both modes:
    - `Axis`: specifies the rotation axis local to the instance.
    - `Reversed`: If set, flips the direction around.
    - `Classname`: Specifies which entity, since the spawnflags required varies.

    For application to an existing entity:
    - `ModifyTarget`: The local name of the entity to modify.

    For brush generation mode:

    - `Pos` and `name` are local to the
      instance, and will set the `origin` and `targetname` respectively.
    - `Keys` are any other keyvalues to be be set.
    - `Flags` sets additional spawnflags. Multiple values may be
       separated by `+`, and will be added together.
    - `Classname` specifies which entity will be created, as well as
       which other values will be set to specify the correct orientation.
    - `AddOut` is used to add outputs to the generated entity. It takes
       the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If
       `Inst_targ` is defined, it will be used with the input to construct
       an instance proxy input. If `OnceOnly` is set, the output will be
       deleted when fired.

    Permitted entities:

       * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating)
       * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot)
       * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button)
       * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating)
       * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button)
    """
    des_axis = res['axis', 'z'].casefold()
    reverse = res.bool('reversed')
    door_type = res['classname', 'func_door_rotating']
    orient = Matrix.from_angle(Angle.from_str(ent['angles']))

    axis = round(Vec.with_axes(des_axis, 1) @ orient, 6)

    if axis.x > 0 or axis.y > 0 or axis.z > 0:
        # If it points forward, we need to reverse the rotating door
        reverse = not reverse
    axis = abs(axis)

    try:
        flag_values = FLAG_ROTATING[door_type]
    except KeyError:
        LOGGER.warning('Unknown rotating brush type "{}"!', door_type)
        return

    name = res['ModifyTarget', '']
    door_ent: Entity | None
    if name:
        name = conditions.local_name(ent, name)
        setter_loc = ent['origin']
        door_ent = None
        spawnflags = 0
    else:
        # Generate a brush.
        name = conditions.local_name(ent, res['name', ''])

        pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0'])
        pos += Vec.from_str(ent['origin', '0 0 0'])
        setter_loc = str(pos)

        door_ent = vmf.create_ent(
            classname=door_type,
            targetname=name,
            origin=pos.join(' '),
        )
        # Extra stuff to apply to the flags (USE, toggle, etc)
        spawnflags = sum(
            map(
                # Add together multiple values
                srctools.conv_int,
                res['flags', '0'].split('+')
                # Make the door always non-solid!
            )) | flag_values.get('solid_flags', 0)

        conditions.set_ent_keys(door_ent, ent, res)

        for output in res.find_all('AddOut'):
            door_ent.add_out(
                Output(
                    out=output['Output', 'OnUse'],
                    inp=output['Input', 'Use'],
                    targ=output['Target', ''],
                    inst_in=output['Inst_targ', None],
                    param=output['Param', ''],
                    delay=srctools.conv_float(output['Delay', '']),
                    times=(1 if srctools.conv_bool(output['OnceOnly',
                                                          False]) else -1),
                ))

        # Generate brush
        door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid]

    # Add or remove flags as needed
    for flag, value in zip(
        ('x', 'y', 'z', 'rev'),
        [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse],
    ):
        if flag not in flag_values:
            continue
        if door_ent is not None:
            if value:
                spawnflags |= flag_values[flag]
            else:
                spawnflags &= ~flag_values[flag]
        else:  # Place a KV setter to set this.
            vmf.create_ent(
                'comp_kv_setter',
                origin=setter_loc,
                target=name,
                mode='flags',
                kv_name=flag_values[flag],
                kv_value_global=value,
            )
    if door_ent is not None:
        door_ent['spawnflags'] = spawnflags

    # This ent uses a keyvalue for reversing...
    if door_type == 'momentary_rot_button':
        vmf.create_ent(
            'comp_kv_setter',
            origin=setter_loc,
            target=name,
            mode='kv',
            kv_name='StartDirection',
            kv_value_global='1' if reverse else '-1',
        )
Beispiel #5
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 #6
0
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None:
    """Implements SetPanelOptions and CreatePanel."""
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))
    normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6)
    origin = Vec.from_str(inst['origin'])
    uaxis, vaxis = Vec.INV_AXIS[normal.axis()]

    points: set[tuple[float, float, float]] = set()

    if 'point' in props:
        for prop in props.find_all('point'):
            points.add(
                conditions.resolve_offset(inst, prop.value,
                                          zoff=-64).as_tuple())
    elif 'pos1' in props and 'pos2' in props:
        pos1, pos2 = Vec.bbox(
            conditions.resolve_offset(inst,
                                      props['pos1', '-48 -48 0'],
                                      zoff=-64),
            conditions.resolve_offset(inst, props['pos2', '48 48 0'],
                                      zoff=-64),
        )
        points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32)))
    else:
        # Default to the full tile.
        points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple()
                       for u in [-48.0, -16.0, 16.0, 48.0]
                       for v in [-48.0, -16.0, 16.0, 48.0]})

    tiles_to_uv: dict[tiling.TileDef, set[tuple[int, int]]] = defaultdict(set)
    for pos in points:
        try:
            tile, u, v = tiling.find_tile(Vec(pos), normal, force=create)
        except KeyError:
            continue
        tiles_to_uv[tile].add((u, v))

    if not tiles_to_uv:
        LOGGER.warning('"{}": No tiles found for panels!', inst['targetname'])
        return

    # If bevels is provided, parse out the overall world positions.
    bevel_world: set[tuple[int, int]] | None
    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 = inst.fixup.substitute(props['template'])
            else:
                panel.template = ''
        if 'nodraw' in props:
            panel.nodraw = srctools.conv_bool(
                inst.fixup.substitute(props['nodraw'], allow_invert=True))
        if 'seal' in props:
            panel.seal = srctools.conv_bool(
                inst.fixup.substitute(props['seal'], allow_invert=True))
        if 'move_bullseye' in props:
            panel.steals_bullseye = srctools.conv_bool(
                inst.fixup.substitute(props['move_bullseye'],
                                      allow_invert=True))
    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[Entity
                           | None] = {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']),
                    Angle.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
Beispiel #7
0
def res_add_overlay_inst(vmf: VMF, inst: Entity,
                         res: Property) -> Optional[Entity]:
    """Add another instance on top of this one.

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

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

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

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

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

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

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

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

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

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

    return overlay_inst
Beispiel #8
0
def res_antlaser(vmf: VMF, res: Property):
    """The condition to generate AntLasers.

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

    beam_conf = res.find_key('BeamKeys', [])
    glow_conf = res.find_key('GlowKeys', [])
    cable_conf = res.find_key('CableKeys', [])

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

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

    # Find all the markers.
    nodes: Dict[str, connections.Item] = {}

    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in conf_inst:
            continue
        name = inst['targetname']
        try:
            # Remove the item - it's no longer going to exist after
            # we're done.
            nodes[name] = connections.ITEMS.pop(name)
        except KeyError:
            raise ValueError('No item for "{}"?'.format(name)) from None

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

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

    groups = []  # type: List[Group]

    # Node -> is grouped already.
    node_pairing = dict.fromkeys(nodes.values(), False)

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

            for conn in list(node.outputs):
                neighbour = conn.to_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, a target of our logic.
                    conn.from_item = group.item
                    has_output = True
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

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

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

            # Do the same for inputs, so we can catch that.
            for conn in list(node.inputs):
                neighbour = conn.from_item
                todo.discard(neighbour)
                pair_state = node_pairing.get(neighbour, None)
                if pair_state is None:
                    # Not a node, an input to the group.
                    conn.to_item = group.item
                    continue
                elif pair_state is False:
                    # Another node.
                    group.nodes.append(neighbour)
                # else: True, node already added.

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

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

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

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

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

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

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

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

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

            sprite_pos = conf_glow_height.copy()
            sprite_pos.localise(
                Vec.from_str(node.inst['origin']),
                Vec.from_str(node.inst['angles']),
            )

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

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

            if beam_conf:
                # Now the beam going from below up to the sprite.
                beam_pos = conf_las_start.copy()
                beam_pos.localise(
                    Vec.from_str(node.inst['origin']),
                    Vec.from_str(node.inst['angles']),
                )
                beam = vmf.create_ent('env_beam')
                for prop in beam_conf:
                    beam[prop.name] = conditions.resolve_value(
                        node.inst, prop.value)

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

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

        # We have a couple different situations to deal with here.
        # Either end could Not exist, be Unlinked, or be Linked = 8 combos.
        # Always flip so we do A to B.
        # AB |
        # NN | Make 2 new ones, one is an endpoint.
        # NU | Flip, do UN.
        # NL | Make A, link A to B. Both are linked.
        # UN | Make B, link A to B. B is unlinked.
        # UU | Link A to B, A is now linked, B is unlinked.
        # UL | Link A to B. Both are linked.
        # LN | Flip, do NL.
        # LU | Flip, do UL
        # LL | Make A, link A to B. Both are linked.
        if cable_conf:
            rope_ind = 0  # Uniqueness value.
            for node_a, node_b in group.links:
                state_a, ent_a = RopeState.from_node(cable_points, node_a)
                state_b, ent_b = RopeState.from_node(cable_points, node_b)

                if (state_a is RopeState.LINKED
                        or (state_a is RopeState.NONE
                            and state_b is RopeState.UNLINKED)):
                    # Flip these, handle the opposite order.
                    state_a, state_b = state_b, state_a
                    ent_a, ent_b = ent_b, ent_a
                    node_a, node_b = node_b, node_a

                pos_a = conf_rope_off.copy()
                pos_a.localise(
                    Vec.from_str(node_a.inst['origin']),
                    Vec.from_str(node_a.inst['angles']),
                )

                pos_b = conf_rope_off.copy()
                pos_b.localise(
                    Vec.from_str(node_b.inst['origin']),
                    Vec.from_str(node_b.inst['angles']),
                )

                # Need to make the A rope if we don't have one that's unlinked.
                if state_a is not RopeState.UNLINKED:
                    rope_a = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_a[prop.name] = conditions.resolve_value(
                            node_a.inst, prop.value)
                    rope_a['origin'] = pos_a
                    rope_ind += 1
                    rope_a['targetname'] = NAME_CABLE(base_name, rope_ind)
                else:
                    # It is unlinked, so it's the rope to use.
                    rope_a = ent_a

                # Only need to make the B rope if it doesn't have one.
                if state_b is RopeState.NONE:
                    rope_b = vmf.create_ent('move_rope')
                    for prop in beam_conf:
                        rope_b[prop.name] = conditions.resolve_value(
                            node_b.inst, prop.value)
                    rope_b['origin'] = pos_b
                    rope_ind += 1
                    name_b = rope_b['targetname'] = NAME_CABLE(
                        base_name, rope_ind)

                    cable_points[node_b] = rope_b  # Someone can use this.
                elif state_b is RopeState.UNLINKED:
                    # Both must be unlinked, we aren't using this link though.
                    name_b = ent_b['targetname']
                else:  # Linked, we just have the name.
                    name_b = ent_b

                # By here, rope_a should be an unlinked rope,
                # and name_b should be a name to link to.
                rope_a['nextkey'] = name_b

                # Figure out how much slack to give.
                # If on floor, we need to be taut to have clearance.

                if on_floor(node_a) or on_floor(node_b):
                    rope_a['slack'] = 60
                else:
                    rope_a['slack'] = 125

                # We're always linking A to B, so A is always linked!
                if state_a is not RopeState.LINKED:
                    cable_points[node_a] = rope_a['targetname']

    return conditions.RES_EXHAUSTED
Beispiel #9
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] = ''
Beispiel #10
0
def res_antlaser(vmf: VMF, res: Property) -> object:
    """The condition to generate AntLasers and Antline Corners.

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

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

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

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

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

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

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

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

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

    groups: list[Group] = []

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return conditions.RES_EXHAUSTED
Beispiel #11
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()

    inst = None
    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()
    del inst  # Make sure we don't use this later.

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

    item_id = res['markerItem']

    # Synthesise the connection config used for the final trigger.
    conn_conf_sp = connections.Config(
        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 = conn_conf_coop = None
        coop_only_once = False
    else:
        coop_only_once = res.bool('coopOnce')
        conn_conf_coop = connections.Config(
            id=item_id + ':TRIGGER',
            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 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=options.get(Vec, "global_ents_loc"),
            angles='0 0 0',
        )
        trig_ent.solids = [
            vmf.make_prism(
                bbox_min,
                bbox_max,
                mat=consts.Tools.TRIGGER,
            ).solid,
        ]

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, inst1, 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(inst1, 'man'),
                origin=origin,
            )

            item = connections.Item(
                out_ent,
                conn_conf_coop,
                ant_floor_style=mark1.ant_floor_style,
                ant_wall_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,
                conn_conf_sp,
                ant_floor_style=mark1.ant_floor_style,
                ant_wall_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