Пример #1
0
def flag_has_inst(flag: Property):
    """Checks if the given instance is present anywhere in the map."""
    flags = instanceLocs.resolve(flag.value)
    return any(
        inst.casefold() in flags
        for inst in
        ALL_INST
    )
Пример #2
0
def res_unst_scaffold_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

    if group not in SCAFFOLD_CONFIGS:
        # Store our values in the CONFIGS dictionary
        targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {}
    else:
        # Grab the already-filled values, and add to them
        targ_inst, links = SCAFFOLD_CONFIGS[group]

    for block in res.find_all("Instance"):
        conf = {
            # If set, adjusts the offset appropriately
            'is_piston': srctools.conv_bool(block['isPiston', '0']),
            'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True),
            'off_floor': Vec.from_str(block['FloorOff', '0 0 0']),
            'off_wall': Vec.from_str(block['WallOff', '0 0 0']),

            'logic_start': block['startlogic', ''],
            'logic_end': block['endLogic', ''],
            'logic_mid': block['midLogic', ''],

            'logic_start_rev': block['StartLogicRev', None],
            'logic_end_rev': block['EndLogicRev', None],
            'logic_mid_rev': block['EndLogicRev', None],

            'inst_wall': block['wallInst', ''],
            'inst_floor': block['floorInst', ''],
            'inst_offset': block['offsetInst', None],
            # Specially rotated to face the next track!
            'inst_end': block['endInst', None],
        }
        for logic_type in ('logic_start', 'logic_mid', 'logic_end'):
            if conf[logic_type + '_rev'] is None:
                conf[logic_type + '_rev'] = conf[logic_type]

        for inst in instanceLocs.resolve(block['file']):
            targ_inst[inst] = conf

    # We need to provide vars to link the tracks and beams.
    for block in res.find_all('LinkEnt'):
        # The name for this set of entities.
        # It must be a '@' name, or the name will be fixed-up incorrectly!
        loc_name = block['name']
        if not loc_name.startswith('@'):
            loc_name = '@' + loc_name
        links[block['nameVar']] = {
            'name': loc_name,
            # The next entity (not set in end logic)
            'next': block['nextVar'],
            # A '*' name to reference all the ents (set on the start logic)
            'all': block['allVar', None],
        }

    return group  # We look up the group name to find the values.
Пример #3
0
def res_vactube_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

    if group not in VAC_CONFIGS:
        # Store our values in the CONFIGS dictionary
        config, inst_configs = VAC_CONFIGS[group] = {}, {}
    else:
        # Grab the already-filled values, and add to them
        config, inst_configs = VAC_CONFIGS[group]

    for block in res.find_all("Instance"):
        # Configuration info for each instance set..
        conf = {
            # The three sizes of corner instance
            ('corner', 1): block['corner_small_inst', ''],
            ('corner', 2): block['corner_medium_inst', ''],
            ('corner', 3): block['corner_large_inst', ''],

            ('corner_temp', 1): block['temp_corner_small', ''],
            ('corner_temp', 2): block['temp_corner_medium', ''],
            ('corner_temp', 3): block['temp_corner_large', ''],

            # Straight instances connected to the next part
            'straight': block['straight_inst', ''],

            # Supports attach to the 4 sides of the straight part,
            # if there's a brush there.
            'support': block['support_inst', ''],

            'is_tsection': srctools.conv_bool(block['is_tsection', '0']),

            ('entry', 'wall'): block['entry_inst'],
            ('entry', 'floor'): block['entry_floor_inst'],
            ('entry', 'ceiling'): block['entry_ceil_inst'],

            'exit': block['exit_inst'],
        }

        for prop in block.find_all("File"):
            try:
                size, file = prop.value.split(":", 1)
            except ValueError:
                size = 1
                file = prop.value

            for inst in instanceLocs.resolve(file):
                inst_configs[inst] = conf, srctools.conv_int(size, 1)

    return group
Пример #4
0
def mon_remove_bullseyes(inst: Entity) -> Optional[object]:
    """Remove bullsyes used for cameras."""
    if not BULLSYE_LOCS:
        return RES_EXHAUSTED

    if inst['file'].casefold() not in instanceLocs.resolve('<ITEM_CATAPULT_TARGET>'):
        return

    origin = Vec(0, 0, -64)
    origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles']))
    origin = origin.as_tuple()

    LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin])

    if BULLSYE_LOCS[origin]:
        BULLSYE_LOCS[origin] -= 1
        inst.remove()
Пример #5
0
    def parse(cls, conf: Property):
        """Read in a fizzler from a config."""
        fizz_id = conf['id']
        item_ids = [
            prop.value.casefold()
            for prop in
            conf.find_all('item_id')
        ]

        try:
            model_name_type = ModelName(conf['NameType', 'same'].casefold())
        except ValueError:
            LOGGER.warning('Bad model name type: "{}"', conf['NameType'])
            model_name_type = ModelName.SAME

        model_local_name = conf['ModelName', '']
        if not model_local_name:
            # We can't rename without a local name.
            model_name_type = ModelName.SAME

        inst = {}
        for inst_type, is_static in itertools.product(FizzInst, (False, True)):
            inst_type_name = inst_type.value + ('_static' if is_static else '')
            inst[inst_type, is_static] = instances = [
                file
                for prop in conf.find_all(inst_type_name)
                for file in instanceLocs.resolve(prop.value)
            ]
            # Allow specifying weights to bias model locations
            weights = conf[inst_type_name + '_weight', '']
            if weights:
                # Produce the weights, then process through the original
                # list to build a new one with repeated elements.
                inst[inst_type, is_static] = instances = [
                    instances[i]
                    for i in conditions.weighted_random(len(instances), weights)
                ]
            # If static versions aren't given, reuse non-static ones.
            # We do False, True so it's already been calculated.
            if not instances and is_static:
                inst[inst_type, True] = inst[inst_type, False]

        if not inst[FizzInst.BASE, False]:
            LOGGER.warning('No base instance set! for "{}"!', fizz_id)

        voice_attrs = []
        for prop in conf.find_all('Has'):
            if prop.has_children():
                for child in prop:
                    voice_attrs.append(child.name.casefold())
            else:
                voice_attrs.append(prop.value.casefold())

        pack_lists = {
            prop.value
            for prop in
            conf.find_all('Pack')
        }
        pack_lists_static = {
            prop.value
            for prop in
            conf.find_all('PackStatic')
        }

        brushes = [
            FizzlerBrush.parse(prop)
            for prop in
            conf.find_all('Brush')
        ]

        beams = []  # type: List[FizzBeam]
        for beam_prop in conf.find_all('Beam'):
            offsets = [
                Vec.from_str(off.value)
                for off in
                beam_prop.find_all('pos')
            ]
            keys = Property('', [
                beam_prop.find_key('Keys', []),
                beam_prop.find_key('LocalKeys', [])
            ])
            beams.append(FizzBeam(
                offsets,
                keys,
                beam_prop.int('RandSpeedMin', 0),
                beam_prop.int('RandSpeedMax', 0),
            ))

        try:
            temp_conf = conf.find_key('TemplateBrush')
        except NoKeyError:
            temp_brush_keys = temp_min = temp_max = temp_single = None
        else:
            temp_brush_keys = Property('--', [
                temp_conf.find_key('Keys'),
                temp_conf.find_key('LocalKeys', []),
            ])

            # Find and load the templates.
            temp_min = temp_conf['Left', None]
            temp_max = temp_conf['Right', None]
            temp_single = temp_conf['Single', None]

        return FizzlerType(
            fizz_id,
            item_ids,
            voice_attrs,
            pack_lists,
            pack_lists_static,
            model_local_name,
            model_name_type,
            brushes,
            beams,
            inst,
            temp_brush_keys,
            temp_min,
            temp_max,
            temp_single,
        )
Пример #6
0
def flag_file_equal(inst: Entity, flag: Property):
    """Evaluates True if the instance matches the given file."""
    return inst['file'].casefold() in instanceLocs.resolve(flag.value)
Пример #7
0
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool], pack_list: Set[str]):
    """Analyse fizzler instances to assign fizzler types.

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

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

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

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

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

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

        name = inst['targetname']

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

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

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

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

        base_inst.outputs.clear()

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

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

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

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

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

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

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

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

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

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

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

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

    for inst in vmf.by_class['func_instance']:
        filename = inst['file'].casefold()

        if filename not in relay_file:
            continue

        inst.remove()

        try:
            fizz_name = fizz_pos[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()]
        except KeyError:
            # Not placed on a fizzler...
            continue
        fizz = FIZZLERS[fizz_name]

        # Copy over fixup values
        fizz.base_inst.fixup.update(inst.fixup)

        for out in inst.outputs:
            new_out = out.copy()
            if out.output == 'ON' and fizz.fizz_type.out_activate is not None:
                new_out.inst_out, new_out.output = fizz.fizz_type.out_activate
            elif out.output == 'OFF' and fizz.fizz_type.out_deactivate is not None:
                new_out.inst_out, new_out.output = fizz.fizz_type.out_deactivate
            else:
                # Not the marker's output somehow?
                continue
            fizz.base_inst.add_out(new_out)
Пример #8
0
def res_track_plat(vmf: VMF, res: Property):
    """Logic specific to Track Platforms.

    This allows switching the instances used depending on if the track
    is horizontal or vertical and sets the track
    targetnames to a useful value. This should be run unconditionally, not
    once per item.
    Values:
    
    * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets.
      This is used to determine all the instance filenames.
    * `single_plat`: An instance used for the entire platform, if it's
      one rail long (and therefore can't move).
    * `track_name`: If set, rename track instances following the pattern
      `plat_name-track_nameXX`. Otherwise all tracks will receive the name
      of the platform.
    * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix
      to the platform.
    * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the
      provided $fixup variable.
    * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup
      variable to indicate the relative direction the top faces.
    """
    # Get the instances from editoritems
    (inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat,
     inst_plat_oscil, inst_single) = instanceLocs.resolve(res['orig_item'])
    single_plat_inst = instanceLocs.resolve_one(res['single_plat', ''])
    track_targets = res['track_name', '']

    track_files = [inst_bottom, inst_middle, inst_top, inst_single]
    platforms = [inst_plat, inst_plat_oscil]

    # All the track_set in the map, indexed by origin
    track_instances = {
        Vec.from_str(inst['origin']).as_tuple(): inst
        for inst in vmf.by_class['func_instance']
        if inst['file'].casefold() in track_files
    }

    LOGGER.debug('Track instances:')
    LOGGER.debug('\n'.join('{!s}: {}'.format(k, v['file'])
                           for k, v in track_instances.items()))

    if not track_instances:
        return RES_EXHAUSTED

    # Now we loop through all platforms in the map, and then locate their
    # track_set
    for plat_inst in vmf.by_class['func_instance']:
        if plat_inst['file'].casefold() not in platforms:
            continue  # Not a platform!

        LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!')

        plat_loc = Vec.from_str(plat_inst['origin'])
        # The direction away from the wall/floor/ceil
        normal = Vec(0, 0, 1).rotate_by_str(plat_inst['angles'])

        for tr_origin, first_track in track_instances.items():
            if plat_loc == tr_origin:
                # Check direction

                if normal == Vec(
                        0, 0, 1).rotate(*Vec.from_str(first_track['angles'])):
                    break
        else:
            raise Exception('Platform "{}" has no track!'.format(
                plat_inst['targetname']))

        track_type = first_track['file'].casefold()
        if track_type == inst_single:
            # Track is one block long, use a single-only instance and
            # remove track!
            plat_inst['file'] = single_plat_inst
            first_track.remove()
            continue  # Next platform

        track_set = set()  # type: Set[Entity]
        if track_type == inst_top or track_type == inst_middle:
            # search left
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=-1,
            )
        if track_type == inst_bottom or track_type == inst_middle:
            # search right
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=+1,
            )

        # Give every track a targetname matching the platform
        for ind, track in enumerate(track_set, start=1):
            if track_targets == '':
                track['targetname'] = plat_inst['targetname']
            else:
                track['targetname'] = (plat_inst['targetname'] + '-' +
                                       track_targets + str(ind))

        # Now figure out which way the track faces:

        # The direction of the platform surface
        facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles'])

        # The direction horizontal track is offset
        uaxis = Vec(x=1).rotate_by_str(first_track['angles'])
        vaxis = Vec(y=1).rotate_by_str(first_track['angles'])

        if uaxis == facing:
            plat_facing = 'vert'
            track_facing = 'E'
        elif uaxis == -facing:
            plat_facing = 'vert'
            track_facing = 'W'
        elif vaxis == facing:
            plat_facing = 'horiz'
            track_facing = 'N'
        elif vaxis == -facing:
            plat_facing = 'horiz'
            track_facing = 'S'
        else:
            raise ValueError('Facing {} is not U({}) or V({})!'.format(
                facing,
                uaxis,
                vaxis,
            ))

        if res.bool('plat_suffix'):
            conditions.add_suffix(plat_inst, '_' + plat_facing)

        plat_var = res['plat_var', '']
        if plat_var:
            plat_inst.fixup[plat_var] = plat_facing

        track_var = res['track_var', '']
        if track_var:
            plat_inst.fixup[track_var] = track_facing

        for track in track_set:
            track.fixup.update(plat_inst.fixup)

    return RES_EXHAUSTED  # Don't re-run
Пример #9
0
def fizzler_out_relay():
    """Link fizzlers with a relay item so they can be given outputs."""
    relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>')
    if not relay_file:
        # No relay item - deactivated most likely.
        return

    # instances
    fizz_models = set()
    # base -> connections
    fizz_bases = {}

    LOGGER.info('Item classes: {}', ITEM_CLASSES)

    for fizz_id in ITEM_CLASSES['itembarrierhazard']:
        base, model = instanceLocs.resolve(
            '<{}: fizz_base, fizz_model>'.format(fizz_id)
        )
        fizz_bases[base.casefold()] = CONNECTIONS[fizz_id]
        fizz_models.add(model.casefold())

    # targetname -> base inst, connections
    fizz_by_name = {}

    # origin, normal -> targetname
    pos_to_name = {}

    marker_inst = []  # type: List[Entity]

    LOGGER.info('Fizzler data: {}', locals())

    for inst in vbsp.VMF.by_class['func_instance']:
        filename = inst['file'].casefold()
        name = inst['targetname']
        if filename in fizz_bases:
            fizz_by_name[inst['targetname']] = inst, fizz_bases[filename]
        elif filename in fizz_models:
            if inst['targetname'].endswith(('_modelStart', '_modelEnd')):
                name = inst['targetname'].rsplit('_', 1)[0]

        elif filename in relay_file:
            marker_inst.append(inst)
            # Remove the marker, we don't need that...
            inst.remove()
            continue
        else:
            continue

        pos_to_name[
            Vec.from_str(inst['origin']).as_tuple(),
            Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()
        ] = name

    for inst in marker_inst:
        try:
            fizz_name = pos_to_name[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()
            ]
        except KeyError:
            # Not placed on a fizzler...
            continue
        base_inst, connections = fizz_by_name[fizz_name]

        # Copy over fixup values
        for key, val in inst.fixup.items():
            base_inst.fixup[key] = val

        for out in inst.outputs:
            new_out = out.copy()
            if out.output == 'ON':
                new_out.inst_out, new_out.output = connections.out_act
            elif out.output == 'OFF':
                new_out.inst_out, new_out.output = connections.out_deact
            else:
                # Not the marker's output somehow?
                continue
            base_inst.add_out(new_out)
Пример #10
0
def res_track_plat(vmf: VMF, res: Property):
    """Logic specific to Track Platforms.

    This allows switching the instances used depending on if the track
    is horizontal or vertical and sets the track
    targetnames to a useful value. This should be run unconditionally, not
    once per item.
    Values:
    
    * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets.
      This is used to determine all the instance filenames.
    * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). 
    * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`.
      Otherwise all tracks will receive the name of the platform.
    * `vert_suffix`: If set, add `_vert` suffixes to vertical track instance names.
    * `horiz_suffix`: Add suffixes to horizontal tracks
            (_horiz, _horiz_mirrored)
    * `plat_suffix`: If set, also add the above `_vert` or `_horiz` suffixes 
      to the platform.
    * `vert_bottom_suffix`: If set, add '_bottom' / '_vert_bottom' to the track at the
            bottom of vertical platforms.
    * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable.
    """
    # Get the instances from editoritems
    (inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat,
     inst_plat_oscil, inst_single) = instanceLocs.resolve(res['orig_item'])
    single_plat_inst = instanceLocs.resolve_one(res['single_plat', ''])
    track_targets = res['track_name', '']

    track_files = [inst_bottom, inst_middle, inst_top, inst_single]
    platforms = [inst_plat, inst_plat_oscil]

    # All the track_set in the map, indexed by origin
    track_instances = {
        Vec.from_str(inst['origin']).as_tuple(): inst
        for inst in vbsp.VMF.by_class['func_instance']
        if inst['file'].casefold() in track_files
    }

    LOGGER.debug('Track instances:')
    LOGGER.debug('\n'.join('{!s}: {}'.format(k, v['file'])
                           for k, v in track_instances.items()))

    if not track_instances:
        return RES_EXHAUSTED

    # Now we loop through all platforms in the map, and then locate their
    # track_set
    for plat_inst in vmf.by_class['func_instance']:
        if plat_inst['file'].casefold() not in platforms:
            continue  # Not a platform!

        LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!')

        plat_loc = Vec.from_str(plat_inst['origin'])
        # The direction away from the wall/floor/ceil
        normal = Vec(0, 0, 1).rotate_by_str(plat_inst['angles'])

        for tr_origin, first_track in track_instances.items():
            if plat_loc == tr_origin:
                # Check direction

                if normal == Vec(
                        0, 0, 1).rotate(*Vec.from_str(first_track['angles'])):
                    break
        else:
            raise Exception('Platform "{}" has no track!'.format(
                plat_inst['targetname']))

        track_type = first_track['file'].casefold()
        if track_type == inst_single:
            # Track is one block long, use a single-only instance and
            # remove track!
            plat_inst['file'] = single_plat_inst
            first_track.remove()
            continue  # Next platform

        track_set = set()
        if track_type == inst_top or track_type == inst_middle:
            # search left
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=-1,
            )
        if track_type == inst_bottom or track_type == inst_middle:
            # search right
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=+1,
            )

        # Give every track a targetname matching the platform
        for ind, track in enumerate(track_set, start=1):
            if track_targets == '':
                track['targetname'] = plat_inst['targetname']
            else:
                track['targetname'] = (plat_inst['targetname'] + '-' +
                                       track_targets + str(ind))

        # Now figure out which way the track faces:

        # The direction horizontal track is offset
        side_dir = Vec(0, 1, 0).rotate_by_str(first_track['angles'])

        # The direction of the platform surface
        facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles'])
        if side_dir == facing:
            track_facing = 'HORIZ'
        elif side_dir == -facing:
            track_facing = 'HORIZ_MIRR'
        else:
            track_facing = 'VERT'
        # Now add the suffixes
        if track_facing == 'VERT':
            if srctools.conv_bool(res['vert_suffix', '']):
                for inst in track_set:
                    conditions.add_suffix(inst, '_vert')
                if srctools.conv_bool(res['plat_suffix', '']):
                    conditions.add_suffix(plat_inst, '_vert')
            if srctools.conv_bool(res['vert_bottom_suffix', '']):
                # We want to find the bottom/top track which is facing the
                # same direction as the platform.
                track_dirs = {
                    inst_top: Vec(-1, 0, 0),
                    inst_bottom: Vec(1, 0, 0)
                }
                for inst in track_set:
                    try:
                        norm_off = track_dirs[inst['file'].casefold()]
                    except KeyError:
                        continue

                    if norm_off.rotate_by_str(inst['angles']) == facing:
                        conditions.add_suffix(inst, '_bottom')

        elif track_facing == 'HORIZ_MIRR':
            if srctools.conv_bool(res['horiz_suffix', '']):
                for inst in track_set:
                    conditions.add_suffix(inst, '_horiz_mirrored')
                if srctools.conv_bool(res['plat_suffix', '']):
                    conditions.add_suffix(plat_inst, '_horiz')
        else:  # == 'HORIZ'
            if srctools.conv_bool(res['horiz_suffix', '']):
                for inst in track_set:
                    conditions.add_suffix(inst, '_horiz')
                if srctools.conv_bool(res['plat_suffix', '']):
                    conditions.add_suffix(plat_inst, '_horiz')

        plat_var = res['plat_var', '']
        if plat_var != '':
            # Skip the '_mirrored' section if needed
            plat_inst.fixup[plat_var] = track_facing[:5].lower()

    return RES_EXHAUSTED  # Don't re-run
Пример #11
0
def res_cutout_tile(vmf: srctools.VMF, res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - `MarkerItem` is the instance file to look for (`<ITEM_BEE2_CUTOUT_TILE>`)
    - `floor_chance`: The percentage change for a segment in the middle of the floor to be a normal tile.
    - `floor_glue_chance`: The chance for any tile to be glue - this should be higher than the regular chance, as that overrides this.
    - `rotateMax` is the maximum angle to rotate squarebeam models.
    - `squarebeamsSkin` sets the skin to use for the squarebeams floor frame.
    - `dispBase`, if true makes the floor a displacement with random alpha.
    - `Materials` blocks specify the possible materials to use:
          - `squarebeams` is the squarebeams variant to use.
          - `ceilingwalls` are the sides of the ceiling section.
          - `floorbase` is the texture under floor sections.
            If `dispBase` is True this is a displacement material.
          - `tile_glue` is used on top of a thinner tile segment.
          - `clip` is the player_clip texture used over floor segments.
            (This allows customising the surfaceprop.)

    """
    marker_filenames = instanceLocs.resolve(res['markeritem'])

    x: float
    y: float
    max_x: float
    max_y: float

    INST_LOCS = {}  # Map targetnames -> surface loc
    CEIL_IO = []  # Pairs of ceil inst corners to cut out.
    FLOOR_IO = []  # Pairs of floor inst corners to cut out.

    overlay_ids = {}  # When we replace brushes, we need to fix any overlays
    # on that surface.

    MATS.clear()
    floor_edges = []  # Values to pass to add_floor_sides() at the end

    sign_locs = set()
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in vmf.by_class['info_overlay']:
        if (over['material'].casefold() in FORCE_TILE_MATS and
                # Only check floor/ceiling overlays
                over['basisnormal'] in ('0 0 1', '0 0 -1')):
            add_signage_loc(sign_locs, Vec.from_str(over['origin']))

    for item in connections.ITEMS.values():
        for ind_pan in item.ind_panels:
            loc = Vec(0, 0, -64)
            loc.localise(
                Vec.from_str(ind_pan['origin']),
                Vec.from_str(ind_pan['angles']),
            )
            add_signage_loc(sign_locs, loc)

    SETTINGS = {
        'floor_chance':
        srctools.conv_int(res['floorChance', '100'], 100),
        'ceil_chance':
        srctools.conv_int(res['ceilingChance', '100'], 100),
        'floor_glue_chance':
        srctools.conv_int(res['floorGlueChance', '0']),
        'ceil_glue_chance':
        srctools.conv_int(res['ceilingGlueChance', '0']),
        'rotate_beams':
        int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION),
        'beam_skin':
        res['squarebeamsSkin', '0'],
        'base_is_disp':
        srctools.conv_bool(res['dispBase', '0']),
        'quad_floor':
        res['FloorSize', '4x4'].casefold() == '2x2',
        'quad_ceil':
        res['CeilingSize', '4x4'].casefold() == '2x2',
    }

    random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE')
    noise = SimplexNoise(period=4 * 40)  # 4 tiles/block, 50 blocks max

    # We want to know the number of neighbouring tile cutouts before
    # placing tiles - blocks away from the sides generate fewer tiles.
    # all_floors[z][x,y] = count
    floor_neighbours = defaultdict(
        dict)  # type: Dict[float, Dict[Tuple[float, float], int]]

    for mat_prop in res.find_key('Materials', []):
        MATS[mat_prop.name].append(mat_prop.value)

    if SETTINGS['base_is_disp']:
        # We want the normal brushes to become nodraw.
        MATS['floorbase_disp'] = MATS['floorbase']
        MATS['floorbase'] = ['tools/toolsnodraw']

        # Since this uses random data for initialisation, the alpha and
        # regular will use slightly different patterns.
        alpha_noise = SimplexNoise(period=4 * 50)
    else:
        alpha_noise = None

    for key, default in TEX_DEFAULT:
        if key not in MATS:
            MATS[key] = [default]

    # Find our marker ents
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker_filenames:
            continue
        targ = inst['targetname']
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0'])
        # Check the orientation of the marker to figure out what to generate
        if normal == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

        # Reuse orient to calculate where the solid face will be.
        loc = Vec.from_str(inst['origin']) - 64 * normal
        INST_LOCS[targ] = loc

        item = connections.ITEMS[targ]
        item.delete_antlines()

        if item.outputs:
            for conn in list(item.outputs):
                if conn.to_item.inst['file'].casefold() in marker_filenames:
                    io_list.append((targ, conn.to_item.name))
                else:
                    LOGGER.warning('Cutout tile connected to non-cutout!')
                conn.remove()  # Delete the connection.
        else:
            # If the item doesn't have any connections, 'connect'
            # it to itself so we'll generate a 128x128 tile segment.
            io_list.append((targ, targ))

        # Remove all traces of this item (other than in connections lists).
        inst.remove()
        del connections.ITEMS[targ]

    return  # TODO: Reimplement cutout tiles.

    for start_floor, end_floor in FLOOR_IO:
        box_min = Vec(INST_LOCS[start_floor])
        box_min.min(INST_LOCS[end_floor])

        box_max = Vec(INST_LOCS[start_floor])
        box_max.max(INST_LOCS[end_floor])

        if box_min.z != box_max.z:
            continue  # They're not in the same level!
        z = box_min.z

        if SETTINGS['rotate_beams']:
            # We have to generate 1 model per 64x64 block to do rotation...
            gen_rotated_squarebeams(
                vmf,
                box_min - (64, 64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin'],
                max_rot=SETTINGS['rotate_beams'],
            )
        else:
            # Make the squarebeams props, using big models if possible
            gen_squarebeams(vmf,
                            box_min + (-64, -64, 0),
                            box_max + (64, 64, -8),
                            skin=SETTINGS['beam_skin'])

        # Add a player_clip brush across the whole area
        vmf.add_brush(
            vmf.make_prism(
                p1=box_min - (64, 64, FLOOR_DEPTH),
                p2=box_max + (64, 64, 0),
                mat=MATS['clip'][0],
            ).solid)

        # Add a noportal_volume covering the surface, in case there's
        # room for a portal.
        noportal_solid = vmf.make_prism(
            # Don't go all the way to the sides, so it doesn't affect wall
            # brushes.
            p1=box_min - (63, 63, 9),
            p2=box_max + (63, 63, 0),
            mat='tools/toolsinvisible',
        ).solid
        noportal_ent = vmf.create_ent(
            classname='func_noportal_volume',
            origin=box_min.join(' '),
        )
        noportal_ent.solids.append(noportal_solid)

        if SETTINGS['base_is_disp']:
            # Use displacements for the base instead.
            make_alpha_base(
                vmf,
                box_min + (-64, -64, 0),
                box_max + (64, 64, 0),
                noise=alpha_noise,
            )

        for x, y in utils.iter_grid(
                min_x=int(box_min.x),
                max_x=int(box_max.x) + 1,
                min_y=int(box_min.y),
                max_y=int(box_max.y) + 1,
                stride=128,
        ):
            # Build the set of all positions..
            floor_neighbours[z][x, y] = -1

        # Mark borders we need to fill in, and the angle (for func_instance)
        # The wall is the face pointing inwards towards the bottom brush,
        # and the ceil is the ceiling of the block above the bordering grid
        # points.
        for x in range(int(box_min.x), int(box_max.x) + 1, 128):
            # North
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_max.y + 64, z - 64),
                    ceil=Vec_tuple(x, box_max.y + 128, z),
                    rot=270,
                ))
            # South
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_min.y - 64, z - 64),
                    ceil=Vec_tuple(x, box_min.y - 128, z),
                    rot=90,
                ))

        for y in range(int(box_min.y), int(box_max.y) + 1, 128):
            # East
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_max.x + 64, y, z - 64),
                    ceil=Vec_tuple(box_max.x + 128, y, z),
                    rot=180,
                ))

            # West
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_min.x - 64, y, z - 64),
                    ceil=Vec_tuple(box_min.x - 128, y, z),
                    rot=0,
                ))

    # Now count boundaries near tiles, then generate them.

    # Do it separately for each z-level:
    for z, xy_dict in floor_neighbours.items():
        for x, y in xy_dict:
            # We want to count where there aren't any tiles
            xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) +
                             ((x - 128, y + 128) not in xy_dict) +
                             ((x + 128, y - 128) not in xy_dict) +
                             ((x + 128, y + 128) not in xy_dict) +
                             ((x - 128, y) not in xy_dict) +
                             ((x + 128, y) not in xy_dict) +
                             ((x, y - 128) not in xy_dict) +
                             ((x, y + 128) not in xy_dict))

        max_x = max_y = 0

        weights = {}
        # Now the counts are all correct, compute the weight to apply
        # for tiles.
        # Adding the neighbouring counts will make a 5x5 area needed to set
        # the center to 0.

        for (x, y), cur_count in xy_dict.items():
            max_x = max(x, max_x)
            max_y = max(y, max_y)

            # Orthrogonal is worth 0.2, diagonal is worth 0.1.
            # Not-present tiles would be 8 - the maximum
            tile_count = (0.8 * cur_count + 0.1 * xy_dict.get(
                (x - 128, y - 128), 8) + 0.1 * xy_dict.get(
                    (x - 128, y + 128), 8) + 0.1 * xy_dict.get(
                        (x + 128, y - 128), 8) + 0.1 * xy_dict.get(
                            (x + 128, y + 128), 8) + 0.2 * xy_dict.get(
                                (x - 128, y), 8) + 0.2 * xy_dict.get(
                                    (x, y - 128), 8) + 0.2 * xy_dict.get(
                                        (x, y + 128), 8) + 0.2 * xy_dict.get(
                                            (x + 128, y), 8))
            # The number ranges from 0 (all tiles) to 12.8 (no tiles).
            # All tiles should still have a small chance to generate tiles.
            weights[x, y] = min((tile_count + 0.5) / 8, 1)

        # Share the detail entity among same-height tiles..
        detail_ent = vmf.create_ent(classname='func_detail', )

        for x, y in xy_dict:
            convert_floor(
                vmf,
                Vec(x, y, z),
                overlay_ids,
                MATS,
                SETTINGS,
                sign_locs,
                detail_ent,
                noise_weight=weights[x, y],
                noise_func=noise,
            )

    add_floor_sides(vmf, floor_edges)

    return conditions.RES_EXHAUSTED
Пример #12
0
def res_cust_output(inst: Entity, res: Property):
    """Add an additional output to the instance with any values.

    Always points to the targeted item.

    If DecConCount is 1, connections
    """
    (
        outputs,
        dec_con_count,
        targ_conditions,
        force_sign_type,
        (sign_act_name, sign_act_out),
        (sign_deact_name, sign_deact_out),
    ) = res.value

    over_name = '@' + inst['targetname'] + '_indicator'
    for toggle in vbsp.VMF.by_class['func_instance']:
        if toggle.fixup['indicator_name', ''] == over_name:
            toggle_name = toggle['targetname']
            break
    else:
        toggle_name = ''  # we want to ignore the toggle instance, if it exists

    # Build a mapping from names to targets.
    # This is also the set of all output items, plus indicators.
    targets = defaultdict(list)
    for out in inst.outputs:
        if out.target != toggle_name:
            targets[out.target].append(out)

    pan_files = instanceLocs.resolve('[indPan]')

    # These all require us to search through the instances.
    if force_sign_type or dec_con_count or targ_conditions:
        for con_inst in vbsp.VMF.by_class['func_instance']:  # type: Entity
            if con_inst['targetname'] not in targets:
                # Not our instance
                continue

            # Is it an indicator panel, and should we be modding it?
            if force_sign_type is not None and con_inst['file'].casefold() in pan_files:
                # Remove the panel
                if force_sign_type == '':
                    con_inst.remove()
                    continue

                # Overwrite the signage instance, and then add the
                # appropriate outputs to control it.
                sign_id, sign_file_id = force_sign_type
                con_inst['file'] = instanceLocs.resolve_one(sign_file_id, error=True)

                # First delete the original outputs:
                for out in targets[con_inst['targetname']]:
                    inst.outputs.remove(out)

                inputs = CONNECTIONS[sign_id]
                act_name, act_inp = inputs.in_act
                deact_name, deact_inp = inputs.in_deact

                LOGGER.info(
                    'outputs: a="{}" d="{}"\n'
                    'inputs: a="{}" d="{}"'.format(
                        (sign_act_name, sign_act_out),
                        (sign_deact_name, sign_deact_out),
                        inputs.in_act,
                        inputs.in_deact
                    )
                )

                if act_inp and sign_act_out:
                    inst.add_out(Output(
                        inst_out=sign_act_name,
                        out=sign_act_out,
                        inst_in=act_name,
                        inp=act_inp,
                        targ=con_inst['targetname'],
                    ))

                if deact_inp and sign_deact_out:
                    inst.add_out(Output(
                        inst_out=sign_deact_name,
                        out=sign_deact_out,
                        inst_in=deact_name,
                        inp=deact_inp,
                        targ=con_inst['targetname'],
                    ))
            if dec_con_count and 'connectioncount' in con_inst.fixup:
                # decrease ConnectionCount on the ents,
                # so they can still process normal inputs
                try:
                    val = int(con_inst.fixup['connectioncount'])
                    con_inst.fixup['connectioncount'] = str(val-1)
                except ValueError:
                    # skip if it's invalid
                    LOGGER.warning(
                        con_inst['targetname'] +
                        ' has invalid ConnectionCount!'
                    )

            if targ_conditions:
                for cond in targ_conditions:  # type: Condition
                    cond.test(con_inst)

    if outputs:
        for targ in targets:
            for out in outputs:
                conditions.add_output(inst, out, targ)
Пример #13
0
def res_cutout_tile(vmf: srctools.VMF, res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - "MarkerItem" is the instance to look for.
    - "TileSize" can be "2x2" or "4x4".
    - rotateMax is the amount of degrees to rotate squarebeam models.

    Materials:
    - "squarebeams" is the squarebeams variant to use.
    - "ceilingwalls" are the sides of the ceiling section.
    - "floorbase" is the texture under floor sections.
    - "tile_glue" is used on top of a thinner tile segment.
    - "clip" is the player_clip texture used over floor segments.
        (This allows customising the surfaceprop.)
    - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to
       override the textures used.
    """
    item = instanceLocs.resolve(res['markeritem'])

    INST_LOCS = {}  # Map targetnames -> surface loc
    CEIL_IO = []  # Pairs of ceil inst corners to cut out.
    FLOOR_IO = []  # Pairs of floor inst corners to cut out.

    overlay_ids = {}  # When we replace brushes, we need to fix any overlays
    # on that surface.

    MATS.clear()
    floor_edges = []  # Values to pass to add_floor_sides() at the end

    sign_loc = set(FORCE_LOCATIONS)
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in vmf.by_class['info_overlay']:
        if (over['material'].casefold() in FORCE_TILE_MATS and
                # Only check floor/ceiling overlays
                over['basisnormal'] in ('0 0 1', '0 0 -1')):
            loc = Vec.from_str(over['origin'])
            # Sometimes (light bridges etc) a sign will be halfway between
            # tiles, so in that case we need to force 2 tiles.
            loc_min = (loc - (15, 15, 0)) // 32 * 32  # type: Vec
            loc_max = (loc + (15, 15, 0)) // 32 * 32  # type: Vec
            loc_min += (16, 16, 0)
            loc_max += (16, 16, 0)
            FORCE_LOCATIONS.add(loc_min.as_tuple())
            FORCE_LOCATIONS.add(loc_max.as_tuple())

    SETTINGS = {
        'floor_chance':
        srctools.conv_int(res['floorChance', '100'], 100),
        'ceil_chance':
        srctools.conv_int(res['ceilingChance', '100'], 100),
        'floor_glue_chance':
        srctools.conv_int(res['floorGlueChance', '0']),
        'ceil_glue_chance':
        srctools.conv_int(res['ceilingGlueChance', '0']),
        'rotate_beams':
        int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION),
        'beam_skin':
        res['squarebeamsSkin', '0'],
        'base_is_disp':
        srctools.conv_bool(res['dispBase', '0']),
        'quad_floor':
        res['FloorSize', '4x4'].casefold() == '2x2',
        'quad_ceil':
        res['CeilingSize', '4x4'].casefold() == '2x2',
    }

    random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE')
    noise = SimplexNoise(period=4 * 40)  # 4 tiles/block, 50 blocks max

    # We want to know the number of neighbouring tile cutouts before
    # placing tiles - blocks away from the sides generate fewer tiles.
    floor_neighbours = defaultdict(dict)  # all_floors[z][x,y] = count

    for mat_prop in res.find_key('Materials', []):
        MATS[mat_prop.name].append(mat_prop.value)

    if SETTINGS['base_is_disp']:
        # We want the normal brushes to become nodraw.
        MATS['floorbase_disp'] = MATS['floorbase']
        MATS['floorbase'] = ['tools/toolsnodraw']

        # Since this uses random data for initialisation, the alpha and
        # regular will use slightly different patterns.
        alpha_noise = SimplexNoise(period=4 * 50)
    else:
        alpha_noise = None

    for key, default in TEX_DEFAULT:
        if key not in MATS:
            MATS[key] = [default]

    # Find our marker ents
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in item:
            continue
        targ = inst['targetname']
        orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0'])
        # Check the orientation of the marker to figure out what to generate
        if orient == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

        # Reuse orient to calculate where the solid face will be.
        loc = (orient * -64) + Vec.from_str(inst['origin'])
        INST_LOCS[targ] = loc

        for out in inst.output_targets():
            io_list.append((targ, out))

        if not inst.outputs and inst.fixup['$connectioncount'] == '0':
            # If the item doesn't have any connections, 'connect'
            # it to itself so we'll generate a 128x128 tile segment.
            io_list.append((targ, targ))
        inst.remove()  # Remove the instance itself from the map.

    for start_floor, end_floor in FLOOR_IO:
        if end_floor not in INST_LOCS:
            # Not a marker - remove this and the antline.
            for toggle in vmf.by_target[end_floor]:
                conditions.remove_ant_toggle(toggle)
            continue

        box_min = Vec(INST_LOCS[start_floor])
        box_min.min(INST_LOCS[end_floor])

        box_max = Vec(INST_LOCS[start_floor])
        box_max.max(INST_LOCS[end_floor])

        if box_min.z != box_max.z:
            continue  # They're not in the same level!
        z = box_min.z

        if SETTINGS['rotate_beams']:
            # We have to generate 1 model per 64x64 block to do rotation...
            gen_rotated_squarebeams(
                vmf,
                box_min - (64, 64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin'],
                max_rot=SETTINGS['rotate_beams'],
            )
        else:
            # Make the squarebeams props, using big models if possible
            gen_squarebeams(vmf,
                            box_min + (-64, -64, 0),
                            box_max + (64, 64, -8),
                            skin=SETTINGS['beam_skin'])

        # Add a player_clip brush across the whole area
        vmf.add_brush(
            vmf.make_prism(
                p1=box_min - (64, 64, FLOOR_DEPTH),
                p2=box_max + (64, 64, 0),
                mat=MATS['clip'][0],
            ).solid)

        # Add a noportal_volume covering the surface, in case there's
        # room for a portal.
        noportal_solid = vmf.make_prism(
            # Don't go all the way to the sides, so it doesn't affect wall
            # brushes.
            p1=box_min - (63, 63, 9),
            p2=box_max + (63, 63, 0),
            mat='tools/toolsinvisible',
        ).solid
        noportal_ent = vmf.create_ent(
            classname='func_noportal_volume',
            origin=box_min.join(' '),
        )
        noportal_ent.solids.append(noportal_solid)

        if SETTINGS['base_is_disp']:
            # Use displacements for the base instead.
            make_alpha_base(
                vmf,
                box_min + (-64, -64, 0),
                box_max + (64, 64, 0),
                noise=alpha_noise,
            )

        for x, y in utils.iter_grid(
                min_x=int(box_min.x),
                max_x=int(box_max.x) + 1,
                min_y=int(box_min.y),
                max_y=int(box_max.y) + 1,
                stride=128,
        ):
            # Build the set of all positions..
            floor_neighbours[z][x, y] = -1

        # Mark borders we need to fill in, and the angle (for func_instance)
        # The wall is the face pointing inwards towards the bottom brush,
        # and the ceil is the ceiling of the block above the bordering grid
        # points.
        for x in range(int(box_min.x), int(box_max.x) + 1, 128):
            # North
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_max.y + 64, z - 64),
                    ceil=Vec_tuple(x, box_max.y + 128, z),
                    rot=270,
                ))
            # South
            floor_edges.append(
                BorderPoints(
                    wall=Vec(x, box_min.y - 64, z - 64),
                    ceil=Vec_tuple(x, box_min.y - 128, z),
                    rot=90,
                ))

        for y in range(int(box_min.y), int(box_max.y) + 1, 128):
            # East
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_max.x + 64, y, z - 64),
                    ceil=Vec_tuple(box_max.x + 128, y, z),
                    rot=180,
                ))

            # West
            floor_edges.append(
                BorderPoints(
                    wall=Vec(box_min.x - 64, y, z - 64),
                    ceil=Vec_tuple(box_min.x - 128, y, z),
                    rot=0,
                ))

    # Now count boundries near tiles, then generate them.

    # Do it seperately for each z-level:
    for z, xy_dict in floor_neighbours.items():  # type: float, dict
        for x, y in xy_dict:  # type: float, float
            # We want to count where there aren't any tiles
            xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) +
                             ((x - 128, y + 128) not in xy_dict) +
                             ((x + 128, y - 128) not in xy_dict) +
                             ((x + 128, y + 128) not in xy_dict) +
                             ((x - 128, y) not in xy_dict) +
                             ((x + 128, y) not in xy_dict) +
                             ((x, y - 128) not in xy_dict) +
                             ((x, y + 128) not in xy_dict))

        max_x = max_y = 0

        weights = {}
        # Now the counts are all correct, compute the weight to apply
        # for tiles.
        # Adding the neighbouring counts will make a 5x5 area needed to set
        # the center to 0.

        for (x, y), cur_count in xy_dict.items():
            max_x = max(x, max_x)
            max_y = max(y, max_y)

            # Orthrogonal is worth 0.2, diagonal is worth 0.1.
            # Not-present tiles would be 8 - the maximum
            tile_count = (0.8 * cur_count + 0.1 * xy_dict.get(
                (x - 128, y - 128), 8) + 0.1 * xy_dict.get(
                    (x - 128, y + 128), 8) + 0.1 * xy_dict.get(
                        (x + 128, y - 128), 8) + 0.1 * xy_dict.get(
                            (x + 128, y + 128), 8) + 0.2 * xy_dict.get(
                                (x - 128, y), 8) + 0.2 * xy_dict.get(
                                    (x, y - 128), 8) + 0.2 * xy_dict.get(
                                        (x, y + 128), 8) + 0.2 * xy_dict.get(
                                            (x + 128, y), 8))
            # The number ranges from 0 (all tiles) to 12.8 (no tiles).
            # All tiles should still have a small chance to generate tiles.
            weights[x, y] = min((tile_count + 0.5) / 8, 1)

        # Share the detail entity among same-height tiles..
        detail_ent = vmf.create_ent(classname='func_detail', )

        for x, y in xy_dict:
            convert_floor(
                vmf,
                Vec(x, y, z),
                overlay_ids,
                MATS,
                SETTINGS,
                sign_loc,
                detail_ent,
                noise_weight=weights[x, y],
                noise_func=noise,
            )

    add_floor_sides(vmf, floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
Пример #14
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 = {}  # type: Dict[str, 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 = {}  # type: Dict[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 = {}  # type: Dict[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
Пример #15
0
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None:
    """Analyse fizzler instances to assign fizzler types.

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

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

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

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

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

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

        name = inst['targetname']

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

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

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

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

        base_inst.outputs.clear()

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

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

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

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

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

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

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

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

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

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

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

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

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

        inst.remove()

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

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

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

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

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

        for conn in list(relay_item.outputs):
            conn.from_item = fizz_item
Пример #16
0
def flag_file_equal(inst: Entity, flag: Property):
    """Evaluates True if the instance matches the given file."""
    return inst['file'].casefold() in instanceLocs.resolve(flag.value)
Пример #17
0
def flag_has_inst(flag: Property):
    """Checks if the given instance is present anywhere in the map."""
    flags = instanceLocs.resolve(flag.value)
    return any(inst.casefold() in flags for inst in ALL_INST)
Пример #18
0
def res_make_catwalk(res: Property):
    """Speciallised result to generate catwalks from markers.

    Only runs once, and then quits the condition list.
    Instances:
        MarkerInst: The instance set in editoritems.
        Straight_128/256/512: Straight sections. Extends East
        Corner: A corner piece. Connects on N and W sides.
        TJunction; A T-piece. Connects on all but the East side.
        CrossJunction: A X-piece. Connects on all sides.
        End: An end piece. Connects on the East side.
        Stair: A stair. Starts East and goes Up and West.
        End_wall: Connects a West wall to a East catwalk.
        Support_Wall: A support extending from the East wall.
        Support_Ceil: A support extending from the ceiling.
        Support_Floor: A support extending from the floor.
        Support_Goo: A floor support, designed for goo pits.
        Single_Wall: A section connecting to an East wall.
    """
    LOGGER.info("Starting catwalk generator...")
    marker = instanceLocs.resolve(res['markerInst'])
    output_target = res['output_name', 'MARKER']

    instances = {
        name: instanceLocs.resolve_one(res[name, ''], error=True)
        for name in
        (
            'straight_128', 'straight_256', 'straight_512',
            'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall',
            'support_wall', 'support_ceil', 'support_floor', 'support_goo',
            'single_wall',
            'markerInst',
        )
    }
    # If there are no attachments remove a catwalk piece
    instances['NONE'] = ''
    if instances['end_wall'] == '':
        instances['end_wall'] = instances['end']

    connections = {}  # The directions this instance is connected by (NSEW)
    markers = {}

    # Find all our markers, so we can look them up by targetname.
    for inst in vbsp.VMF.by_class['func_instance']:
        if inst['file'].casefold() not in marker:
            continue
        #                   [North, South, East,  West ]
        connections[inst] = [False, False, False, False]
        markers[inst['targetname']] = inst

        # Snap the markers to the grid. If on glass it can become offset...
        origin = Vec.from_str(inst['origin'])
        origin = origin // 128 * 128  # type: Vec
        origin += 64

        while origin.as_tuple() in conditions.GOO_LOCS:
            # The instance is in goo! Switch to floor orientation, and move
            # up until it's in air.
            inst['angles'] = '0 0 0'
            origin.z += 128

        inst['origin'] = str(origin)

    if not markers:
        return RES_EXHAUSTED

    LOGGER.info('Connections: {}', connections)
    LOGGER.info('Markers: {}', markers)

    # First loop through all the markers, adding connecting sections
    for inst in markers.values():
        for conn in inst.outputs:
            if conn.output != output_target or conn.input != output_target:
                # Indicator toggles or similar, delete these entities.
                # Find the associated overlays too.
                for del_inst in vbsp.VMF.by_target[conn.target]:
                    conditions.remove_ant_toggle(del_inst)
                continue

            inst2 = markers[conn.target]
            LOGGER.debug('{} <-> {}', inst['targetname'], inst2['targetname'])
            origin1 = Vec.from_str(inst['origin'])
            origin2 = Vec.from_str(inst2['origin'])
            if origin1.x != origin2.x and origin1.y != origin2.y:
                LOGGER.warning('Instances not aligned!')
                continue

            y_dir = origin1.x == origin2.x  # Which way the connection is
            if y_dir:
                dist = abs(origin1.y - origin2.y)
            else:
                dist = abs(origin1.x - origin2.x)
            vert_dist = origin1.z - origin2.z

            LOGGER.debug('Dist = {}, Vert = {}', dist, vert_dist)

            if (dist - 128) // 2 < abs(vert_dist):
                # The stairs are 2 long, 1 high. Check there's enough room
                # Subtract the last block though, since that's a corner.
                LOGGER.warning('Not enough room for stairs!')
                continue

            if dist > 128:
                # add straight sections in between
                place_catwalk_connections(instances, origin1, origin2)

            # Update the lists based on the directions that were set
            conn_lst1 = connections[inst]
            conn_lst2 = connections[inst2]
            if origin1.x < origin2.x:
                conn_lst1[2] = True  # E
                conn_lst2[3] = True  # W
            elif origin2.x < origin1.x:
                conn_lst1[3] = True  # W
                conn_lst2[2] = True  # E

            if origin1.y < origin2.y:
                conn_lst1[0] = True  # N
                conn_lst2[1] = True  # S
            elif origin2.y < origin1.y:
                conn_lst1[1] = True  # S
                conn_lst2[0] = True  # N

        inst.outputs.clear()  # Remove the outputs now, they're useless

    for inst, dir_mask in connections.items():
        # Set the marker instances based on the attached walkways.
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles'])

        new_type, inst['angles'] = utils.CONN_LOOKUP[tuple(dir_mask)]
        inst['file'] = instances[CATWALK_TYPES[new_type]]

        if new_type is utils.CONN_TYPES.side:
            # If the end piece is pointing at a wall, switch the instance.
            if normal.z == 0:
                # Treat booleans as ints to get the direction the connection is
                # in - True == 1, False == 0
                conn_dir = Vec(
                    x=dir_mask[2] - dir_mask[3],  # +E, -W
                    y=dir_mask[0] - dir_mask[1],  # +N, -S,
                    z=0,
                )
                if normal == conn_dir:
                    inst['file'] = instances['end_wall']
            continue  # We never have normal supports on end pieces
        elif new_type is utils.CONN_TYPES.none:
            # Unconnected catwalks on the wall switch to a special instance.
            # This lets players stand next to a portal surface on the wall.
            if normal.z == 0:
                inst['file'] = instances['single_wall']
                inst['angles'] = INST_ANGLE[normal.as_tuple()]
            else:
                inst.remove()
            continue  # These don't get supports otherwise

        # Add regular supports
        if normal == (0, 0, 1):
            # If in goo, use different supports!
            origin = Vec.from_str(inst['origin'])
            origin.z -= 128
            if origin.as_tuple() in conditions.GOO_LOCS:
                supp = instances['support_goo']
            else:
                supp = instances['support_floor']
        elif normal == (0, 0, -1):
            supp = instances['support_ceil']
        else:
            supp = instances['support_wall']

        if supp:
            vbsp.VMF.create_ent(
                classname='func_instance',
                origin=inst['origin'],
                angles=INST_ANGLE[normal.as_tuple()],
                file=supp,
            )

    LOGGER.info('Finished catwalk generation!')
    return RES_EXHAUSTED
Пример #19
0
def associate_faith_plates(vmf: VMF) -> None:
    """Parse through the map, collecting all faithplate segments.

    Tiling, instancelocs and connections must have been parsed first.
    Once complete all targets have been removed.

    This is done as a meta-condition to allow placing tiles we will attach to.
    """

    # Find all the triggers and targets first.
    triggers: Dict[str, Entity] = {}
    helper_trigs: Dict[str, Entity] = {}
    paint_trigs: Dict[str, Entity] = {}

    for trig in vmf.by_class['trigger_catapult']:
        name = trig['targetname']
        # Conveniently, we can determine what sort of catapult was made by
        # examining the local name used.
        if name.endswith('-helperTrigger'):
            helper_trigs[name[:-14]] = trig
            # Also store None in the main trigger if no key is there,
            # so we can detect missing main triggers...
            triggers.setdefault(name[:-14], None)
        elif name.endswith('-trigger'):
            triggers[name[:-8]] = trig
            # Remove the original relay inputs. We need to keep the output
            # to the helper if necessary.
            trig.outputs[:] = [out for out in trig.outputs if not out.inst_in]
        elif name.endswith('-catapult'):
            # Paint droppers.
            paint_trigs[name[:-9]] = trig
        else:
            LOGGER.warning('Unknown trigger "{}"?', name)

    target_to_pos: Dict[str, Union[Vec, tiling.TileDef]] = {}

    for targ in vmf.by_class['info_target']:
        name = targ['targetname']
        # All should be faith targets, with this name.
        if not name.endswith('-target'):
            LOGGER.warning('Unknown info_target "{}" @ {}?', name,
                           targ['origin'])
            continue
        name = name[:-7]

        # Find the tile we're attached to. Unfortunately no angles, so we
        # have to try both directions.
        origin = Vec.from_str(targ['origin'])

        # If the plate isn't on a tile (placed on goo for example),
        # use the direct position.
        tile = Vec.from_str(targ['origin'])

        grid_pos: Vec = origin // 128 * 128 + 64
        norm = (origin - grid_pos).norm()

        # If we're on the floor above the top of goo, move down to the surface.
        block_type = brushLoc.POS['world':tile - (0, 0, 64)]
        if block_type.is_goo and block_type.is_top:
            tile.z -= 32

        for norm in [norm, -norm]:
            # Try both directions.
            try:
                tile = tiling.TILES[(origin - 64 * norm).as_tuple(),
                                    norm.as_tuple(), ]
                break
            except KeyError:
                pass

        # We don't need the entity anymore, we'll regenerate them later.
        targ.remove()
        target_to_pos[name] = tile

    # Loop over instances, recording plates and moving targets into the tiledefs.
    instances: Dict[str, Entity] = {}

    faith_targ_file = instanceLocs.resolve('<ITEM_CATAPULT_TARGET>')
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() in faith_targ_file:
            inst.remove()  # Don't keep the targets.
            origin = Vec.from_str(inst['origin'])
            norm = Vec(z=1).rotate_by_str(inst['angles'])
            try:
                tile = tiling.TILES[(origin - 128 * norm).as_tuple(),
                                    norm.as_tuple()]
            except KeyError:
                LOGGER.warning('No tile for bullseye at {}!',
                               origin - 64 * norm)
                continue
            tile.bullseye_count += 1
            tile.add_portal_helper()
        else:
            instances[inst['targetname']] = inst

    # Now, combine into plate objects for each.
    for name, trig in triggers.items():
        if trig is None:
            raise ValueError(f'Faith plate {name} has a helper '
                             'trigger but no main trigger!')
        try:
            pos = target_to_pos[name]
        except KeyError:
            # No position, it's a straight plate.
            PLATES[name] = StraightPlate(instances[name], trig,
                                         helper_trigs[name])
        else:
            # Target position, angled plate.
            PLATES[name] = AngledPlate(instances[name], trig, pos)

    # And paint droppers
    for name, trig in paint_trigs.items():
        try:
            pos = target_to_pos[name]
        except KeyError:
            LOGGER.warning('No target for paint dropper {}!', name)
            continue
        # Target position, angled plate.
        PLATES[name] = PaintDropper(instances[name], trig, pos)
Пример #20
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 = {}  # type: Dict[str, 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 = {}  # type: Dict[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 = {}  # type: Dict[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
Пример #21
0
def res_make_catwalk(vmf: VMF, res: Property):
    """Speciallised result to generate catwalks from markers.

    Only runs once, and then quits the condition list.
    
    * Instances:
        * `markerInst: The instance set in editoritems.
        * `straight_128`/`256`/`512`: Straight sections. Extends East.
        * `corner: An L-corner piece. Connects on North and West sides.
        * `TJunction`: A T-piece. Connects on all but the East side.
        * `crossJunction`: A X-piece. Connects on all sides.
        * `end`: An end piece. Connects on the East side.
        * `stair`: A stair. Starts East and goes Up and West.
        * `end_wall`: Connects a West wall to a East catwalk.
        * `support_wall`: A support extending from the East wall.
        * `support_ceil`: A support extending from the ceiling.
        * `support_floor`: A support extending from the floor.
        * `support_goo`: A floor support, designed for goo pits.
        * `single_wall`: A section connecting to an East wall.
    """
    LOGGER.info("Starting catwalk generator...")
    marker = instanceLocs.resolve(res['markerInst'])

    instances = {
        name: instanceLocs.resolve_one(res[name, ''], error=True)
        for name in (
            'straight_128',
            'straight_256',
            'straight_512',
            'corner',
            'tjunction',
            'crossjunction',
            'end',
            'stair',
            'end_wall',
            'support_wall',
            'support_ceil',
            'support_floor',
            'support_goo',
            'single_wall',
            'markerInst',
        )
    }
    # If there are no attachments remove a catwalk piece
    instances['NONE'] = ''
    if instances['end_wall'] == '':
        instances['end_wall'] = instances['end']

    # The directions this instance is connected by (NSEW)
    links = {}  # type: Dict[Entity, Link]
    markers = {}

    # Find all our markers, so we can look them up by targetname.
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker:
            continue
        links[inst] = Link()
        markers[inst['targetname']] = inst

        # Snap the markers to the grid. If on glass it can become offset...
        origin = Vec.from_str(inst['origin'])
        origin = origin // 128 * 128
        origin += 64

        while brushLoc.POS['world':origin].is_goo:
            # The instance is in goo! Switch to floor orientation, and move
            # up until it's in air.
            inst['angles'] = '0 0 0'
            origin.z += 128

        inst['origin'] = str(origin)

    if not markers:
        return RES_EXHAUSTED

    LOGGER.info('Connections: {}', links)
    LOGGER.info('Markers: {}', markers)

    # First loop through all the markers, adding connecting sections
    for marker_name, inst in markers.items():
        mark_item = ITEMS[marker_name]
        mark_item.delete_antlines()
        for conn in list(mark_item.outputs):
            try:
                inst2 = markers[conn.to_item.name]
            except KeyError:
                LOGGER.warning('Catwalk connected to non-catwalk!')

            conn.remove()

            origin1 = Vec.from_str(inst['origin'])
            origin2 = Vec.from_str(inst2['origin'])
            if origin1.x != origin2.x and origin1.y != origin2.y:
                LOGGER.warning('Instances not aligned!')
                continue

            y_dir = origin1.x == origin2.x  # Which way the connection is
            if y_dir:
                dist = abs(origin1.y - origin2.y)
            else:
                dist = abs(origin1.x - origin2.x)
            vert_dist = origin1.z - origin2.z

            if (dist - 128) // 2 < abs(vert_dist):
                # The stairs are 2 long, 1 high. Check there's enough room
                # Subtract the last block though, since that's a corner.
                LOGGER.warning('Not enough room for stairs!')
                continue

            if dist > 128:
                # add straight sections in between
                place_catwalk_connections(vmf, instances, origin1, origin2)

            # Update the lists based on the directions that were set
            conn_lst1 = links[inst]
            conn_lst2 = links[inst2]
            if origin1.x < origin2.x:
                conn_lst1.E = conn_lst2.W = True
            elif origin2.x < origin1.x:
                conn_lst1.W = conn_lst2.E = True

            if origin1.y < origin2.y:
                conn_lst1.N = conn_lst2.S = True
            elif origin2.y < origin1.y:
                conn_lst1.S = conn_lst2.N = True

    for inst, dir_mask in links.items():
        # Set the marker instances based on the attached walkways.
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles'])

        new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()]
        inst['file'] = instances[CATWALK_TYPES[new_type]]

        if new_type is utils.CONN_TYPES.side:
            # If the end piece is pointing at a wall, switch the instance.
            if normal.z == 0:
                if normal == dir_mask.conn_dir():
                    inst['file'] = instances['end_wall']
            continue  # We never have normal supports on end pieces
        elif new_type is utils.CONN_TYPES.none:
            # Unconnected catwalks on the wall switch to a special instance.
            # This lets players stand next to a portal surface on the wall.
            if normal.z == 0:
                inst['file'] = instances['single_wall']
                inst['angles'] = INST_ANGLE[normal.as_tuple()]
            else:
                inst.remove()
            continue  # These don't get supports otherwise

        # Add regular supports
        supp = None
        if normal == (0, 0, 1):
            # If in goo, use different supports!
            origin = Vec.from_str(inst['origin'])
            origin.z -= 128
            if brushLoc.POS['world':origin].is_goo:
                supp = instances['support_goo']
            else:
                supp = instances['support_floor']
        elif normal == (0, 0, -1):
            supp = instances['support_ceil']
        else:
            supp = instances['support_wall']

        if supp:
            vmf.create_ent(
                classname='func_instance',
                origin=inst['origin'],
                angles=INST_ANGLE[normal.as_tuple()],
                file=supp,
            )

    LOGGER.info('Finished catwalk generation!')
    return RES_EXHAUSTED
Пример #22
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 item type used for the final trigger.
    item_type_sp = connections.ItemType(
        id=item_id + ':TRIGGER',
        output_act=Output.parse_name(res['triggerActivate',
                                         'OnStartTouchAll']),
        output_deact=Output.parse_name(res['triggerDeactivate',
                                           'OnEndTouchAll']),
    )

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

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

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

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

        inst1 = mark1.inst
        inst2 = mark2.inst

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

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

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

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

        # Use 'keys' and 'localkeys' blocks to set all the other keyvalues.
        conditions.set_ent_keys(trig_ent, 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,
                item_type_coop,
                mark1.ant_floor_style,
                mark1.ant_wall_style,
            )

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

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

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

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

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

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

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

    return RES_EXHAUSTED
Пример #23
0
    def parse(cls, conf: Property):
        """Read in a fizzler from a config."""
        fizz_id = conf['id']
        item_ids = [prop.value.casefold() for prop in conf.find_all('item_id')]

        try:
            model_name_type = ModelName(conf['NameType', 'same'].casefold())
        except ValueError:
            LOGGER.warning('Bad model name type: "{}"', conf['NameType'])
            model_name_type = ModelName.SAME

        model_local_name = conf['ModelName', '']
        if not model_local_name:
            # We can't rename without a local name.
            model_name_type = ModelName.SAME

        inst = {}
        for inst_type, is_static in itertools.product(FizzInst, (False, True)):
            inst_type_name = inst_type.value + ('_static' if is_static else '')
            inst[inst_type, is_static] = instances = [
                file for prop in conf.find_all(inst_type_name)
                for file in instanceLocs.resolve(prop.value)
            ]
            # Allow specifying weights to bias model locations
            weights = conf[inst_type_name + '_weight', '']
            if weights:
                # Produce the weights, then process through the original
                # list to build a new one with repeated elements.
                inst[inst_type, is_static] = instances = [
                    instances[i] for i in conditions.weighted_random(
                        len(instances), weights)
                ]
            # If static versions aren't given, reuse non-static ones.
            # We do False, True so it's already been calculated.
            if not instances and is_static:
                inst[inst_type, True] = inst[inst_type, False]

        if not inst[FizzInst.BASE, False]:
            LOGGER.warning('No base instance set! for "{}"!', fizz_id)

        voice_attrs = []
        for prop in conf.find_all('Has'):
            if prop.has_children():
                for child in prop:
                    voice_attrs.append(child.name.casefold())
            else:
                voice_attrs.append(prop.value.casefold())

        out_activate = conf['OutActivate', None]
        if out_activate is not None:
            out_activate = Output.parse_name(out_activate)

        out_deactivate = conf['OutDeactivate', None]
        if out_deactivate is not None:
            out_deactivate = Output.parse_name(out_deactivate)

        pack_lists = {prop.value for prop in conf.find_all('Pack')}
        pack_lists_static = {
            prop.value
            for prop in conf.find_all('PackStatic')
        }

        brushes = [FizzlerBrush.parse(prop) for prop in conf.find_all('Brush')]

        beams = []  # type: List[FizzBeam]
        for beam_prop in conf.find_all('Beam'):
            offsets = [
                Vec.from_str(off.value) for off in beam_prop.find_all('pos')
            ]
            keys = Property('', [
                beam_prop.find_key('Keys', []),
                beam_prop.find_key('LocalKeys', [])
            ])
            beams.append(
                FizzBeam(
                    offsets,
                    keys,
                    beam_prop.int('RandSpeedMin', 0),
                    beam_prop.int('RandSpeedMax', 0),
                ))

        try:
            temp_conf = conf.find_key('TemplateBrush')
        except NoKeyError:
            temp_brush_keys = temp_min = temp_max = temp_single = None
        else:
            temp_brush_keys = Property('--', [
                temp_conf.find_key('Keys'),
                temp_conf.find_key('LocalKeys', []),
            ])

            # Find and load the templates.
            temp_min = temp_conf['Left', None]
            temp_max = temp_conf['Right', None]
            temp_single = temp_conf['Single', None]

        return FizzlerType(
            fizz_id,
            item_ids,
            voice_attrs,
            pack_lists,
            pack_lists_static,
            model_local_name,
            model_name_type,
            out_activate,
            out_deactivate,
            brushes,
            beams,
            inst,
            temp_brush_keys,
            temp_min,
            temp_max,
            temp_single,
        )
Пример #24
0
def res_make_catwalk(vmf: VMF, res: Property):
    """Speciallised result to generate catwalks from markers.

    Only runs once, and then quits the condition list.
    
    * Instances:
        * `markerInst: The instance set in editoritems.
        * `straight_128`/`256`/`512`: Straight sections. Extends East.
        * `corner: An L-corner piece. Connects on North and West sides.
        * `TJunction`: A T-piece. Connects on all but the East side.
        * `crossJunction`: A X-piece. Connects on all sides.
        * `end`: An end piece. Connects on the East side.
        * `stair`: A stair. Starts East and goes Up and West.
        * `end_wall`: Connects a West wall to a East catwalk.
        * `support_wall`: A support extending from the East wall.
        * `support_ceil`: A support extending from the ceiling.
        * `support_floor`: A support extending from the floor.
        * `support_goo`: A floor support, designed for goo pits.
        * `single_wall`: A section connecting to an East wall.
    """
    LOGGER.info("Starting catwalk generator...")
    marker = instanceLocs.resolve(res['markerInst'])

    instances = {
        name: instanceLocs.resolve_one(res[name, ''], error=True)
        for name in
        (
            'straight_128', 'straight_256', 'straight_512',
            'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall',
            'support_wall', 'support_ceil', 'support_floor', 'support_goo',
            'single_wall',
            'markerInst',
        )
    }
    # If there are no attachments remove a catwalk piece
    instances['NONE'] = ''
    if instances['end_wall'] == '':
        instances['end_wall'] = instances['end']

    # The directions this instance is connected by (NSEW)
    links = {}  # type: Dict[Entity, Link]
    markers = {}

    # Find all our markers, so we can look them up by targetname.
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker:
            continue
        links[inst] = Link()
        markers[inst['targetname']] = inst

        # Snap the markers to the grid. If on glass it can become offset...
        origin = Vec.from_str(inst['origin'])
        origin = origin // 128 * 128
        origin += 64

        while brushLoc.POS['world': origin].is_goo:
            # The instance is in goo! Switch to floor orientation, and move
            # up until it's in air.
            inst['angles'] = '0 0 0'
            origin.z += 128

        inst['origin'] = str(origin)

    if not markers:
        return RES_EXHAUSTED

    LOGGER.info('Connections: {}', links)
    LOGGER.info('Markers: {}', markers)

    # First loop through all the markers, adding connecting sections
    for marker_name, inst in markers.items():
        mark_item = ITEMS[marker_name]
        mark_item.delete_antlines()
        for conn in list(mark_item.outputs):
            try:
                inst2 = markers[conn.to_item.name]
            except KeyError:
                LOGGER.warning('Catwalk connected to non-catwalk!')

            conn.remove()

            origin1 = Vec.from_str(inst['origin'])
            origin2 = Vec.from_str(inst2['origin'])
            if origin1.x != origin2.x and origin1.y != origin2.y:
                LOGGER.warning('Instances not aligned!')
                continue

            y_dir = origin1.x == origin2.x  # Which way the connection is
            if y_dir:
                dist = abs(origin1.y - origin2.y)
            else:
                dist = abs(origin1.x - origin2.x)
            vert_dist = origin1.z - origin2.z

            if (dist - 128) // 2 < abs(vert_dist):
                # The stairs are 2 long, 1 high. Check there's enough room
                # Subtract the last block though, since that's a corner.
                LOGGER.warning('Not enough room for stairs!')
                continue

            if dist > 128:
                # add straight sections in between
                place_catwalk_connections(vmf, instances, origin1, origin2)

            # Update the lists based on the directions that were set
            conn_lst1 = links[inst]
            conn_lst2 = links[inst2]
            if origin1.x < origin2.x:
                conn_lst1.E = conn_lst2.W = True
            elif origin2.x < origin1.x:
                conn_lst1.W = conn_lst2.E = True

            if origin1.y < origin2.y:
                conn_lst1.N = conn_lst2.S = True
            elif origin2.y < origin1.y:
                conn_lst1.S = conn_lst2.N = True

    for inst, dir_mask in links.items():
        # Set the marker instances based on the attached walkways.
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles'])

        new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()]
        inst['file'] = instances[CATWALK_TYPES[new_type]]

        if new_type is utils.CONN_TYPES.side:
            # If the end piece is pointing at a wall, switch the instance.
            if normal.z == 0:
                if normal == dir_mask.conn_dir():
                    inst['file'] = instances['end_wall']
            continue  # We never have normal supports on end pieces
        elif new_type is utils.CONN_TYPES.none:
            # Unconnected catwalks on the wall switch to a special instance.
            # This lets players stand next to a portal surface on the wall.
            if normal.z == 0:
                inst['file'] = instances['single_wall']
                inst['angles'] = INST_ANGLE[normal.as_tuple()]
            else:
                inst.remove()
            continue  # These don't get supports otherwise

        # Add regular supports
        supp = None
        if normal == (0, 0, 1):
            # If in goo, use different supports!
            origin = Vec.from_str(inst['origin'])
            origin.z -= 128
            if brushLoc.POS['world': origin].is_goo:
                supp = instances['support_goo']
            else:
                supp = instances['support_floor']
        elif normal == (0, 0, -1):
            supp = instances['support_ceil']
        else:
            supp = instances['support_wall']

        if supp:
            vmf.create_ent(
                classname='func_instance',
                origin=inst['origin'],
                angles=INST_ANGLE[normal.as_tuple()],
                file=supp,
            )

    LOGGER.info('Finished catwalk generation!')
    return RES_EXHAUSTED
Пример #25
0
def res_cutout_tile(vmf: srctools.VMF, res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

    - "MarkerItem" is the instance to look for.
    - "TileSize" can be "2x2" or "4x4".
    - rotateMax is the amount of degrees to rotate squarebeam models.

    Materials:
    - "squarebeams" is the squarebeams variant to use.
    - "ceilingwalls" are the sides of the ceiling section.
    - "floorbase" is the texture under floor sections.
    - "tile_glue" is used on top of a thinner tile segment.
    - "clip" is the player_clip texture used over floor segments.
        (This allows customising the surfaceprop.)
    - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to
       override the textures used.
    """
    marker_filenames = instanceLocs.resolve(res['markeritem'])

    INST_LOCS = {}  # Map targetnames -> surface loc
    CEIL_IO = []  # Pairs of ceil inst corners to cut out.
    FLOOR_IO = []  # Pairs of floor inst corners to cut out.

    overlay_ids = {}  # When we replace brushes, we need to fix any overlays
    # on that surface.

    MATS.clear()
    floor_edges = []  # Values to pass to add_floor_sides() at the end

    sign_locs = set()
    # If any signage is present in the map, we need to force tiles to
    # appear at that location!
    for over in vmf.by_class['info_overlay']:
        if (
                over['material'].casefold() in FORCE_TILE_MATS and
                # Only check floor/ceiling overlays
                over['basisnormal'] in ('0 0 1', '0 0 -1')
                ):
            add_signage_loc(sign_locs, Vec.from_str(over['origin']))

    for item in connections.ITEMS.values():
        for ind_pan in item.ind_panels:
            loc = Vec(0, 0, -64)
            loc.localise(
                Vec.from_str(ind_pan['origin']),
                Vec.from_str(ind_pan['angles']),
            )
            add_signage_loc(sign_locs, loc)

    SETTINGS = {
        'floor_chance': srctools.conv_int(
            res['floorChance', '100'], 100),
        'ceil_chance': srctools.conv_int(
            res['ceilingChance', '100'], 100),
        'floor_glue_chance': srctools.conv_int(
            res['floorGlueChance', '0']),
        'ceil_glue_chance': srctools.conv_int(
            res['ceilingGlueChance', '0']),

        'rotate_beams': int(srctools.conv_float(
            res['rotateMax', '0']) * BEAM_ROT_PRECISION),

        'beam_skin': res['squarebeamsSkin', '0'],

        'base_is_disp': srctools.conv_bool(res['dispBase', '0']),

        'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2',
        'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2',
    }

    random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE')
    noise = SimplexNoise(period=4 * 40)  # 4 tiles/block, 50 blocks max

    # We want to know the number of neighbouring tile cutouts before
    # placing tiles - blocks away from the sides generate fewer tiles.
    floor_neighbours = defaultdict(dict)  # all_floors[z][x,y] = count

    for mat_prop in res.find_key('Materials', []):
        MATS[mat_prop.name].append(mat_prop.value)

    if SETTINGS['base_is_disp']:
        # We want the normal brushes to become nodraw.
        MATS['floorbase_disp'] = MATS['floorbase']
        MATS['floorbase'] = ['tools/toolsnodraw']

        # Since this uses random data for initialisation, the alpha and
        # regular will use slightly different patterns.
        alpha_noise = SimplexNoise(period=4 * 50)
    else:
        alpha_noise = None

    for key, default in TEX_DEFAULT:
        if key not in MATS:
            MATS[key] = [default]

    # Find our marker ents
    for inst in vmf.by_class['func_instance']:
        if inst['file'].casefold() not in marker_filenames:
            continue
        targ = inst['targetname']
        normal = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0'])
        # Check the orientation of the marker to figure out what to generate
        if normal == (0, 0, 1):
            io_list = FLOOR_IO
        else:
            io_list = CEIL_IO

        # Reuse orient to calculate where the solid face will be.
        loc = Vec.from_str(inst['origin']) - 64 * normal
        INST_LOCS[targ] = loc

        item = connections.ITEMS[targ]
        item.delete_antlines()

        if item.outputs:
            for conn in list(item.outputs):
                if conn.to_item.inst['file'].casefold() in marker_filenames:
                    io_list.append((targ, conn.to_item.name))
                else:
                    LOGGER.warning('Cutout tile connected to non-cutout!')
                conn.remove()  # Delete the connection.
        else:
            # If the item doesn't have any connections, 'connect'
            # it to itself so we'll generate a 128x128 tile segment.
            io_list.append((targ, targ))

        # Remove all traces of this item (other than in connections lists).
        inst.remove()
        del connections.ITEMS[targ]

    for start_floor, end_floor in FLOOR_IO:
        box_min = Vec(INST_LOCS[start_floor])
        box_min.min(INST_LOCS[end_floor])

        box_max = Vec(INST_LOCS[start_floor])
        box_max.max(INST_LOCS[end_floor])

        if box_min.z != box_max.z:
            continue  # They're not in the same level!
        z = box_min.z

        if SETTINGS['rotate_beams']:
            # We have to generate 1 model per 64x64 block to do rotation...
            gen_rotated_squarebeams(
                vmf,
                box_min - (64, 64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin'],
                max_rot=SETTINGS['rotate_beams'],
            )
        else:
            # Make the squarebeams props, using big models if possible
            gen_squarebeams(
                vmf,
                box_min + (-64, -64, 0),
                box_max + (64, 64, -8),
                skin=SETTINGS['beam_skin']
            )

        # Add a player_clip brush across the whole area
        vmf.add_brush(vmf.make_prism(
            p1=box_min - (64, 64, FLOOR_DEPTH),
            p2=box_max + (64, 64, 0),
            mat=MATS['clip'][0],
        ).solid)

        # Add a noportal_volume covering the surface, in case there's
        # room for a portal.
        noportal_solid = vmf.make_prism(
            # Don't go all the way to the sides, so it doesn't affect wall
            # brushes.
            p1=box_min - (63, 63, 9),
            p2=box_max + (63, 63, 0),
            mat='tools/toolsinvisible',
        ).solid
        noportal_ent = vmf.create_ent(
            classname='func_noportal_volume',
            origin=box_min.join(' '),
        )
        noportal_ent.solids.append(noportal_solid)

        if SETTINGS['base_is_disp']:
            # Use displacements for the base instead.
            make_alpha_base(
                vmf,
                box_min + (-64, -64, 0),
                box_max + (64, 64, 0),
                noise=alpha_noise,
            )

        for x, y in utils.iter_grid(
                min_x=int(box_min.x),
                max_x=int(box_max.x) + 1,
                min_y=int(box_min.y),
                max_y=int(box_max.y) + 1,
                stride=128,
                ):
            # Build the set of all positions..
            floor_neighbours[z][x, y] = -1

        # Mark borders we need to fill in, and the angle (for func_instance)
        # The wall is the face pointing inwards towards the bottom brush,
        # and the ceil is the ceiling of the block above the bordering grid
        # points.
        for x in range(int(box_min.x), int(box_max.x) + 1, 128):
            # North
            floor_edges.append(BorderPoints(
                wall=Vec(x, box_max.y + 64, z - 64),
                ceil=Vec_tuple(x, box_max.y + 128, z),
                rot=270,
            ))
            # South
            floor_edges.append(BorderPoints(
                wall=Vec(x, box_min.y - 64, z - 64),
                ceil=Vec_tuple(x, box_min.y - 128, z),
                rot=90,
            ))

        for y in range(int(box_min.y), int(box_max.y) + 1, 128):
            # East
            floor_edges.append(BorderPoints(
                wall=Vec(box_max.x + 64, y, z - 64),
                ceil=Vec_tuple(box_max.x + 128, y, z),
                rot=180,
            ))

            # West
            floor_edges.append(BorderPoints(
                wall=Vec(box_min.x - 64, y, z - 64),
                ceil=Vec_tuple(box_min.x - 128, y, z),
                rot=0,
            ))

    # Now count boundaries near tiles, then generate them.

    # Do it separately for each z-level:
    for z, xy_dict in floor_neighbours.items():  # type: float, dict
        for x, y in xy_dict:  # type: float, float
            # We want to count where there aren't any tiles
            xy_dict[x, y] = (
                ((x - 128, y - 128) not in xy_dict) +
                ((x - 128, y + 128) not in xy_dict) +
                ((x + 128, y - 128) not in xy_dict) +
                ((x + 128, y + 128) not in xy_dict) +

                ((x - 128, y) not in xy_dict) +
                ((x + 128, y) not in xy_dict) +
                ((x, y - 128) not in xy_dict) +
                ((x, y + 128) not in xy_dict)
            )

        max_x = max_y = 0

        weights = {}
        # Now the counts are all correct, compute the weight to apply
        # for tiles.
        # Adding the neighbouring counts will make a 5x5 area needed to set
        # the center to 0.

        for (x, y), cur_count in xy_dict.items():
            max_x = max(x, max_x)
            max_y = max(y, max_y)

            # Orthrogonal is worth 0.2, diagonal is worth 0.1.
            # Not-present tiles would be 8 - the maximum
            tile_count = (
                0.8 * cur_count +
                0.1 * xy_dict.get((x - 128, y - 128), 8) +
                0.1 * xy_dict.get((x - 128, y + 128), 8) +
                0.1 * xy_dict.get((x + 128, y - 128), 8) +
                0.1 * xy_dict.get((x + 128, y + 128), 8) +

                0.2 * xy_dict.get((x - 128, y), 8) +
                0.2 * xy_dict.get((x, y - 128), 8) +
                0.2 * xy_dict.get((x, y + 128), 8) +
                0.2 * xy_dict.get((x + 128, y), 8)
            )
            # The number ranges from 0 (all tiles) to 12.8 (no tiles).
            # All tiles should still have a small chance to generate tiles.
            weights[x, y] = min((tile_count + 0.5) / 8, 1)

        # Share the detail entity among same-height tiles..
        detail_ent = vmf.create_ent(
            classname='func_detail',
        )

        for x, y in xy_dict:
            convert_floor(
                vmf,
                Vec(x, y, z),
                overlay_ids,
                MATS,
                SETTINGS,
                sign_locs,
                detail_ent,
                noise_weight=weights[x, y],
                noise_func=noise,
            )

    add_floor_sides(vmf, floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
Пример #26
0
def res_track_plat(vmf: VMF, res: Property):
    """Logic specific to Track Platforms.

    This allows switching the instances used depending on if the track
    is horizontal or vertical and sets the track
    targetnames to a useful value. This should be run unconditionally, not
    once per item.
    Values:
    
    * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets.
      This is used to determine all the instance filenames.
    * `single_plat`: An instance used for the entire platform, if it's
      one rail long (and therefore can't move).
    * `track_name`: If set, rename track instances following the pattern
      `plat_name-track_nameXX`. Otherwise all tracks will receive the name
      of the platform.
    * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix
      to the platform.
    * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the
      provided $fixup variable.
    * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup
      variable to indicate the relative direction the top faces.
    """
    # Get the instances from editoritems
    (
        inst_bot_grate, inst_bottom, inst_middle,
        inst_top, inst_plat, inst_plat_oscil, inst_single
    ) = instanceLocs.resolve(res['orig_item'])
    single_plat_inst = instanceLocs.resolve_one(res['single_plat', ''])
    track_targets = res['track_name', '']

    track_files = [inst_bottom, inst_middle, inst_top, inst_single]
    platforms = [inst_plat, inst_plat_oscil]

    # All the track_set in the map, indexed by origin
    track_instances = {
        Vec.from_str(inst['origin']).as_tuple(): inst
        for inst in
        vmf.by_class['func_instance']
        if inst['file'].casefold() in track_files
    }

    LOGGER.debug('Track instances:')
    LOGGER.debug('\n'.join(
        '{!s}: {}'.format(k, v['file'])
        for k, v in
        track_instances.items()
    ))

    if not track_instances:
        return RES_EXHAUSTED

    # Now we loop through all platforms in the map, and then locate their
    # track_set
    for plat_inst in vmf.by_class['func_instance']:
        if plat_inst['file'].casefold() not in platforms:
            continue  # Not a platform!

        LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!')

        plat_loc = Vec.from_str(plat_inst['origin'])
        # The direction away from the wall/floor/ceil
        normal = Vec(0, 0, 1).rotate_by_str(
            plat_inst['angles']
        )

        for tr_origin, first_track in track_instances.items():
            if plat_loc == tr_origin:
                # Check direction

                if normal == Vec(0, 0, 1).rotate(
                        *Vec.from_str(first_track['angles'])
                        ):
                    break
        else:
            raise Exception('Platform "{}" has no track!'.format(
                plat_inst['targetname']
            ))

        track_type = first_track['file'].casefold()
        if track_type == inst_single:
            # Track is one block long, use a single-only instance and
            # remove track!
            plat_inst['file'] = single_plat_inst
            first_track.remove()
            continue  # Next platform

        track_set = set()  # type: Set[Entity]
        if track_type == inst_top or track_type == inst_middle:
            # search left
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=-1,
            )
        if track_type == inst_bottom or track_type == inst_middle:
            # search right
            track_scan(
                track_set,
                track_instances,
                first_track,
                middle_file=inst_middle,
                x_dir=+1,
            )

        # Give every track a targetname matching the platform
        for ind, track in enumerate(track_set, start=1):
            if track_targets == '':
                track['targetname'] = plat_inst['targetname']
            else:
                track['targetname'] = (
                    plat_inst['targetname'] +
                    '-' +
                    track_targets + str(ind)
                )

        # Now figure out which way the track faces:

        # The direction of the platform surface
        facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles'])

        # The direction horizontal track is offset
        uaxis = Vec(x=1).rotate_by_str(first_track['angles'])
        vaxis = Vec(y=1).rotate_by_str(first_track['angles'])

        if uaxis == facing:
            plat_facing = 'vert'
            track_facing = 'E'
        elif uaxis == -facing:
            plat_facing = 'vert'
            track_facing = 'W'
        elif vaxis == facing:
            plat_facing = 'horiz'
            track_facing = 'N'
        elif vaxis == -facing:
            plat_facing = 'horiz'
            track_facing = 'S'
        else:
            raise ValueError('Facing {} is not U({}) or V({})!'.format(
                facing,
                uaxis,
                vaxis,
            ))

        if res.bool('plat_suffix'):
            conditions.add_suffix(plat_inst, '_' + plat_facing)

        plat_var = res['plat_var', '']
        if plat_var:
            plat_inst.fixup[plat_var] = plat_facing

        track_var = res['track_var', '']
        if track_var:
            plat_inst.fixup[track_var] = track_facing

        for track in track_set:
            track.fixup.update(plat_inst.fixup)

    return RES_EXHAUSTED  # Don't re-run
Пример #27
0
def fizzler_out_relay():
    """Link fizzlers with a relay item so they can be given outputs."""
    relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>')
    if not relay_file:
        # No relay item - deactivated most likely.
        return

    # instances
    fizz_models = set()
    # base -> connections
    fizz_bases = {}

    for fizz_id in ITEMS_WITH_CLASS[const.ItemClass.FIZZLER]:
        base, model = instanceLocs.resolve(
            '<{}: fizz_base, fizz_model>'.format(fizz_id))
        fizz_bases[base.casefold()] = CONNECTIONS[fizz_id]
        fizz_models.add(model.casefold())

    # targetname -> base inst, connections
    fizz_by_name = {}

    # origin, normal -> targetname
    pos_to_name = {}

    marker_inst = []  # type: List[Entity]

    LOGGER.info('Fizzler data: {}', locals())

    for inst in vbsp.VMF.by_class['func_instance']:
        filename = inst['file'].casefold()
        name = inst['targetname']
        if filename in fizz_bases:
            fizz_by_name[inst['targetname']] = inst, fizz_bases[filename]
        elif filename in fizz_models:
            if inst['targetname'].endswith(('_modelStart', '_modelEnd')):
                name = inst['targetname'].rsplit('_', 1)[0]

        elif filename in relay_file:
            marker_inst.append(inst)
            # Remove the marker, we don't need that...
            inst.remove()
            continue
        else:
            continue

        pos_to_name[
            Vec.from_str(inst['origin']).as_tuple(),
            Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] = name

    for inst in marker_inst:
        try:
            fizz_name = pos_to_name[
                Vec.from_str(inst['origin']).as_tuple(),
                Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()]
        except KeyError:
            # Not placed on a fizzler...
            continue
        base_inst, connections = fizz_by_name[fizz_name]

        # Copy over fixup values
        for key, val in inst.fixup.items():
            base_inst.fixup[key] = val

        for out in inst.outputs:
            new_out = out.copy()
            if out.output == 'ON':
                new_out.inst_out, new_out.output = connections.out_act
            elif out.output == 'OFF':
                new_out.inst_out, new_out.output = connections.out_deact
            else:
                # Not the marker's output somehow?
                continue
            base_inst.add_out(new_out)
Пример #28
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)
    # Grab a copy of the beam spawnflags so we can set our own options.
    conf_beam_flags = res.find_key('BeamKeys', []).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
    )
    conf_outputs = [
        Output.parse(prop) for prop in res
        if prop.name in ('onenabled', 'ondisabled')
    ]

    # Find all the markers.
    nodes = {}  # type: Dict[str, 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

    # 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((node, neighbour))

            # If we have a real output, we need to transfer it.
            # Otherwise we can just destroy it.
            if has_output:
                group.item.antlines.update(node.antlines)
                group.item.ind_panels.update(node.ind_panels)
                group.item.shape_signs.extend(node.shape_signs)
            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((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())

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

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

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

            # First add the sprite at the right height.
            sprite_pos = conf_glow_height.copy()
            sprite_pos.localise(
                Vec.from_str(node.inst['origin']),
                Vec.from_str(node.inst['angles']),
            )
            sprite = vmf.create_ent('env_sprite')
            conditions.set_ent_keys(sprite, node.inst, res, 'GlowKeys')
            sprite['origin'] = sprite_pos
            sprite['targetname'] = NAME_SPR(base_name, i)

            # 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')
            conditions.set_ent_keys(beam, node.inst, res, 'BeamKeys')
            beam['origin'] = 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

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

    return conditions.RES_EXHAUSTED
Пример #29
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

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

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

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

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

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

    marker_names = set()

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

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

    item_id = res['markerItem']

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

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

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

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

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

        inst1 = mark1.inst
        inst2 = mark2.inst

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return RES_EXHAUSTED
Пример #30
0
def res_resizeable_trigger(res: Property):
    """Replace two markers with a trigger brush.  

    This is run once to affect all of an item.  
    Options:
    * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename.
    * `markerItem`: The item's ID
    * `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 `instance:name;Input` value
        to turn the previewInst on and off.

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

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

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

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

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

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

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

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

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

    # Display preview overlays if it's preview mode, and the config is true
    if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''],
                                                     False):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        pre_act_name, pre_act_inp = Output.parse_name(res['previewActivate',
                                                          ''])
        pre_deact_name, pre_deact_inp = Output.parse_name(
            res['previewDeactivate', ''])
        preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25)
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        pre_act_name = pre_deact_name = None
        pre_act_inp = pre_deact_inp = ''
        preview_scale = 0.25

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return RES_EXHAUSTED