Exemple #1
0
        def iter_lines(conf: Property) -> Iterator[Property]:
            """Iterate over the varios line blocks."""
            yield from conf.find_all("Quotes", "Group", "Quote", "Line")

            yield from conf.find_all("Quotes", "Midchamber", "Quote", "Line")

            for group in conf.find_children("Quotes", "CoopResponses"):
                if group.has_children():
                    yield from group
Exemple #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.
Exemple #3
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.
Exemple #4
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 resolve_inst(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.
def load_settings(pit: Property):
    if not pit:
        SETTINGS.clear()
        # No pits are permitted..
        return

    SETTINGS.update({
        'use_skybox': srctools.conv_bool(pit['teleport', '0']),
        'tele_dest': pit['tele_target', '@goo_targ'],
        'tele_ref': pit['tele_ref', '@goo_ref'],
        'off_x': srctools.conv_int(pit['off_x', '0']),
        'off_y': srctools.conv_int(pit['off_y', '0']),
        'skybox': pit['sky_inst', ''],
        'skybox_ceil': pit['sky_inst_ceil', ''],
        'targ': pit['targ_inst', ''],
        'blend_light': pit['blend_light', '']
    })
    for inst_type in (
        'support',
        'side',
        'corner',
        'double',
        'triple',
        'pillar',
    ):
        vals = [
            prop.value
            for prop in
            pit.find_all(inst_type + '_inst')
        ]
        if len(vals) == 0:
            vals = [""]
        PIT_INST[inst_type] = vals
Exemple #6
0
def load_settings(pit: Property):
    if not pit:
        SETTINGS.clear()
        # No pits are permitted..
        return

    SETTINGS.update({
        'use_skybox': srctools.conv_bool(pit['teleport', '0']),
        'tele_dest': pit['tele_target', '@goo_targ'],
        'tele_ref': pit['tele_ref', '@goo_ref'],
        'off_x': srctools.conv_int(pit['off_x', '0']),
        'off_y': srctools.conv_int(pit['off_y', '0']),
        'skybox': pit['sky_inst', ''],
        'skybox_ceil': pit['sky_inst_ceil', ''],
        'targ': pit['targ_inst', ''],
        'blend_light': pit['blend_light', '']
    })
    for inst_type in (
        'support',
        'side',
        'corner',
        'double',
        'triple',
        'pillar',
    ):
        vals = [
            prop.value
            for prop in
            pit.find_all(inst_type + '_inst')
        ]
        if len(vals) == 0:
            vals = [""]
        PIT_INST[inst_type] = vals
Exemple #7
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
        inst_configs = VAC_CONFIGS[group] = {}
    else:
        # Grab the already-filled values, and add to them
        inst_configs = VAC_CONFIGS[group]

    def get_temp(key):
        try:
            temp_id = block['temp_' + key]
        except LookupError:
            return None
        try:
            return template_brush.get_template(temp_id)
        except template_brush.InvalidTemplateName:
            LOGGER.warning('Invalid template "{}" for vactube group {}!',
                           temp_id, group)
            return None

    for block in res.find_all("Instance"):
        # Configuration info for each instance set..
        conf = Config(
            # The three sizes of corner instance
            inst_corner=[
                block['corner_small_inst', ''],
                block['corner_medium_inst', ''],
                block['corner_large_inst', ''],
            ],
            temp_corner=[
                get_temp('corner_small'),
                get_temp('corner_medium'),
                get_temp('corner_large'),
            ],
            # Straight instances connected to the next part
            inst_straight=block['straight_inst', ''],
            # Supports attach to the 4 sides of the straight part,
            # if there's a brush there.
            inst_support=block['support_inst', ''],
            inst_entry_floor=block['entry_floor_inst'],
            inst_entry_wall=block['entry_inst'],
            inst_entry_ceil=block['entry_ceil_inst'],
            inst_exit=block['exit_inst'],
        )

        for prop in block.find_all("File"):
            try:
                size_str, file = prop.value.split(":", 1)
                # Users enter 1-3, use 0-2 in code.
                size = srctools.conv_int(size_str, 1) - 1
            except ValueError:
                size = 0
                file = prop.value

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

    return group
Exemple #8
0
 def generate_fizzler_sides(self, conf: Property):
     fizz_colors = {}
     mat_path = self.abs_path('bee2/materials/BEE2/fizz_sides/side_color_')
     for brush_conf in conf.find_all('Fizzlers', 'Fizzler', 'Brush'):
         fizz_color = brush_conf['Side_color', '']
         if fizz_color:
             fizz_colors[Vec.from_str(fizz_color).as_tuple()] = (
                 brush_conf.float('side_alpha',
                                  1), brush_conf['side_vortex', fizz_color])
     if fizz_colors:
         os.makedirs(self.abs_path('bee2/materials/BEE2/fizz_sides/'),
                     exist_ok=True)
     for fizz_color, (alpha, fizz_vortex_color) in fizz_colors.items():
         file_path = mat_path + '{:02X}{:02X}{:02X}.vmt'.format(
             round(fizz_color.x * 255),
             round(fizz_color.y * 255),
             round(fizz_color.z * 255),
         )
         with open(file_path, 'w') as f:
             f.write(
                 FIZZLER_EDGE_MAT.format(Vec(fizz_color),
                                         fizz_vortex_color))
             if alpha != 1:
                 # Add the alpha value, but replace 0.5 -> .5 to save a char.
                 f.write('$outputintensity {}\n'.format(
                     format(alpha, 'g').replace('0.', '.')))
             f.write(FIZZLER_EDGE_MAT_PROXY)
Exemple #9
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
Exemple #10
0
def set_cond_source(props: Property, source: str) -> None:
    """Set metadata for Conditions in the given config blocks.

    This generates '__src__' keyvalues in Condition blocks with info like
    the source object ID and originating file, so errors can be traced back
    to the config file creating it.
    """
    for cond in props.find_all('Conditions', 'Condition'):
        cond['__src__'] = source
Exemple #11
0
def res_timed_relay_setup(res: Property):
    var = res['variable', consts.FixupVars.TIM_DELAY]
    name = res['targetname']
    disabled = res['disabled', '0']
    flags = res['spawnflags', '0']

    final_outs = [
        Output.parse(subprop) for prop in res.find_all('FinalOutputs')
        for subprop in prop
    ]

    rep_outs = [
        Output.parse(subprop) for prop in res.find_all('RepOutputs')
        for subprop in prop
    ]

    # Never use the comma seperator in the final output for consistency.
    for out in itertools.chain(rep_outs, final_outs):
        out.comma_sep = False

    return var, name, disabled, flags, final_outs, rep_outs
Exemple #12
0
    def clean_editor_models(self, editoritems: Property):
        """The game is limited to having 1024 models loaded at once.

        Editor models are always being loaded, so we need to keep the number
        small. Go through editoritems, and disable (by renaming to .mdl_dis)
        unused ones.
        """
        # If set, force them all to be present.
        force_on = GEN_OPTS.get_bool('Debug', 'force_all_editor_models')

        used_models = {
            mdl.value.rsplit('.', 1)[0].casefold()
            for mdl in editoritems.find_all(
                'Item',
                'Editor',
                'Subtype',
                'Model',
                'ModelName',
            )
        }

        mdl_count = 0

        for mdl_folder in [
                self.abs_path('bee2/models/props_map_editor/'),
                self.abs_path('bee2_dev/models/props_map_editor/'),
        ]:
            if not os.path.exists(mdl_folder):
                continue
            for file in os.listdir(mdl_folder):
                if not file.endswith(('.mdl', '.mdl_dis')):
                    continue

                mdl_count += 1

                file_no_ext, ext = os.path.splitext(file)
                if force_on or file_no_ext.casefold() in used_models:
                    new_ext = '.mdl'
                else:
                    new_ext = '.mdl_dis'

                if new_ext != ext:
                    try:
                        os.remove(
                            os.path.join(mdl_folder, file_no_ext + new_ext))
                    except FileNotFoundError:
                        pass
                    os.rename(
                        os.path.join(mdl_folder, file_no_ext + ext),
                        os.path.join(mdl_folder, file_no_ext + new_ext),
                    )

        LOGGER.info('{}/{} editor models used.', len(used_models), mdl_count)
Exemple #13
0
def gen_sound_manifest(additional, excludes):
    """Generate a new game_sounds_manifest.txt file.

    This includes all the current scripts defined, plus any custom ones.
    Excludes is a list of scripts to remove from the listing - this allows
    overriding the sounds without VPK overrides.
    """
    if not additional:
        return  # Don't pack, there aren't any new sounds..

    orig_manifest = os.path.join(
        '..',
        SOUND_MAN_FOLDER.get(CONF['game_id', ''], 'portal2'),
        'scripts',
        'game_sounds_manifest.txt',
    )

    try:
        with open(orig_manifest) as f:
            props = Property.parse(f, orig_manifest).find_key(
                'game_sounds_manifest',
                [],
            )
    except FileNotFoundError:  # Assume no sounds
        props = Property('game_sounds_manifest', [])

    scripts = [prop.value for prop in props.find_all('precache_file')]

    for script in additional:
        scripts.append(script)

    for script in excludes:
        try:
            scripts.remove(script)
        except ValueError:
            LOGGER.warning(
                '"{}" should be excluded, but it\'s'
                ' not in the manifest already!',
                script,
            )

    # Build and unbuild it to strip other things out - Valve includes a bogus
    # 'new_sound_scripts_must_go_below_here' entry..
    new_props = Property('game_sounds_manifest',
                         [Property('precache_file', file) for file in scripts])

    inject_loc = os.path.join('bee2', 'inject', 'soundscript_manifest.txt')
    with open(inject_loc, 'w') as f:
        for line in new_props.export():
            f.write(line)
    LOGGER.info('Written new soundscripts_manifest..')
Exemple #14
0
def res_timed_relay_setup(res: Property):
    var = res['variable', consts.FixupVars.TIM_DELAY]
    name = res['targetname']
    disabled = res['disabled', '0']
    flags = res['spawnflags', '0']

    final_outs = [
        Output.parse(subprop)
        for prop in res.find_all('FinalOutputs')
        for subprop in prop
    ]

    rep_outs = [
        Output.parse(subprop)
        for prop in res.find_all('RepOutputs')
        for subprop in prop
    ]

    # Never use the comma seperator in the final output for consistency.
    for out in itertools.chain(rep_outs, final_outs):
        out.comma_sep = False

    return var, name, disabled, flags, final_outs, rep_outs
Exemple #15
0
def res_cust_antline_setup(res: Property):
    def find(cat):
        """Helper to reduce code duplication."""
        return [p.value for p in res.find_all(cat)]

    # Allow overriding these options. If unset use the style's value - the
    # amount of destruction will usually be the same.
    broken_chance = res.float(
        'broken_antline_chance',
        vbsp_options.get(float, 'broken_antline_chance'),
    )
    broken_dist = res.int(
        'broken_antline_distance',
        vbsp_options.get(int, 'broken_antline_distance'),
    )

    toggle_inst = res['instance', '']
    toggle_out = list(res.find_all('addOut'))

    # These textures are required - the base ones.
    straight_tex = find('straight')
    corner_tex = find('corner')

    # Arguments to pass to setAntlineMat
    straight_args = [
        straight_tex,
        find('straightFloor') or (),
        # Extra broken antline textures / options, if desired.
        broken_chance,
        broken_dist,
        find('brokenStraight') or (),
        find('brokenStraightFloor') or (),
    ]

    # The same but for corners.
    corner_args = [
        corner_tex,
        find('cornerFloor') or (),
        broken_chance,
        broken_dist,
        find('brokenCorner') or (),
        find('brokenCornerFloor') or (),
    ]

    if not straight_tex or not corner_tex:
        # If we don't have two textures, something's wrong. Remove this result.
        LOGGER.warning('custAntline has no textures!')
        return None
    else:
        return straight_args, corner_args, toggle_inst, toggle_out
Exemple #16
0
def read_configs(conf: Property) -> None:
    """Read in the fizzler data."""
    for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'):
        fizz = FizzlerType.parse(fizz_conf)

        if fizz.id in FIZZ_TYPES:
            raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id))

        FIZZ_TYPES[fizz.id] = fizz

    LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES))

    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']:
        return
    # In Aperture Tag, we don't have portals. For fizzler types which block
    # portals (trigger_portal_cleanser), additionally fizzle paint.
    for fizz in FIZZ_TYPES.values():
        for brush in fizz.brushes:
            if brush.keys['classname'].casefold() == 'trigger_portal_cleanser':
                brush_name = brush.name
                # Retrieve what key is used for start-disabled.
                brush_start_disabled = None
                for key_map in [brush.keys, brush.local_keys]:
                    if brush_start_disabled is None:
                        for key, value in key_map.items():
                            if key.casefold() == 'startdisabled':
                                brush_start_disabled = value
                                break
                break  # Jump past else.
        else:
            # No fizzlers in this item.
            continue

        # Add a paint fizzler brush to these fizzlers.
        fizz.brushes.append(
            FizzlerBrush(
                brush_name,
                textures={
                    TexGroup.TRIGGER: const.Tools.TRIGGER,
                },
                keys={
                    'classname': 'trigger_paint_cleanser',
                    'startdisabled': brush_start_disabled or '0',
                    'spawnflags': '9',
                },
                local_keys={},
                outputs=[],
                singular=True,
            ))
Exemple #17
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
Exemple #18
0
def read_configs(conf: Property) -> None:
    """Read in the fizzler data."""
    for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'):
        fizz = FizzlerType.parse(fizz_conf)

        if fizz.id in FIZZ_TYPES:
            raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id))

        FIZZ_TYPES[fizz.id] = fizz

    LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES))

    if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']:
        return
    # In Aperture Tag, we don't have portals. For fizzler types which block
    # portals (trigger_portal_cleanser), additionally fizzle paint.
    for fizz in FIZZ_TYPES.values():
        for brush in fizz.brushes:
            if brush.keys['classname'].casefold() == 'trigger_portal_cleanser':
                brush_name = brush.name
                # Retrieve what key is used for start-disabled.
                brush_start_disabled = None
                for key_map in [brush.keys, brush.local_keys]:
                    if brush_start_disabled is None:
                        for key, value in key_map.items():
                            if key.casefold() == 'startdisabled':
                                brush_start_disabled = value
                                break
                break  # Jump past else.
        else:
            # No fizzlers in this item.
            continue

        # Add a paint fizzler brush to these fizzlers.
        fizz.brushes.append(FizzlerBrush(
            brush_name,
            textures={
                TexGroup.TRIGGER: const.Tools.TRIGGER,
            },
            keys={
                'classname': 'trigger_paint_cleanser',
                'startdisabled': brush_start_disabled or '0',
                'spawnflags': '9',
            },
            local_keys={},
            outputs=[],
            singular=True,
        ))
Exemple #19
0
def res_cust_output_setup(res: Property):
    conds = [
        Condition.parse(sub_res) for sub_res in res
        if sub_res.name == 'targcondition'
    ]
    outputs = list(res.find_all('addOut'))
    dec_con_count = srctools.conv_bool(res["decConCount", '0'], False)
    sign_type = IND_PANEL_TYPES.get(res['sign_type', None], None)

    if sign_type is None:
        sign_act = sign_deact = (None, '')
    else:
        # The outputs which trigger the sign.
        sign_act = Output.parse_name(res['sign_activate', ''])
        sign_deact = Output.parse_name(res['sign_deactivate', ''])

    return outputs, dec_con_count, conds, sign_type, sign_act, sign_deact
Exemple #20
0
 def parse(cls, prop: Property) -> 'AntType':
     """Parse this from a property block."""
     broken_chance = prop.float('broken_chance')
     tex_straight: List[AntTex] = []
     tex_corner: List[AntTex] = []
     brok_straight: List[AntTex] = []
     brok_corner: List[AntTex] = []
     for ant_list, name in zip(
         [tex_straight, tex_corner, brok_straight, brok_corner],
         ('straight', 'corner', 'broken_straight', 'broken_corner'),
     ):
         for sub_prop in prop.find_all(name):
             ant_list.append(AntTex.parse(sub_prop))
     return cls(
         tex_straight,
         tex_corner,
         brok_straight,
         brok_corner,
         broken_chance,
     )
Exemple #21
0
 def parse(cls, prop: Property):
     """Parse this from a property block."""
     broken_chance = prop.float('broken_chance')
     tex_straight = []
     tex_corner = []
     brok_straight = []
     brok_corner = []
     for ant_list, name in zip(
         [tex_straight, tex_corner, brok_straight, brok_corner],
         ('straight', 'corner', 'broken_straight', 'broken_corner'),
     ):
         for sub_prop in prop.find_all(name):
             ant_list.append(AntTex.parse(sub_prop))
     return cls(
         tex_straight,
         tex_corner,
         brok_straight,
         brok_corner,
         broken_chance,
     )
Exemple #22
0
def gen_part_manifest(additional):
    """Generate a new particle system manifest file.

    This includes all the current ones defined, plus any custom ones.
    """
    if not additional:
        return  # Don't pack, there aren't any new particles..

    orig_manifest = os.path.join(
        '..',
        GAME_FOLDER.get(CONF['game_id', ''], 'portal2'),
        'particles',
        'particles_manifest.txt',
    )

    try:
        with open(orig_manifest) as f:
            props = Property.parse(f, orig_manifest).find_key(
                'particles_manifest',
                [],
            )
    except FileNotFoundError:  # Assume no particles
        props = Property('particles_manifest', [])

    parts = [prop.value for prop in props.find_all('file')]

    for particle in additional:
        parts.append(particle)

    # Build and unbuild it to strip comments and similar lines.
    new_props = Property('particles_manifest',
                         [Property('file', file) for file in parts])

    inject_loc = os.path.join('bee2', 'inject', 'particles_manifest.txt')
    with open(inject_loc, 'w') as f:
        for line in new_props.export():
            f.write(line)

    LOGGER.info('Written new particles_manifest..')
Exemple #23
0
def desc_parse(
    info: Property,
    desc_id: str = '',
    *,
    prop_name: str = 'description',
) -> tkMarkdown.MarkdownData:
    """Parse the description blocks, to create data which matches richTextBox.

    """
    has_warning = False
    lines = []
    for prop in info.find_all(prop_name):
        if prop.has_children():
            for line in prop:
                if line.name and not has_warning:
                    LOGGER.warning('Old desc format: {}', desc_id)
                    has_warning = True
                lines.append(line.value)
        else:
            lines.append(prop.value)

    return tkMarkdown.convert('\n'.join(lines))
Exemple #24
0
    def parse(cls, prop: Property) -> AntType:
        """Parse this from a property block."""
        broken_chance = prop.float('broken_chance')
        tex_straight: list[AntTex] = []
        tex_corner: list[AntTex] = []
        brok_straight: list[AntTex] = []
        brok_corner: list[AntTex] = []
        for ant_list, name in zip(
            [tex_straight, tex_corner, brok_straight, brok_corner],
            ('straight', 'corner', 'broken_straight', 'broken_corner'),
        ):
            for sub_prop in prop.find_all(name):
                ant_list.append(AntTex.parse(sub_prop))

        if broken_chance < 0.0:
            LOGGER.warning('Antline broken chance must be between 0-100, got "{}"!', prop['broken_chance'])
            broken_chance = 0.0
        if broken_chance > 100.0:
            LOGGER.warning('Antline broken chance must be between 0-100, got "{}"!', prop['broken_chance'])
            broken_chance = 100.0

        if broken_chance == 0.0:
            brok_straight.clear()
            brok_corner.clear()

        # Cannot have broken corners if corners/straights are the same.
        if not tex_corner:
            brok_corner.clear()

        return cls(
            tex_straight,
            tex_corner,
            brok_straight,
            brok_corner,
            broken_chance,
        )
Exemple #25
0
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None:
    """Implements SetPanelOptions and CreatePanel."""
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))
    normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6)
    origin = Vec.from_str(inst['origin'])
    uaxis, vaxis = Vec.INV_AXIS[normal.axis()]

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

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

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

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

    # If bevels is provided, parse out the overall world positions.
    bevel_world: set[tuple[int, int]] | None
    try:
        bevel_prop = props.find_key('bevel')
    except NoKeyError:
        bevel_world = None
    else:
        bevel_world = set()
        if bevel_prop.has_children():
            # Individually specifying offsets.
            for bevel_str in bevel_prop.as_array():
                bevel_point = Vec.from_str(bevel_str) @ orient + origin
                bevel_world.add(
                    (int(bevel_point[uaxis]), int(bevel_point[vaxis])))
        elif srctools.conv_bool(bevel_prop.value):
            # Fill the bounding box.
            bbox_min, bbox_max = Vec.bbox(map(Vec, points))
            off = Vec.with_axes(uaxis, 32, vaxis, 32)
            bbox_min -= off
            bbox_max += off
            for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
                if pos.as_tuple() not in points:
                    bevel_world.add((pos[uaxis], pos[vaxis]))
        # else: No bevels.
    panels: list[tiling.Panel] = []

    for tile, uvs in tiles_to_uv.items():
        if create:
            panel = tiling.Panel(
                None,
                inst,
                tiling.PanelType.NORMAL,
                thickness=4,
                bevels=(),
            )
            panel.points = uvs
            tile.panels.append(panel)
        else:
            for panel in tile.panels:
                if panel.same_item(inst) and panel.points == uvs:
                    break
            else:
                LOGGER.warning('No panel to modify found for "{}"!',
                               inst['targetname'])
                continue
        panels.append(panel)

        pan_type = '<nothing?>'
        try:
            pan_type = conditions.resolve_value(inst, props['type'])
            panel.pan_type = tiling.PanelType(pan_type.lower())
        except LookupError:
            pass
        except ValueError:
            raise ValueError('Unknown panel type "{}"!'.format(pan_type))

        if 'thickness' in props:
            panel.thickness = srctools.conv_int(
                conditions.resolve_value(inst, props['thickness']))
            if panel.thickness not in (2, 4, 8):
                raise ValueError(
                    '"{}": Invalid panel thickess {}!\n'
                    'Must be 2, 4 or 8.',
                    inst['targetname'],
                    panel.thickness,
                )

        if bevel_world is not None:
            panel.bevels.clear()
            for u, v in bevel_world:
                # Convert from world points to UV positions.
                u = (u - tile.pos[uaxis] + 48) // 32
                v = (v - tile.pos[vaxis] + 48) // 32
                # Cull outside here, we wont't use them.
                if -1 <= u <= 4 and -1 <= v <= 4:
                    panel.bevels.add((u, v))

        if 'offset' in props:
            panel.offset = conditions.resolve_offset(inst, props['offset'])
            panel.offset -= Vec.from_str(inst['origin'])
        if 'template' in props:
            # We only want the template inserted once. So remove it from all but one.
            if len(panels) == 1:
                panel.template = inst.fixup.substitute(props['template'])
            else:
                panel.template = ''
        if 'nodraw' in props:
            panel.nodraw = srctools.conv_bool(
                inst.fixup.substitute(props['nodraw'], allow_invert=True))
        if 'seal' in props:
            panel.seal = srctools.conv_bool(
                inst.fixup.substitute(props['seal'], allow_invert=True))
        if 'move_bullseye' in props:
            panel.steals_bullseye = srctools.conv_bool(
                inst.fixup.substitute(props['move_bullseye'],
                                      allow_invert=True))
    if 'keys' in props or 'localkeys' in props:
        # First grab the existing ent, so we can edit it.
        # These should all have the same value, unless they were independently
        # edited with mismatching point sets. In that case destroy all those existing ones.
        existing_ents: set[Entity
                           | None] = {panel.brush_ent
                                      for panel in panels}
        try:
            [brush_ent] = existing_ents
        except ValueError:
            LOGGER.warning(
                'Multiple independent panels for "{}" were made, then the '
                'brush entity was edited as a group! Discarding '
                'individual ents...', inst['targetname'])
            for brush_ent in existing_ents:
                if brush_ent is not None and brush_ent in vmf.entities:
                    brush_ent.remove()
            brush_ent = None

        if brush_ent is None:
            brush_ent = vmf.create_ent('')

        old_pos = brush_ent.keys.pop('origin', None)

        conditions.set_ent_keys(brush_ent, inst, props)
        if not brush_ent['classname']:
            if create:  # This doesn't make sense, you could just omit the prop.
                LOGGER.warning(
                    'No classname provided for panel "{}"!',
                    inst['targetname'],
                )
            # Make it a world brush.
            brush_ent.remove()
            brush_ent = None
        else:
            # We want to do some post-processing.
            # Localise any origin value.
            if 'origin' in brush_ent.keys:
                pos = Vec.from_str(brush_ent['origin'])
                pos.localise(
                    Vec.from_str(inst['origin']),
                    Angle.from_str(inst['angles']),
                )
                brush_ent['origin'] = pos
            elif old_pos is not None:
                brush_ent['origin'] = old_pos

            # If it's func_detail, clear out all the keys.
            # Particularly `origin`, but the others are useless too.
            if brush_ent['classname'] == 'func_detail':
                brush_ent.clear_keys()
                brush_ent['classname'] = 'func_detail'
        for panel in panels:
            panel.brush_ent = brush_ent
Exemple #26
0
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property):
    """Convert a fizzler connected via the output to a new shape.

    This allows for different placing of fizzler items.
    Each `segment` parameter should be a `x y z;x y z` pair of positions
    that represent the ends of the fizzler.
    `up_axis` should be set to a normal vector pointing in the new 'upward'
    direction.
    `default` is the ID of a fizzler type which should be used if no outputs
    are fired.
    """
    shape_name = shape_inst['targetname']
    shape_item = connections.ITEMS[shape_name]

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

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

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

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

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

    for seg_prop in res.find_all('Segment'):
        vec1, vec2 = seg_prop.value.split(';')
        seg_min_max = Vec.bbox(
            Vec.from_str(vec1).rotate(*shape_angles) + origin,
            Vec.from_str(vec2).rotate(*shape_angles) + origin,
        )
        fizz.emitters.append(seg_min_max)
Exemple #27
0
    def parse(item_id: str, conf: Property):
        """Read the item type info from the given config."""

        def get_outputs(prop_name):
            """Parse all the outputs with this name."""
            return [
                Output.parse(prop)
                for prop in
                conf.find_all(prop_name)
                # Allow blank to indicate no output.
                if prop.value != ''
            ]

        enable_cmd = get_outputs('enable_cmd')
        disable_cmd = get_outputs('disable_cmd')
        lock_cmd = get_outputs('lock_cmd')
        unlock_cmd = get_outputs('unlock_cmd')

        inf_lock_only = conf.bool('inf_lock_only')

        timer_done_cmd = get_outputs('timer_done_cmd')
        if 'timer_sound_pos' in conf:
            timer_sound_pos = conf.vec('timer_sound_pos')
            force_timer_sound = conf.bool('force_timer_sound')
        else:
            timer_sound_pos = None
            force_timer_sound = False

        try:
            input_type = InputType(
                conf['Type', 'default'].casefold()
            )
        except ValueError:
            raise ValueError('Invalid input type "{}": {}'.format(
                item_id, conf['type'],
            )) from None

        invert_var = conf['invertVar', '0']

        try:
            spawn_fire = FeatureMode(conf['spawnfire', 'never'].casefold())
        except ValueError:
            # Older config option - it was a bool for always/never.
            spawn_fire_bool = conf.bool('spawnfire', None)
            if spawn_fire_bool is None:
                raise  # Nope, not a bool.

            spawn_fire = FeatureMode.ALWAYS if spawn_fire_bool else FeatureMode.NEVER

        try:
            sec_spawn_fire = FeatureMode(conf['sec_spawnfire', 'never'].casefold())
        except ValueError:  # Default to primary value.
            sec_spawn_fire = FeatureMode.NEVER

        if input_type is InputType.DUAL:
            sec_enable_cmd = get_outputs('sec_enable_cmd')
            sec_disable_cmd = get_outputs('sec_disable_cmd')

            try:
                default_dual = CONN_TYPE_NAMES[
                    conf['Default_Dual', 'primary'].casefold()
                ]
            except KeyError:
                raise ValueError('Invalid default type for "{}": {}'.format(
                    item_id, conf['Default_Dual'],
                )) from None

            # We need an affinity to use when nothing else specifies it.
            if default_dual is ConnType.DEFAULT:
                raise ValueError('Must specify a default type for "{}"!'.format(
                    item_id,
                )) from None

            sec_invert_var = conf['sec_invertVar', '0']
        else:
            # No dual type, set to dummy values.
            sec_enable_cmd = []
            sec_disable_cmd = []
            default_dual = ConnType.DEFAULT
            sec_invert_var = ''

        try:
            output_type = CONN_TYPE_NAMES[
                conf['DualType', 'default'].casefold()
            ]
        except KeyError:
            raise ValueError('Invalid output affinity for "{}": {}'.format(
                item_id, conf['DualType'],
            )) from None

        def get_input(prop_name: str):
            """Parse an input command."""
            try:
                return Output.parse_name(conf[prop_name])
            except IndexError:
                return None

        out_act = get_input('out_activate')
        out_deact = get_input('out_deactivate')
        out_lock = get_input('out_lock')
        out_unlock = get_input('out_unlock')

        timer_start = timer_stop = None
        if 'out_timer_start' in conf:
            timer_start = [
                Output.parse_name(prop.value)
                for prop in conf.find_all('out_timer_start')
                if prop.value
            ]
        if 'out_timer_stop' in conf:
            timer_stop = [
                Output.parse_name(prop.value)
                for prop in conf.find_all('out_timer_stop')
                if prop.value
            ]

        return Config(
            item_id, default_dual, input_type,
            spawn_fire, invert_var, enable_cmd, disable_cmd,
            sec_spawn_fire, sec_invert_var, sec_enable_cmd, sec_disable_cmd,
            output_type, out_act, out_deact,
            lock_cmd, unlock_cmd, out_lock, out_unlock, inf_lock_only,
            timer_sound_pos, timer_done_cmd, force_timer_sound,
            timer_start, timer_stop,
        )
Exemple #28
0
def res_add_shuffle_group(vmf: VMF, res: Property) -> Callable[[Entity], None]:
    """Pick from a pool of instances to randomise decoration.

    For each sub-condition that succeeds, a random instance is placed, with
    a fixup set to a value corresponding to the condition.

    Parameters:
        - Var: The fixup variable to set on each item. This is used to tweak it
          to match the condition.
        - Conditions: Each value here is the value to produce if this instance
          is required. The contents of the block is then a condition flag to
          check.
        - Pool: A list of instances to randomly allocate to the conditions. There
          should be at least as many pool values as there are conditions.
        - Seed: Value to modify the seed with before placing.
    """
    conf_variable = res['var']
    conf_seed = 'sg' + res['seed', '']
    conf_pools: dict[str, list[str]] = {}
    for prop in res.find_children('pool'):
        if prop.has_children():
            raise ValueError('Instances in pool cannot be a property block!')
        conf_pools.setdefault(prop.name, []).append(prop.value)

    # (flag, value, pools)
    conf_selectors: list[tuple[list[Property], str, frozenset[str]]] = []
    for prop in res.find_all('selector'):
        conf_value = prop['value', '']
        conf_flags = list(prop.find_children('conditions'))
        try:
            picked_pools = prop['pools'].casefold().split()
        except LookupError:
            picked_pools = frozenset(conf_pools)
        else:
            for pool_name in picked_pools:
                if pool_name not in conf_pools:
                    raise ValueError(f'Unknown pool name {pool_name}!')
        conf_selectors.append(
            (conf_flags, conf_value, frozenset(picked_pools)))

    all_pools = [(name, inst) for name, instances in conf_pools.items()
                 for inst in instances]
    all_pools.sort()  # Ensure consistent order.

    def add_group(inst: Entity) -> None:
        """Place the group."""
        rng = rand.seed(b'shufflegroup', conf_seed, inst)
        pools = all_pools.copy()
        for (flags, value, potential_pools) in conf_selectors:
            for flag in flags:
                if not conditions.check_flag(vmf, flag, inst):
                    break
            else:  # Succeeded.
                allowed_inst = [(name, inst) for (name, inst) in pools
                                if name in potential_pools]
                name, filename = rng.choice(allowed_inst)
                pools.remove((name, filename))
                vmf.create_ent(
                    'func_instance',
                    targetname=inst['targetname'],
                    file=filename,
                    angles=inst['angles'],
                    origin=inst['origin'],
                    fixup_style='0',
                ).fixup[conf_variable] = value

    return add_group
Exemple #29
0
def res_fix_rotation_axis(ent: Entity, res: Property):
    """Generate a `func_rotating`, `func_door_rotating` or any similar entity.

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

    Permitted entities:
       * `func_rotating`
       * `func_door_rotating`
       * `func_rot_button`
       * `func_platrot`
    """
    des_axis = res['axis', 'z'].casefold()
    reverse = srctools.conv_bool(res['reversed', '0'])
    door_type = res['classname', 'func_door_rotating']

    # Extra stuff to apply to the flags (USE, toggle, etc)
    flags = sum(map(
        # Add together multiple values
        srctools.conv_int,
        res['flags', '0'].split('+')
    ))

    name = conditions.local_name(ent, res['name', ''])

    axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0'])

    pos = Vec.from_str(
        res['Pos', '0 0 0']
    ).rotate_by_str(ent['angles', '0 0 0'])
    pos += Vec.from_str(ent['origin', '0 0 0'])

    door_ent = vbsp.VMF.create_ent(
        classname=door_type,
        targetname=name,
        origin=pos.join(' '),
    )

    conditions.set_ent_keys(door_ent, ent, res)

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

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

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

    flag_values = FLAG_ROTATING[door_type]
    # Make the door always non-solid!
    flags |= flag_values.get('solid_flags', 0)
    # Add or remove flags as needed.
    # flags |= bit sets it to 1.
    # flags |= ~bit sets it to 0.
    if axis.x != 0:
        flags |= flag_values.get('x', 0)
    else:
        flags &= ~flag_values.get('x', 0)

    if axis.y != 0:
        flags |= flag_values.get('y', 0)
    else:
        flags &= ~flag_values.get('y', 0)

    if axis.z != 0:
        flags |= flag_values.get('z', 0)
    else:
        flags &= ~flag_values.get('z', 0)

    if door_type == 'momentary_rot_button':
        door_ent['startdirection'] = '1' if reverse else '-1'
    else:
        if reverse:
            flags |= flag_values.get('rev', 0)
        else:
            flags &= ~flag_values.get('rev', 0)
    door_ent['spawnflags'] = str(flags)
Exemple #30
0
def find_group_quotes(
    vmf: VMF,
    group: Property,
    mid_quotes,
    allow_mid_voices,
    use_dings,
    conf,
    mid_name: str,
    player_flag_set: Set[str],
) -> Iterator[PossibleQuote]:
    """Scan through a group, looking for applicable quote options."""
    is_mid = (group.name == 'midchamber')

    if is_mid:
        group_id = 'MIDCHAMBER'
    else:
        group_id = group['name'].upper()

    all_quotes = list(group.find_all('quote'))
    valid_quotes = 0

    for quote in all_quotes:
        valid_quote = True
        for flag in quote:
            name = flag.name
            if name in ('priority', 'name', 'id',
                        'line') or name.startswith('line_'):
                # Not flags!
                continue
            if not conditions.check_flag(vmf, flag, fake_inst):
                valid_quote = False
                break

        if not valid_quote:
            continue

        valid_quotes += 1

        poss_quotes = []
        line_mid_quotes = []
        for line in mode_quotes(quote, player_flag_set):
            line_id = line['id', line['name', '']].casefold()

            # Check if the ID is enabled!
            if conf.get_bool(group_id, line_id, True):
                if allow_mid_voices and is_mid:
                    line_mid_quotes.append((line, use_dings, mid_name))
                else:
                    poss_quotes.append(line)
            else:
                LOGGER.info(
                    'Line "{}" is disabled..',
                    line['name', '??'],
                )

        if line_mid_quotes:
            mid_quotes.append(line_mid_quotes)

        if poss_quotes:
            yield PossibleQuote(
                quote['priority', '0'],
                poss_quotes,
            )

    LOGGER.info('"{}": {}/{} quotes..', group_id, valid_quotes,
                len(all_quotes))
Exemple #31
0
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None:
    """Create a conveyor belt.

    * Options:
        * `SegmentInst`: Generated at each square. (`track` is the name of the
          path to attach to.)
        * `TrackTeleport`: Set the track points so they teleport trains to the start.
        * `Speed`: The fixup or number for the train speed.
        * `MotionTrig`: If set, a trigger_multiple will be spawned that
          `EnableMotion`s weighted cubes. The value is the name of the relevant filter.
        * `EndOutput`: Adds an output to the last track. The value is the same as
          outputs in VMFs.
        `RotateSegments`: If true (default), force segments to face in the
          direction of movement.
        * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam
          travelling from start to end. The origin is treated specially - X is
          the distance from walls, y is the distance to the side, and z is the
          height.
        `RailTemplate`: A template for the track sections. This is made into a
          non-solid func_brush, combining all sections.
        * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor
          under the track.
        * `PaintFizzler`: If set, add a paint fizzler underneath the belt.
    """
    move_dist = inst.fixup.int('$travel_distance')

    if move_dist <= 2:
        # There isn't room for a conveyor, so don't bother.
        inst.remove()
        return

    orig_orient = Matrix.from_angle(Angle.from_str(inst['angles']))
    move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction'])
    move_dir = move_dir @ orig_orient
    start_offset = inst.fixup.float('$starting_position')
    teleport_to_start = res.bool('TrackTeleport', True)
    segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', ''])
    rail_template = res['RailTemplate', None]

    track_speed = res['speed', None]

    start_pos = Vec.from_str(inst['origin'])
    end_pos = start_pos + move_dist * move_dir

    if start_offset > 0:
        # If an oscillating platform, move to the closest side..
        offset = start_offset * move_dir
        # The instance is placed this far along, so move back to the end.
        start_pos -= offset
        end_pos -= offset
        if start_offset > 0.5:
            # Swap the direction of movement..
            start_pos, end_pos = end_pos, start_pos
        inst['origin'] = start_pos

    norm = orig_orient.up()

    if res.bool('rotateSegments', True):
        orient = Matrix.from_basis(x=move_dir, z=norm)
        inst['angles'] = orient.to_angle()
    else:
        orient = orig_orient

    # Add the EnableMotion trigger_multiple seen in platform items.
    # This wakes up cubes when it starts moving.
    motion_filter = res['motionTrig', None]

    # Disable on walls, or if the conveyor can't be turned on.
    if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0':
        motion_filter = None

    track_name = conditions.local_name(inst, 'segment_{}')
    rail_temp_solids = []
    last_track = None
    # Place tracks at the top, so they don't appear inside wall sections.
    track_start: Vec = start_pos + 48 * norm
    track_end: Vec = end_pos + 48 * norm
    for index, pos in enumerate(track_start.iter_line(track_end, stride=128),
                                start=1):
        track = vmf.create_ent(
            classname='path_track',
            targetname=track_name.format(index) + '-track',
            origin=pos,
            spawnflags=0,
            orientationtype=0,  # Don't rotate
        )
        if track_speed is not None:
            track['speed'] = track_speed
        if last_track:
            last_track['target'] = track['targetname']

        if index == 1 and teleport_to_start:
            track['spawnflags'] = 16  # Teleport here..

        last_track = track

        # Don't place at the last point - it doesn't teleport correctly,
        # and would be one too many.
        if segment_inst_file and pos != track_end:
            seg_inst = conditions.add_inst(
                vmf,
                targetname=track_name.format(index),
                file=segment_inst_file,
                origin=pos,
                angles=orient,
            )
            seg_inst.fixup.update(inst.fixup)

        if rail_template:
            temp = template_brush.import_template(
                vmf,
                rail_template,
                pos,
                orient,
                force_type=template_brush.TEMP_TYPES.world,
                add_to_map=False,
            )
            rail_temp_solids.extend(temp.world)

    if rail_temp_solids:
        vmf.create_ent(
            classname='func_brush',
            origin=track_start,
            spawnflags=1,  # Ignore +USE
            solidity=1,  # Not solid
            vrad_brush_cast_shadows=1,
            drawinfastreflection=1,
        ).solids = rail_temp_solids

    if teleport_to_start:
        # Link back to the first track..
        last_track['target'] = track_name.format(1) + '-track'

    # Generate an env_beam pointing from the start to the end of the track.
    try:
        beam_keys = res.find_key('BeamKeys')
    except LookupError:
        pass
    else:
        beam = vmf.create_ent(classname='env_beam')

        beam_off = beam_keys.vec('origin', 0, 63, 56)

        for prop in beam_keys:
            beam[prop.real_name] = prop.value

        # Localise the targetname so it can be triggered..
        beam['LightningStart'] = beam['targetname'] = conditions.local_name(
            inst, beam['targetname', 'beam'])
        del beam['LightningEnd']
        beam['origin'] = start_pos + Vec(
            -beam_off.x,
            beam_off.y,
            beam_off.z,
        ) @ orient
        beam['TargetPoint'] = end_pos + Vec(
            +beam_off.x,
            beam_off.y,
            beam_off.z,
        ) @ orient

    # Allow adding outputs to the last path_track.
    for prop in res.find_all('EndOutput'):
        output = Output.parse(prop)
        output.output = 'OnPass'
        output.inst_out = None
        output.comma_sep = False
        output.target = conditions.local_name(inst, output.target)
        last_track.add_out(output)

    if motion_filter is not None:
        motion_trig = vmf.create_ent(
            classname='trigger_multiple',
            targetname=conditions.local_name(inst, 'enable_motion_trig'),
            origin=start_pos,
            filtername=motion_filter,
            startDisabled=1,
            wait=0.1,
        )
        motion_trig.add_out(
            Output('OnStartTouch', '!activator', 'ExitDisabledState'))
        # Match the size of the original...
        motion_trig.solids.append(
            vmf.make_prism(
                start_pos + Vec(72, -56, 58) @ orient,
                end_pos + Vec(-72, 56, 144) @ orient,
                mat=consts.Tools.TRIGGER,
            ).solid)

    if res.bool('NoPortalFloor'):
        # Block portals on the floor..
        floor_noportal = vmf.create_ent(
            classname='func_noportal_volume',
            origin=track_start,
        )
        floor_noportal.solids.append(
            vmf.make_prism(
                start_pos + Vec(-60, -60, -66) @ orient,
                end_pos + Vec(60, 60, -60) @ orient,
                mat=consts.Tools.INVISIBLE,
            ).solid)

    # A brush covering under the platform.
    base_trig = vmf.make_prism(
        start_pos + Vec(-64, -64, 48) @ orient,
        end_pos + Vec(64, 64, 56) @ orient,
        mat=consts.Tools.INVISIBLE,
    ).solid

    vmf.add_brush(base_trig)

    # Make a paint_cleanser under the belt..
    if res.bool('PaintFizzler'):
        pfizz = vmf.create_ent(
            classname='trigger_paint_cleanser',
            origin=start_pos,
        )
        pfizz.solids.append(base_trig.copy())
        for face in pfizz.sides():
            face.mat = consts.Tools.TRIGGER
Exemple #32
0
    def build_instance_data(editoritems: Property):
        """Build a property tree listing all of the instances for each item.
        as well as another listing the input and output commands.
        VBSP uses this to reduce duplication in VBSP_config files.

        This additionally strips custom instance definitions from the original
        list.
        """
        instance_locs = Property("AllInstances", [])
        cust_inst = Property("CustInstances", [])
        commands = Property("Connections", [])
        item_classes = Property("ItemClasses", [])
        root_block = Property(None, [instance_locs, item_classes, cust_inst, commands])

        for item in editoritems.find_all("Item"):
            instance_block = Property(item["Type"], [])
            instance_locs.append(instance_block)

            comm_block = Property(item["Type"], [])

            for inst_block in item.find_all("Exporting", "instances"):
                for inst in inst_block.value[:]:  # type: Property
                    if inst.name.isdigit():
                        # Direct Portal 2 value
                        instance_block.append(Property("Instance", inst["Name"]))
                    else:
                        # It's a custom definition, remove from editoritems
                        inst_block.value.remove(inst)

                        # Allow the name to start with 'bee2_' also to match
                        # the <> definitions - it's ignored though.
                        name = inst.name
                        if name[:5] == "bee2_":
                            name = name[5:]

                        cust_inst.set_key(
                            (item["type"], name),
                            # Allow using either the normal block format,
                            # or just providing the file - we don't use the
                            # other values.
                            inst["name"] if inst.has_children() else inst.value,
                        )

            # Look in the Inputs and Outputs blocks to find the io definitions.
            # Copy them to property names like 'Input_Activate'.
            for io_type in ("Inputs", "Outputs"):
                for block in item.find_all("Exporting", io_type, CONN_NORM):
                    for io_prop in block:
                        comm_block[io_type[:-1] + "_" + io_prop.real_name] = io_prop.value

            # The funnel item type is special, having the additional input type.
            # Handle that specially.
            if item["type"] == "item_tbeam":
                for block in item.find_all("Exporting", "Inputs", CONN_FUNNEL):
                    for io_prop in block:
                        comm_block["TBEAM_" + io_prop.real_name] = io_prop.value

            # Fizzlers don't work correctly with outputs. This is a signal to
            # conditions.fizzler, but it must be removed in editoritems.
            if item["ItemClass", ""].casefold() == "itembarrierhazard":
                for block in item.find_all("Exporting", "Outputs"):
                    if CONN_NORM in block:
                        del block[CONN_NORM]

            # Record the itemClass for each item type.
            item_classes[item["type"]] = item["ItemClass", "ItemBase"]

            # Only add the block if the item actually has IO.
            if comm_block.value:
                commands.append(comm_block)

        return root_block.export()
Exemple #33
0
def res_cust_fizzler(base_inst: Entity, res: Property):
    """Customises the various components of a custom fizzler item.

    This should be executed on the base instance. Brush and MakeLaserField
    are not permitted on laserfield barriers.
    When executed, the $is_laser variable will be set on the base.
    Options:
        * ModelName: sets the targetname given to the model instances.
        * UniqueModel: If true, each model instance will get a suffix to
            allow unique targetnames.
        * Brush: A brush entity that will be generated (the original is
         deleted.) This cannot be used on laserfields.
            * Name is the instance name for the brush
            * Left/Right/Center/Short/Nodraw are the textures used
            * Keys are a block of keyvalues to be set. Targetname and
              Origin are auto-set.
            * Thickness will change the thickness of the fizzler if set.
              By default it is 2 units thick.
            * Outputs is a block of outputs (laid out like in VMFs). The
              targetnames will be localised to the instance.
            * MergeBrushes, if true will merge this brush set into one
              entity for each fizzler. This is useful for non-fizzlers to
              reduce the entity count.
            * SimplifyBrush, if true will merge the three parts into one brush.
              All sides will receive the "nodraw" texture at 0.25 scale.
            * MaterialModify generates material_modify_controls to control
              the brush. One is generated for each texture used in the brush.
              This has subkeys 'name' and 'var' - the entity name and shader
              variable to be modified. MergeBrushes must be enabled if this
              is present.
        * MakeLaserField generates a brush stretched across the whole
          area.
            * Name, keys and thickness are the same as the regular Brush.
            * Texture/Nodraw are the textures.
            * Width is the pixel width of the laser texture, used to
              scale it correctly.
    """
    model_name = res['modelname', None]
    make_unique = res.bool('UniqueModel')
    fizz_name = base_inst['targetname', '']

    # search for the model instances
    model_targetnames = (
        fizz_name + '_modelStart',
        fizz_name + '_modelEnd',
        )
    is_laser = False
    for inst in vbsp.VMF.by_class['func_instance']:
        if inst['targetname'] in model_targetnames:
            if inst.fixup['skin', '0'] == '2':
                is_laser = True
            if model_name is not None:
                if model_name == '':
                    inst['targetname'] = base_inst['targetname']
                else:
                    inst['targetname'] = (
                        base_inst['targetname'] +
                        '-' +
                        model_name
                    )
            if make_unique:
                inst.make_unique()

            for key, value in base_inst.fixup.items():
                inst.fixup[key] = value

    base_inst.fixup['$is_laser'] = is_laser

    new_brush_config = list(res.find_all('brush'))
    if len(new_brush_config) == 0:
        return  # No brush modifications

    if is_laser:
        # This is a laserfield! We can't edit those brushes!
        LOGGER.warning('CustFizzler executed on LaserField!')
        return

    # Record which materialmodify controls are used, so we can add if needed.
    # Conf id -> (brush_name, conf, [textures])
    modify_controls = {}

    for orig_brush in (
            vbsp.VMF.by_class['trigger_portal_cleanser'] &
            vbsp.VMF.by_target[fizz_name + '_brush']):
        orig_brush.remove()
        for config in new_brush_config:

            new_brush = orig_brush.copy()
            # Unique to the particular config property & fizzler name
            conf_key = (id(config), fizz_name)

            if config.bool('SimplifyBrush'):
                # Replace the brush with a simple one of the same size.
                bbox_min, bbox_max = new_brush.get_bbox()
                new_brush.solids = [vbsp.VMF.make_prism(
                    bbox_min, bbox_max,
                    mat=const.Tools.NODRAW,
                ).solid]

            should_merge = config.bool('MergeBrushes')
            if should_merge and conf_key in FIZZ_BRUSH_ENTS:
                # These are shared by both ents, but new_brush won't be added to
                # the map. (We need it though for the widening code to work).
                FIZZ_BRUSH_ENTS[conf_key].solids.extend(new_brush.solids)
            else:
                vbsp.VMF.add_ent(new_brush)
                # Don't allow restyling it
                vbsp.IGNORED_BRUSH_ENTS.add(new_brush)

                new_brush.clear_keys()  # Wipe the original keyvalues
                new_brush['origin'] = orig_brush['origin']
                new_brush['targetname'] = conditions.local_name(
                    base_inst,
                    config['name'],
                )
                # All ents must have a classname!
                new_brush['classname'] = 'trigger_portal_cleanser'

                conditions.set_ent_keys(
                    new_brush, base_inst,
                    config,
                )

                for out_prop in config.find_children('Outputs'):
                    out = Output.parse(out_prop)
                    out.comma_sep = False
                    out.target = conditions.local_name(
                        base_inst,
                        out.target
                    )
                    new_brush.add_out(out)

                if should_merge:  # The first brush...
                    FIZZ_BRUSH_ENTS[conf_key] = new_brush

            mat_mod_conf = config.find_key('MaterialModify', [])
            if mat_mod_conf:
                try:
                    used_materials = modify_controls[id(mat_mod_conf)][2]
                except KeyError:
                    used_materials = set()
                    modify_controls[id(mat_mod_conf)] = (
                        new_brush['targetname'],
                        mat_mod_conf,
                        used_materials
                    )
                # It can only parent to one brush, so it can't attach
                # to them all properly.
                if not should_merge:
                    raise Exception(
                        "MaterialModify won't work without MergeBrushes!"
                    )
            else:
                used_materials = None

            laserfield_conf = config.find_key('MakeLaserField', None)
            if laserfield_conf.value is not None:
                # Resize the brush into a laserfield format, without
                # the 128*64 parts. If the brush is 128x128, we can
                # skip the resizing since it's already correct.
                laser_tex = laserfield_conf['texture', const.Special.LASERFIELD]
                nodraw_tex = laserfield_conf['nodraw', const.Tools.NODRAW]
                tex_width = laserfield_conf.int('texwidth', 512)
                is_short = False
                for side in new_brush.sides():
                    if side == const.Fizzler.SHORT:
                        is_short = True
                        break

                if is_short:
                    for side in new_brush.sides():
                        if side == const.Fizzler.SHORT:
                            side.mat = laser_tex

                            side.uaxis.offset = 0
                            side.scale = 0.25
                        else:
                            side.mat = nodraw_tex
                else:
                    # The hard part - stretching the brush.
                    convert_to_laserfield(
                        new_brush,
                        laser_tex,
                        nodraw_tex,
                        tex_width,
                    )
                if used_materials is not None:
                    used_materials.add(laser_tex.casefold())
            else:
                # Just change the textures
                for side in new_brush.sides():
                    try:
                        tex_cat = TEX_FIZZLER[side.mat.casefold()]
                        side.mat = config[tex_cat]
                    except (KeyError, IndexError):
                        # If we fail, just use the original textures
                        pass
                    else:
                        if used_materials is not None and tex_cat != 'nodraw':
                            used_materials.add(side.mat.casefold())

            widen_amount = config.float('thickness', 2.0)
            if widen_amount != 2:
                for brush in new_brush.solids:
                    conditions.widen_fizz_brush(
                        brush,
                        thickness=widen_amount,
                    )

    for brush_name, config, textures in modify_controls.values():
        skip_if_static = config.bool('dynamicOnly', True)
        if skip_if_static and base_inst.fixup['$connectioncount'] == '0':
            continue
        mat_mod_name = config['name', 'modify']
        var = config['var', '$outputintensity']
        if not var.startswith('$'):
            var = '$' + var
        for tex in textures:
            vbsp.VMF.create_ent(
                classname='material_modify_control',
                origin=base_inst['origin'],
                targetname=conditions.local_name(base_inst, mat_mod_name),
                materialName='materials/' + tex + '.vmt',
                materialVar=var,
                parentname=brush_name,
            )
Exemple #34
0
def res_conveyor_belt(inst: Entity, res: Property):
    """Create a conveyor belt.

    Options:
        SegmentInst: Generated at each square. ('track' is the name of the path.)
        TrackTeleport: Set the track points so they teleport trains to the start.
        Speed: The fixup or number for the train speed.
        MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions
          weighted cubes. The value is the name of the relevant filter.
        EndOutput: Adds an output to the last track. The value is the same as
          outputs in VMFs.
        RotateSegments: If true (default), force segments to face in the
          direction of movement
        RailTemplate: A template for the railings. This is made into a non-solid
          func_brush, combining all sections.
    """
    move_dist = srctools.conv_int(inst.fixup['$travel_distance'])

    if move_dist <= 2:
        # There isn't room for a catwalk, so don't bother.
        inst.remove()
        return

    move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction'])
    move_dir.rotate_by_str(inst['angles'])
    start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0)
    teleport_to_start = res.bool('TrackTeleport', True)
    segment_inst_file = res['SegmentInst', '']
    rail_template = res['RailTemplate', None]

    vmf = inst.map

    if segment_inst_file:
        segment_inst_file = conditions.resolve_inst(segment_inst_file)[0]

    track_speed = res['speed', None]

    start_pos = Vec.from_str(inst['origin'])
    end_pos = start_pos + move_dist * move_dir

    if start_offset > 0:
        # If an oscillating platform, move to the closest side..
        offset = start_offset * move_dir
        # The instance is placed this far along, so move back to the end.
        start_pos -= offset
        end_pos -= offset
        if start_offset > 0.5:
            # Swap the direction of movement..
            start_pos, end_pos = end_pos, start_pos
        inst['origin'] = start_pos

    # Find the angle which generates an instance pointing in the direction
    # of movement, with the same normal.
    norm = Vec(z=1).rotate_by_str(inst['angles'])
    for roll in range(0, 360, 90):
        angles = move_dir.to_angle(roll)
        if Vec(z=1).rotate(*angles) == norm:
            break
    else:
        raise ValueError(
            "Can't find angles to give a"
            ' z={} and x={}!'.format(norm, move_dir)
        )

    if res.bool('rotateSegments', True):
        inst['angles'] = angles
    else:
        angles = Vec.from_str(inst['angles'])

    # Add the EnableMotion trigger_multiple seen in platform items.
    # This wakes up cubes when it starts moving.
    motion_filter = res['motionTrig', None]

    # Disable on walls, or if the conveyor can't be turned on.
    if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0':
        motion_filter = None

    track_name = conditions.local_name(inst, 'segment_{}')
    rail_temp_solids = []
    last_track = None
    # Place beams at the top, so they don't appear inside wall sections.
    beam_start = start_pos + 48 * norm  # type: Vec
    beam_end = end_pos + 48 * norm  # type: Vec
    for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1):
        track = vmf.create_ent(
            classname='path_track',
            targetname=track_name.format(index) + '-track',
            origin=pos,
            spawnflags=0,
            orientationtype=0,  # Don't rotate
        )
        if track_speed is not None:
            track['speed'] = track_speed
        if last_track:
            last_track['target'] = track['targetname']

        if index == 1 and teleport_to_start:
            track['spawnflags'] = 16  # Teleport here..

        last_track = track

        # Don't place at the last point - it doesn't teleport correctly,
        # and would be one too many.
        if segment_inst_file and pos != end_pos:
            seg_inst = vmf.create_ent(
                classname='func_instance',
                targetname=track_name.format(index),
                file=segment_inst_file,
                origin=pos,
                angles=angles,
            )
            seg_inst.fixup.update(inst.fixup)

        if rail_template:
            temp = conditions.import_template(
                rail_template,
                pos,
                angles,
                force_type=conditions.TEMP_TYPES.world,
                add_to_map=False,
            )
            rail_temp_solids.extend(temp.world)

    if rail_temp_solids:
        vmf.create_ent(
            classname='func_brush',
            origin=beam_start,
            spawnflags=1,  # Ignore +USE
            solidity=1,  # Not solid
            vrad_brush_cast_shadows=1,
            drawinfastreflection=1,
        ).solids = rail_temp_solids

    if teleport_to_start:
        # Link back to the first track..
        last_track['target'] = track_name.format(1) + '-track'

    # Generate an env_beam pointing from the start to the end of the track.
    beam_keys = res.find_key('BeamKeys', [])
    if beam_keys.value:
        beam = vmf.create_ent(classname='env_beam')

        # 3 offsets - x = distance from walls, y = side, z = height
        beam_off = beam_keys.vec('origin', 0, 63, 56)

        for prop in beam_keys:
            beam[prop.real_name] = prop.value

        # Localise the targetname so it can be triggered..
        beam['LightningStart'] = beam['targetname'] = conditions.local_name(
            inst,
            beam['targetname', 'beam']
        )
        del beam['LightningEnd']
        beam['origin'] = start_pos + Vec(
            -beam_off.x, beam_off.y, beam_off.z,
        ).rotate(*angles)
        beam['TargetPoint'] = end_pos + Vec(
            +beam_off.x, beam_off.y, beam_off.z,
        ).rotate(*angles)

    # Allow adding outputs to the last path_track.
    for prop in res.find_all('EndOutput'):
        output = Output.parse(prop)
        output.output = 'OnPass'
        output.inst_out = None
        output.comma_sep = False
        output.target = conditions.local_name(inst, output.target)
        last_track.add_out(output)

    if motion_filter is not None:
        motion_trig = vmf.create_ent(
            classname='trigger_multiple',
            targetname=conditions.local_name(inst, 'enable_motion_trig'),
            origin=start_pos,
            filtername=motion_filter,
            startDisabled=1,
            wait=0.1,
        )
        motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState'))
        # Match the size of the original...
        motion_trig.solids.append(vmf.make_prism(
            start_pos + Vec(72, -56, 58).rotate(*angles),
            end_pos + Vec(-72, 56, 144).rotate(*angles),
            mat='tools/toolstrigger',
        ).solid)

    if res.bool('NoPortalFloor'):
        # Block portals on the floor..
        floor_noportal = vmf.create_ent(
            classname='func_noportal_volume',
            origin=beam_start,
        )
        floor_noportal.solids.append(vmf.make_prism(
            start_pos + Vec(-60, -60, -66).rotate(*angles),
            end_pos + Vec(60, 60, -60).rotate(*angles),
            mat='tools/toolsinvisible',
        ).solid)

    # A brush covering under the platform.
    base_trig = vmf.make_prism(
        start_pos + Vec(-64, -64, 48).rotate(*angles),
        end_pos + Vec(64, 64, 56).rotate(*angles),
        mat='tools/toolsinvisible',
    ).solid

    vmf.add_brush(base_trig)

    # Make a paint_cleanser under the belt..
    if res.bool('PaintFizzler'):
        pfizz = vmf.create_ent(
            classname='trigger_paint_cleanser',
            origin=start_pos,
        )
        pfizz.solids.append(base_trig.copy())
        for face in pfizz.sides():
            face.mat = 'tools/toolstrigger'
Exemple #35
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,
        )
Exemple #36
0
def improve_item(item: Property) -> None:
    """Improve editoritems formats in various ways.

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

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

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

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

            if 'pos1' not in voxel_part or 'pos2' not in voxel_part:
                LOGGER.warning(
                    'Item {} has invalid OccupiedVoxels part '
                    '(needs Pos1 and Pos2)!', item['type'])
                continue
            voxel_part.name = "Voxel"
            bbox_min, bbox_max = Vec.bbox(
                voxel_part.vec('pos1'),
                voxel_part.vec('pos2'),
            )
            del voxel_part['pos1']
            del voxel_part['pos2']
            for pos in Vec.iter_grid(bbox_min, bbox_max):
                new_part = voxel_part.copy()
                new_part['Pos'] = str(pos)
                occu_voxels.append(new_part)
Exemple #37
0
def load_config(conf: Property):
    """Setup all the generators from the config data."""
    global SPECIAL, OVERLAYS
    global_options = {
        prop.name: prop.value
        for prop in conf.find_children('Options')
    }
    # Give generators access to the global settings.
    Generator.global_settings.update(
        parse_options(
            # Pass it to both, the second will fail too.
            global_options,
            global_options,
        ))

    data: Dict[Any, Tuple[Dict[str, Any], Dict[str, List[str]]]] = {}

    gen_cat: GenCat
    gen_orient: Optional[Orient]
    gen_portal: Optional[Portalable]

    # Use this to allow alternate names for generators.
    conf_for_gen: Dict[Tuple[GenCat, Optional[Orient], Optional[Portalable]],
                       Property, ] = {}

    for prop in conf:
        if prop.name in ('options', 'antlines'):
            continue
        if '.' in prop.name:
            try:
                gen_cat_name, gen_portal_raw, gen_orient_raw = prop.name.split(
                    '.')
                gen_cat = GEN_CATS[gen_cat_name]
                gen_orient = ORIENTS[gen_orient_raw]
                gen_portal = Portalable(gen_portal_raw)
            except (KeyError, ValueError):
                LOGGER.warning('Could not parse texture generator type "{}"!',
                               prop.name)
                continue
            conf_for_gen[gen_cat, gen_orient, gen_portal] = prop
        else:
            try:
                gen_cat = GEN_CATS[prop.name]
            except KeyError:
                LOGGER.warning('Unknown texture generator type "{}"!',
                               prop.name)
                continue
            conf_for_gen[gen_cat, None, None] = prop

    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        if isinstance(gen_key, GenCat):
            # It's a non-tile generator.
            is_tile = False
            gen_cat = gen_key
            try:
                gen_conf = conf_for_gen[gen_key, None, None]
            except KeyError:
                gen_conf = Property(gen_key.value, [])
        else:
            # Tile-type generator
            is_tile = True
            try:
                gen_conf = conf_for_gen[gen_key]
            except KeyError:
                gen_conf = Property('', [])

            if not gen_conf.has_children():
                # Special case - using a single value to indicate that all
                # textures are the same.
                gen_conf = Property(
                    gen_conf.real_name,
                    [
                        Property('4x4', gen_conf.value),
                        Property(
                            'Options',
                            [
                                # Clumping isn't useful since it's all the same.
                                Property('Algorithm', 'RAND'),
                            ])
                    ])
        textures = {}

        # First parse the options.
        options = parse_options(
            {
                prop.name: prop.value
                for prop in gen_conf.find_children('Options')
            }, global_options)

        # Now do textures.
        if is_tile:
            # Tile generator, always have all tile sizes, and
            # only use the defaults if no textures were specified.
            for tex_name in TileSize:
                textures[tex_name] = [
                    prop.value for prop in gen_conf.find_all(str(tex_name))
                ]

            # In case someone switches them around, add on 2x1 to 1x2 textures.
            textures[TileSize.TILE_2x1] += [
                prop.value for prop in gen_conf.find_all('1x2')
            ]

            if not any(textures.values()):
                for tex_name, tex_default in tex_defaults.items():
                    textures[tex_name] = [tex_default]
        else:
            # Non-tile generator, use defaults for each value
            for tex_name, tex_default in tex_defaults.items():
                textures[tex_name] = tex = [
                    prop.value for prop in gen_conf.find_all(str(tex_name))
                ]
                if not tex and tex_default:
                    tex.append(tex_default)

        data[gen_key] = options, textures

        # Next, do a check to see if any texture names were specified that
        # we don't recognise.
        extra_keys = {prop.name for prop in gen_conf}
        extra_keys.discard('options')  # Not a texture name, but valid.

        if isinstance(gen_key, GenCat):
            extra_keys.difference_update(map(str.casefold,
                                             tex_defaults.keys()))
        else:
            # The defaults are just the size values.
            extra_keys.difference_update(map(str, TileSize))

        if extra_keys:
            LOGGER.warning('{}: Unknown texture names {}',
                           format_gen_key(gen_key),
                           ', '.join(sorted(extra_keys)))

    # Now complete textures for tile types,
    # copying over data from other generators.
    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        if isinstance(gen_key, GenCat):
            continue
        gen_cat, gen_orient, gen_portal = gen_key

        options, textures = data[gen_key]

        if not any(textures.values()) and gen_cat is not GenCat.NORMAL:
            # For the additional categories of tiles, we copy the entire
            # NORMAL one over if it's not set.
            textures.update(data[GenCat.NORMAL, gen_orient, gen_portal][1])

        if not textures[TileSize.TILE_4x4]:
            raise ValueError('No 4x4 tile set for "{}"!'.format(gen_key))

        # Copy 4x4, 2x2, 2x1 textures to the 1x1 size if the option was set.
        # Do it before inheriting tiles, so there won't be duplicates.
        if options['mixtiles']:
            block_tex = textures[TileSize.TILE_1x1]
            block_tex += textures[TileSize.TILE_4x4]
            block_tex += textures[TileSize.TILE_2x2]
            block_tex += textures[TileSize.TILE_2x1]

        # We need to do more processing.
        for orig, targ in TILE_INHERIT:
            if not textures[targ]:
                textures[targ] = textures[orig].copy()

    # Now finally create the generators.
    for gen_key, tex_defaults in TEX_DEFAULTS.items():
        options, textures = data[gen_key]

        if isinstance(gen_key, tuple):
            # Check the algorithm to use.
            algo = options['algorithm']
            gen_cat, gen_orient, gen_portal = gen_key
            try:
                generator: Type[Generator] = GEN_CLASSES[algo]  # type: ignore
            except KeyError:
                raise ValueError('Invalid algorithm "{}" for {}!'.format(
                    algo, gen_key))
        else:
            # Signage, Overlays always use the Random generator.
            generator = GenRandom
            gen_cat = gen_key
            gen_orient = gen_portal = None

        GENERATORS[gen_key] = gen = generator(gen_cat, gen_orient, gen_portal,
                                              options, textures)

        # Allow it to use the default enums as direct lookups.
        if isinstance(gen, GenRandom):
            if gen_portal is None:
                gen.set_enum(tex_defaults.items())
            else:
                # Tiles always use TileSize.
                gen.set_enum((size.value, size) for size in TileSize)

    SPECIAL = GENERATORS[GenCat.SPECIAL]
    OVERLAYS = GENERATORS[GenCat.OVERLAYS]
Exemple #38
0
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property):
    """Properly setup rotating brush entities to match the instance.

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

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

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

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

    For brush generation mode:

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

    Permitted entities:

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

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

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

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

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

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

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

        conditions.set_ent_keys(door_ent, ent, res)

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

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

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

    # This ent uses a keyvalue for reversing...
    if door_type == 'momentary_rot_button':
        vmf.create_ent(
            'comp_kv_setter',
            origin=setter_loc,
            target=name,
            mode='kv',
            kv_name='StartDirection',
            kv_value_global='1' if reverse else '-1',
        )
Exemple #39
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,
        )
Exemple #40
0
    def build_instance_data(editoritems: Property):
        """Build a property tree listing all of the instances for each item.
        as well as another listing the input and output commands.
        VBSP uses this to reduce duplication in VBSP_config files.

        This additionally strips custom instance definitions from the original
        list.
        """
        instance_locs = Property("AllInstances", [])
        cust_inst = Property("CustInstances", [])
        commands = Property("Connections", [])
        item_classes = Property("ItemClasses", [])
        root_block = Property(None, [
            instance_locs,
            item_classes,
            cust_inst,
            commands,
        ])

        for item in editoritems.find_all("Item"):
            instance_block = Property(item['Type'], [])
            instance_locs.append(instance_block)

            comm_block = Property(item['Type'], [])

            for inst_block in item.find_all("Exporting", "instances"):
                for inst in inst_block.value[:]:  # type: Property
                    if inst.name.isdigit():
                        # Direct Portal 2 value
                        instance_block.append(
                            Property('Instance', inst['Name'])
                        )
                    else:
                        # It's a custom definition, remove from editoritems
                        inst_block.value.remove(inst)

                        # Allow the name to start with 'bee2_' also to match
                        # the <> definitions - it's ignored though.
                        name = inst.name
                        if name[:5] == 'bee2_':
                            name = name[5:]

                        cust_inst.set_key(
                            (item['type'], name),
                            # Allow using either the normal block format,
                            # or just providing the file - we don't use the
                            # other values.
                            inst['name'] if inst.has_children() else inst.value,
                        )

            # Look in the Inputs and Outputs blocks to find the io definitions.
            # Copy them to property names like 'Input_Activate'.
            for io_type in ('Inputs', 'Outputs'):
                for block in item.find_all('Exporting', io_type, CONN_NORM):
                    for io_prop in block:
                        comm_block[
                            io_type[:-1] + '_' + io_prop.real_name
                        ] = io_prop.value

            # The funnel item type is special, having the additional input type.
            # Handle that specially.
            if item['type'].casefold() == 'item_tbeam':
                for block in item.find_all('Exporting', 'Inputs', CONN_FUNNEL):
                    for io_prop in block:
                        comm_block['TBeam_' + io_prop.real_name] = io_prop.value

            # Fizzlers don't work correctly with outputs. This is a signal to
            # conditions.fizzler, but it must be removed in editoritems.
            if item['ItemClass', ''].casefold() == 'itembarrierhazard':
                for block in item.find_all('Exporting', 'Outputs'):
                    if CONN_NORM in block:
                        del block[CONN_NORM]

            # Record the itemClass for each item type.
            item_classes[item['type']] = item['ItemClass', 'ItemBase']

            # Only add the block if the item actually has IO.
            if comm_block.value:
                commands.append(comm_block)

        return root_block.export()
Exemple #41
0
def res_fix_rotation_axis(ent: Entity, res: Property):
    """Generate a `func_rotating`, `func_door_rotating` or any similar entity.

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

    Permitted entities:
       * `func_rotating`
       * `func_door_rotating`
       * `func_rot_button`
       * `func_platrot`
    """
    des_axis = res['axis', 'z'].casefold()
    reverse = srctools.conv_bool(res['reversed', '0'])
    door_type = res['classname', 'func_door_rotating']

    # Extra stuff to apply to the flags (USE, toggle, etc)
    flags = sum(
        map(
            # Add together multiple values
            srctools.conv_int,
            res['flags', '0'].split('+')))

    name = conditions.local_name(ent, res['name', ''])

    axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0'])

    pos = Vec.from_str(res['Pos', '0 0 0']).rotate_by_str(ent['angles',
                                                              '0 0 0'])
    pos += Vec.from_str(ent['origin', '0 0 0'])

    door_ent = vbsp.VMF.create_ent(
        classname=door_type,
        targetname=name,
        origin=pos.join(' '),
    )

    conditions.set_ent_keys(door_ent, ent, res)

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

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

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

    flag_values = FLAG_ROTATING[door_type]
    # Make the door always non-solid!
    flags |= flag_values.get('solid_flags', 0)
    # Add or remove flags as needed.
    # flags |= bit sets it to 1.
    # flags |= ~bit sets it to 0.
    if axis.x != 0:
        flags |= flag_values.get('x', 0)
    else:
        flags &= ~flag_values.get('x', 0)

    if axis.y != 0:
        flags |= flag_values.get('y', 0)
    else:
        flags &= ~flag_values.get('y', 0)

    if axis.z != 0:
        flags |= flag_values.get('z', 0)
    else:
        flags &= ~flag_values.get('z', 0)

    if door_type == 'momentary_rot_button':
        door_ent['startdirection'] = '1' if reverse else '-1'
    else:
        if reverse:
            flags |= flag_values.get('rev', 0)
        else:
            flags &= ~flag_values.get('rev', 0)
    door_ent['spawnflags'] = str(flags)
def res_linked_cube_dropper(drp_inst: Entity, res: Property):
    """Link a cube and dropper together, to preplace the cube at a location."""
    time = drp_inst.fixup.int('$timer_delay')
    # Portal 2 bug - when loading existing maps, timers are set to 3...
    if not (3 < time <= 30):
        # Infinite or 3-second - this behaviour is disabled..
        return

    try:

        cube_inst, cube_type, resp_out_name, resp_out = LINKED_CUBES[time]
    except KeyError:
        raise Exception('Unknown cube "linkage" value ({}) in dropper!'.format(
            time,
        ))

    # Force the dropper to match the cube..
    #  = cube_type

    # Set auto-drop to False (so there isn't two cubes),
    # and auto-respawn to True (so it actually functions).
    drp_inst.fixup['$disable_autodrop'] = '1'
    drp_inst.fixup['$disable_autorespawn'] = '0'

    fizz_out_name, fizz_out = Output.parse_name(res['FizzleOut'])

    # Output to destroy the cube when the dropper is triggered externally.
    drp_inst.add_out(Output(
        inst_out=fizz_out_name,
        out=fizz_out,
        targ=local_name(cube_inst, 'cube'),
        inp='Dissolve',
        only_once=True,
    ))

    # Cube items don't have proxies, so we need to use AddOutput
    # after it's created (@relay_spawn_3's time).
    try:
        relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3']
    except KeyError:
        relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] = cube_inst.map.create_ent(
            classname='logic_relay',
            targetname='@relay_spawn_3',
            origin=cube_inst['origin'],
        )

    respawn_inp = list(res.find_all('RespawnIn'))
    # There's some voice-logic specific to companion cubes.
    respawn_inp.extend(res.find_all(
        'RespawnCcube' if
        drp_inst.fixup['$cube_type'] == '1'
        else 'RespawnCube'
    ))

    for inp in respawn_inp:
        resp_in_name, resp_in = inp.value.split(':', 1)

        out = Output(
            out='OnFizzled',
            targ=drp_inst,
            inst_in=resp_in_name,
            inp=resp_in,
            only_once=True,
        )

        relay_spawn_3.add_out(Output(
            out='OnTrigger',
            targ=local_name(cube_inst, 'cube'),
            inp='AddOutput',
            param=out.gen_addoutput(),
            only_once=True,
            delay=0.01,
        ))