Exemplo n.º 1
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Exemplo n.º 2
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

        outputs = [
            Output.parse(prop)
            for prop in
            conf.find_children('Outputs')
        ]

        textures = {}
        for group in TexGroup:
            textures[group] = conf['tex_' + group.value, None]

        keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('keys')
        }

        local_keys = {
            prop.name: prop.value
            for prop in
            conf.find_children('localkeys')
        }

        if 'classname' not in keys:
            raise ValueError(
                'Fizzler Brush "{}" does not have a classname!'.format(
                conf['name'],
                )
            )

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Exemplo n.º 3
0
def res_add_global_inst(res: Property):
    """Add one instance in a location.

    Options:
        allow_multiple: Allow multiple copies of this instance. If 0, the
            instance will not be added if it was already added.
        name: The targetname of the instance. IF blank, the instance will
              be given a name of the form 'inst_1234'.
        file: The filename for the instance.
        Angles: The orientation of the instance (defaults to '0 0 0').
        Origin: The location of the instance (defaults to '0 0 -10000').
        Fixup_style: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
    """
    if res.value is not None:
        if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
            # By default we will skip adding the instance
            # if was already added - this is helpful for
            # items that add to original items, or to avoid
            # bugs.
            new_inst = vbsp.VMF.create_ent(
                classname="func_instance",
                targetname=res['name', ''],
                file=instanceLocs.resolve_one(res['file'], error=True),
                angles=res['angles', '0 0 0'],
                origin=res['position', '0 0 -10000'],
                fixup_style=res['fixup_style', '0'],
            )
            GLOBAL_INSTANCES.add(res['file'])
            if new_inst['targetname'] == '':
                new_inst['targetname'] = "inst_"
                new_inst.make_unique()
    return RES_EXHAUSTED
Exemplo n.º 4
0
    def parse(prop: Property):
        """Parse from property values.

        The value can be in four forms:
        "prop" "material"
        "prop" "<scale>|material"
        "prop" "<scale>|material|static"
        "prop"
            {
            "tex"    "<mat>"
            "scale"  "<scale>"
            "static" "<is_static>"
            }
        """
        if prop.has_children():
            tex = prop['tex']
            scale = prop.float('scale', 0.25)
            static = prop.bool('static')
        else:
            vals = prop.value.split('|')
            opts = ()
            scale_str = '0.25'

            if len(vals) == 2:
                scale_str, tex = vals
            elif len(vals) > 2:
                scale_str, tex, *opts = vals
            else:
                # Unpack to ensure it only has 1 section
                [tex] = vals
            scale = conv_float(scale_str, 0.25)
            static = 'static' in opts

        return AntTex(tex, scale, static)
Exemplo n.º 5
0
def res_replace_instance(vmf: VMF, inst: Entity, res: Property):
    """Replace an instance with another entity.

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

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

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

    vmf.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    new_ent['origin'] = Vec.from_str(new_ent['origin']) @ angles + origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Exemplo n.º 6
0
    def parse(prop: Property):
        """Parse from property values.

        The value can be in four forms:
        "prop" "material"
        "prop" "<scale>|material"
        "prop" "<scale>|material|static"
        "prop"
            {
            "tex"    "<mat>"
            "scale"  "<scale>"
            "static" "<is_static>"
            }
        """
        if prop.has_children():
            tex = prop['tex']
            scale = prop.float('scale', 0.25)
            static = prop.bool('static')
        else:
            vals = prop.value.split('|')
            opts = ()
            scale_str = '0.25'

            if len(vals) == 2:
                scale_str, tex = vals
            elif len(vals) > 2:
                scale_str, tex, *opts = vals
            else:
                # Unpack to ensure it only has 1 section
                [tex] = vals
            scale = conv_float(scale_str, 0.25)
            static = 'static' in opts

        return AntTex(tex, scale, static)
Exemplo n.º 7
0
def res_calc_opposite_wall_dist(inst: Entity, res: Property):
    """Calculate the distance between this item and the opposing wall.

    The value is stored in the `$var` specified by the property value.
    Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value.
    `GooCollide` means that it will stop when goo is found, otherwise it is
    ignored.
    `GooAdjust` means additionally if the space is goo, the distance will
    be modified so that it specifies the surface of the goo.
    """
    if res.has_children():
        result_var = res['ResultVar']
        dist_off = res.float('offset')
        collide_goo = res.bool('GooCollide')
        adjust_goo = res.bool('GooAdjust')
    else:
        result_var = res.value
        dist_off = 0
        collide_goo = adjust_goo = False

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

    mask = [
        brushLoc.Block.SOLID,
        brushLoc.Block.EMBED,
        brushLoc.Block.PIT_BOTTOM,
        brushLoc.Block.PIT_SINGLE,
    ]

    # Only if actually downward.
    if normal == (0, 0, -1) and collide_goo:
        mask.append(brushLoc.Block.GOO_TOP)
        mask.append(brushLoc.Block.GOO_SINGLE)

    opposing_pos = brushLoc.POS.raycast_world(
        origin,
        normal,
        mask,
    )

    if adjust_goo and brushLoc.POS['world':opposing_pos + 128 * normal].is_goo:
        # If the top is goo, adjust so the 64 below is the top of the goo.
        dist_off += 32

    inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
Exemplo n.º 8
0
def res_calc_opposite_wall_dist(inst: Entity, res: Property):
    """Calculate the distance between this item and the opposing wall.

    The value is stored in the `$var` specified by the property value.
    Alternately it is set by `ResultVar`, and `offset` adds or subtracts to the value.
    `GooCollide` means that it will stop when goo is found, otherwise it is
    ignored.
    `GooAdjust` means additionally if the space is goo, the distance will
    be modified so that it specifies the surface of the goo.
    """
    if res.has_children():
        result_var = res['ResultVar']
        dist_off = res.float('offset')
        collide_goo = res.bool('GooCollide')
        adjust_goo = res.bool('GooAdjust')
    else:
        result_var = res.value
        dist_off = 0
        collide_goo = adjust_goo = False

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

    mask = [
        brushLoc.Block.SOLID,
        brushLoc.Block.EMBED,
        brushLoc.Block.PIT_BOTTOM,
        brushLoc.Block.PIT_SINGLE,
    ]

    # Only if actually downward.
    if normal == (0, 0, -1) and collide_goo:
        mask.append(brushLoc.Block.GOO_TOP)
        mask.append(brushLoc.Block.GOO_SINGLE)

    opposing_pos = brushLoc.POS.raycast_world(
        origin,
        normal,
        mask,
    )

    if adjust_goo and brushLoc.POS['world': opposing_pos + 128*normal].is_goo:
        # If the top is goo, adjust so the 64 below is the top of the goo.
        dist_off += 32

    inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
Exemplo n.º 9
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

    It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic.
    The two parameters `origin` and `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

    is_tag = options.get(str, 'game_id') == utils.STEAM_IDS['TAG']

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

    # Some styles might want to ignore the instance we're running on.
    if not res.bool('global'):
        origin = origin.rotate_by_str(inst['angles'])
        normal = normal.rotate_by_str(inst['angles'])
        origin += Vec.from_str(inst['origin'])

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Exemplo n.º 10
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

    It produces either an instance or the normal spawn entity. This is required since ATLAS may need to have the paint gun logic.
    The two parameters `origin` and `facing` must be set to determine the required position.
    If `global` is set, the spawn point will be absolute instead of relative to the current instance.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

    is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG']

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

    # Some styles might want to ignore the instance we're running on.
    if not res.bool('global'):
        origin = origin.rotate_by_str(inst['angles'])
        normal = normal.rotate_by_str(inst['angles'])
        origin += Vec.from_str(inst['origin'])

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Exemplo n.º 11
0
def load_handler(props: Property) -> None:
    """Load compiler options from the palette."""
    chosen_thumb.set(props['sshot_type', chosen_thumb.get()])
    cleanup_screenshot.set(
        props.bool('sshot_cleanup', cleanup_screenshot.get()))

    if 'sshot_data' in props:
        screenshot_parts = b'\n'.join([
            prop.value.encode('ascii')
            for prop in props.find_children('sshot_data')
        ])
        screenshot_data = base64.decodebytes(screenshot_parts)
        with atomic_write(SCREENSHOT_LOC, mode='wb', overwrite=True) as f:
            f.write(screenshot_data)

    # Refresh these.
    set_screen_type()
    set_screenshot()

    start_in_elev.set(props.bool('spawn_elev', start_in_elev.get()))

    try:
        player_mdl = props['player_model']
    except LookupError:
        pass
    else:
        player_model_var.set(PLAYER_MODELS[player_mdl])
        COMPILE_CFG['General']['player_model'] = player_mdl

    VOICE_PRIORITY_VAR.set(
        props.bool('voiceline_priority', VOICE_PRIORITY_VAR.get()))

    corr_prop = props.find_block('corridor', or_blank=True)
    for group, win in CORRIDOR.items():
        try:
            sel_id = corr_prop[group]
        except LookupError:
            "No config option, ok."
        else:
            win.sel_item_id(sel_id)
            COMPILE_CFG['Corridor'][
                group] = '0' if sel_id == '<NONE>' else sel_id

    COMPILE_CFG.save_check()
    return None
Exemplo n.º 12
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

    This creates an instance with the desired orientation.
    The two parameters 'origin' and 'angles' must be set.
    """
    if vbsp.GAME_MODE != 'COOP':
        return RES_EXHAUSTED

    is_tag = vbsp_options.get(str, 'game_id') == utils.STEAM_IDS['TAG']

    origin = res.vec('origin')
    normal = res.vec('facing', z=1)

    # Some styles might want to ignore the instance we're running on.
    if not res.bool('global'):
        origin = origin.rotate_by_str(inst['angles'])
        normal = normal.rotate_by_str(inst['angles'])
        origin += Vec.from_str(inst['origin'])

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Exemplo n.º 13
0
def res_cust_antline_setup(res: Property) -> Callable[[Entity], None]:
    """Customise the output antlines.

    Options:

    * `wall`: The configuration for antlines on walls. Same as global
        style options.
    * `floor`: The configuration for floor/ceiling antlines. If not provided,
        this is assumed to be the same as `wall`.
    * `remove_signs`: If true, remove the indicator signs.
    * `toggle_var`: If set, this item controls the toggle state fully of its
        antlines. This is a fixup var which will be set to the name of the
        overlays, for user control.
    """
    wall_style: antlines.AntType | None
    floor_type: antlines.AntType | None
    if 'wall' in res:
        wall_style = antlines.AntType.parse(res.find_key('wall'))
    else:
        wall_style = None

    if 'floor' in res:
        floor_style = antlines.AntType.parse(res.find_key('floor'))
    else:
        floor_style = wall_style

    remove_signs = res.bool('remove_signs')
    toggle_var = res['toggle_var', '']

    def change_antlines(inst: Entity) -> None:
        """Change the antlines of an item."""
        item = connections.ITEMS[inst['targetname']]
        if wall_style is not None:
            item.ant_wall_style = wall_style
        if floor_style is not None:
            item.ant_floor_style = floor_style

        if remove_signs:
            for sign in item.ind_panels:
                sign.remove()
            item.ind_panels.clear()

        if toggle_var:
            item.ant_toggle_var = toggle_var

    return change_antlines
Exemplo n.º 14
0
def res_cust_antline_setup(res: Property):
    if 'wall' in res:
        wall_type = antlines.AntType.parse(res.find_key('wall'))
    else:
        wall_type = None

    if 'floor' in res:
        floor_type = antlines.AntType.parse(res.find_key('floor'))
    else:
        floor_type = wall_type

    return (
        wall_type,
        floor_type,
        res.bool('remove_signs'),
        res['toggle_var', ''],
    )
Exemplo n.º 15
0
def res_cust_antline_setup(res: Property):
    if 'wall' in res:
        wall_type = antlines.AntType.parse(res.find_key('wall'))
    else:
        wall_type = None

    if 'floor' in res:
        floor_type = antlines.AntType.parse(res.find_key('floor'))
    else:
        floor_type = wall_type

    return (
        wall_type,
        floor_type,
        res.bool('remove_signs'),
        res['toggle_var', ''],
    )
Exemplo n.º 16
0
def flag_angles(flag: Property) -> Callable[[Entity], bool]:
    """Check that a instance is pointed in a direction.

    The value should be either just the angle to check, or a block of
    options:

    - `direction`: A unit vector (XYZ value) pointing in a direction, or some
        keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side.
    - `From_dir`: The direction the unrotated instance is pointed in.
        This lets the flag check multiple directions.
    - `Allow_inverse`: If true, this also returns True if the instance is
        pointed the opposite direction .
    """

    if flag.has_children():
        targ_angle = flag['direction', '0 0 0']
        from_dir = flag['from_dir', '0 0 1']
        if from_dir.casefold() in DIRECTIONS:
            from_dir = Vec(DIRECTIONS[from_dir.casefold()])
        else:
            from_dir = Vec.from_str(from_dir, 0, 0, 1)
        allow_inverse = flag.bool('allow_inverse')
    else:
        targ_angle = flag.value
        from_dir = Vec(0, 0, 1)
        allow_inverse = False

    try:
        normal = DIRECTIONS[targ_angle.casefold()]
    except KeyError:
        normal = Vec.from_str(targ_angle)

    def check_orient(inst: Entity) -> bool:
        """Check the orientation against the instance."""
        inst_normal = from_dir @ Angle.from_str(inst['angles'])

        if normal == 'WALL':
            # Special case - it's not on the floor or ceiling
            return abs(inst_normal.z) < 1e-6
        else:
            return inst_normal == normal or (
                allow_inverse and -inst_normal == normal
            )
    return check_orient
Exemplo n.º 17
0
def res_global_input_setup(res: Property) -> tuple[str, Output]:
    """Pre-parse the global input."""
    if res.has_children():
        name = res['name', '']
        if not name and res.bool('alsoonload'):
            name = ON_LOAD
        inp_name, inp_command = Output.parse_name(res['input'])
        return name, Output(
            out=res['output', 'OnTrigger'],
            targ=res['target', ''],
            inp=inp_command,
            inst_in=inp_name,
            delay=srctools.conv_float(res['delay', '']),
            param=res['param', ''],
        )
    else:
        out = Output.parse(res)
        out.output = ''  # Don't need to store GlobalInput...
        return '', out
Exemplo n.º 18
0
def res_add_global_inst(vmf: VMF, res: Property):
    """Add one instance in a specific location.

    Options:

    - `allow_multiple`: Allow multiple copies of this instance. If 0, the
        instance will not be added if it was already added.
    - `name`: The targetname of the instance. If blank, the instance will
          be given a name of the form `inst_1234`.
    - `file`: The filename for the instance.
    - `angles`: The orientation of the instance (defaults to `0 0 0`).
    - `fixup_style`: The Fixup style for the instance. `0` (default) is
        Prefix, `1` is Suffix, and `2` is None.
    - `position`: The location of the instance. If not set, it will be placed
        in a 128x128 nodraw room somewhere in the map. Objects which can
        interact with nearby object should not be placed there.
    """
    if not res.has_children():
        res = Property('AddGlobal', [Property('File', res.value)])
    file = instanceLocs.resolve_one(res['file'], error=True)

    if res.bool('allow_multiple') or file.casefold() not in conditions.GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vmf.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=file,
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = options.get(Vec, 'global_ents_loc')
        conditions.GLOBAL_INSTANCES.add(file.casefold())
        conditions.ALL_INST.add(file.casefold())
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return conditions.RES_EXHAUSTED
Exemplo n.º 19
0
def res_get_item_config(inst: Entity, res: Property):
    """Check if an item config panel value matches another value.

    ID is the ID of the group. Name is the name of the widget.
    If UseTimer is true, it uses $timer_delay to choose the value to use.
    Value is the value to compare to.
    """
    group_id = res['ID']
    wid_name = res['Name'].casefold()
    desired_value = res['Value']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    conf = vbsp_options.get_itemconf((group_id, wid_name), None, timer_delay)
    if conf is None:  # Doesn't exist
        return False

    return conf == desired_value
Exemplo n.º 20
0
def flag_angles(inst: Entity, flag: Property):
    """Check that a instance is pointed in a direction.

    The value should be either just the angle to check, or a block of
    options:

    - `direction`: A unit vector (XYZ value) pointing in a direction, or some
        keywords: `+z`, `-y`, `N`/`S`/`E`/`W`, `up`/`down`, `floor`/`ceiling`, or `walls` for any wall side.
    - `From_dir`: The direction the unrotated instance is pointed in.
        This lets the flag check multiple directions.
    - `Allow_inverse`: If true, this also returns True if the instance is
        pointed the opposite direction .
    """
    angle = inst['angles', '0 0 0']

    if flag.has_children():
        targ_angle = flag['direction', '0 0 0']
        from_dir = flag['from_dir', '0 0 1']
        if from_dir.casefold() in DIRECTIONS:
            from_dir = Vec(DIRECTIONS[from_dir.casefold()])
        else:
            from_dir = Vec.from_str(from_dir, 0, 0, 1)
        allow_inverse = flag.bool('allow_inverse')
    else:
        targ_angle = flag.value
        from_dir = Vec(0, 0, 1)
        allow_inverse = False

    normal = DIRECTIONS.get(targ_angle.casefold(), None)
    if normal is None:
        return False  # If it's not a special angle,
        # so it failed the exact match

    inst_normal = from_dir.rotate_by_str(angle)

    if normal == 'WALL':
        # Special case - it's not on the floor or ceiling
        return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1))
    else:
        return inst_normal == normal or (allow_inverse
                                         and -inst_normal == normal)
Exemplo n.º 21
0
def res_match_item_config(inst: Entity, res: Property) -> bool:
    """Check if an Item Config Panel value matches another value.

    * `ID` is the ID of the group.
    * `Name` is the name of the widget.
    * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use.
    * `Value` is the value to compare to.
    """
    group_id = res['ID']
    wid_name = res['Name'].casefold()
    desired_value = res['Value']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    conf = options.get_itemconf((group_id, wid_name), None, timer_delay)
    if conf is None:  # Doesn't exist
        return False

    return conf == desired_value
Exemplo n.º 22
0
def res_match_item_config(inst: Entity, res: Property) -> bool:
    """Check if an item config panel value matches another value.

    * `ID` is the ID of the group.
    * `Name` is the name of the widget.
    * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use.
    * `Value` is the value to compare to.
    """
    group_id = res['ID']
    wid_name = res['Name'].casefold()
    desired_value = res['Value']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    conf = vbsp_options.get_itemconf((group_id, wid_name), None, timer_delay)
    if conf is None:  # Doesn't exist
        return False

    return conf == desired_value
Exemplo n.º 23
0
def res_get_item_config(inst: Entity, res: Property):
    """Load a config from the item config panel onto a fixup.

    ID is the ID of the group. Name is the name of the widget, and resultVar
    is the location to store. If UseTimer is true, it uses $timer_delay to
    choose the value to use. Default is the default value, if the config
    isn't found.
    """
    group_id = res['ID']
    wid_name = res['Name']
    default = res['default']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf(
        (group_id, wid_name),
        default,
        timer_delay,
    )
Exemplo n.º 24
0
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

    Options:
        allow_multiple: Allow multiple copies of this instance. If 0, the
            instance will not be added if it was already added.
        name: The targetname of the instance. IF blank, the instance will
              be given a name of the form 'inst_1234'.
        file: The filename for the instance.
        Angles: The orientation of the instance (defaults to '0 0 0').
        Fixup_style: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
        Position: The location of the instance. If not set, it will be placed
            in a 128x128 nodraw room somewhere in the map. Objects which can
            interact with nearby object should not be placed there.
    """
    if not res.has_children():
        res = Property('AddGlobal', [Property('File', res.value)])

    if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vbsp.VMF.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=instanceLocs.resolve_one(res['file'], error=True),
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc')
        GLOBAL_INSTANCES.add(res['file'])
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return RES_EXHAUSTED
Exemplo n.º 25
0
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

    Options:
        `allow_multiple`: Allow multiple copies of this instance. If 0, the
            instance will not be added if it was already added.
        `name`: The targetname of the instance. If blank, the instance will
              be given a name of the form `inst_1234`.
        `file`: The filename for the instance.
        `angles`: The orientation of the instance (defaults to `0 0 0`).
        `fixup_style`: The Fixup style for the instance. `0` (default) is
            Prefix, `1` is Suffix, and `2` is None.
        `position`: The location of the instance. If not set, it will be placed
            in a 128x128 nodraw room somewhere in the map. Objects which can
            interact with nearby object should not be placed there.
    """
    if not res.has_children():
        res = Property('AddGlobal', [Property('File', res.value)])

    if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vbsp.VMF.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=instanceLocs.resolve_one(res['file'], error=True),
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc')
        GLOBAL_INSTANCES.add(res['file'])
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return RES_EXHAUSTED
Exemplo n.º 26
0
def res_item_config_to_fixup(inst: Entity, res: Property):
    """Load a config from the item config panel onto a fixup.

    * `ID` is the ID of the group.
    * `Name` is the name of the widget.
    * `resultVar` is the location to store the value into.
    * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use.
    * `Default` is the default value, if the config isn't found.
    """
    group_id = res['ID']
    wid_name = res['Name']
    default = res['default']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf(
        (group_id, wid_name),
        default,
        timer_delay,
    )
Exemplo n.º 27
0
def res_item_config_to_fixup(inst: Entity, res: Property):
    """Load a config from the item config panel onto a fixup.

    * `ID` is the ID of the group.
    * `Name` is the name of the widget.
    * `resultVar` is the location to store the value into.
    * If `UseTimer` is true, it uses `$timer_delay` to choose the value to use.
    * `Default` is the default value, if the config isn't found.
    """
    group_id = res['ID']
    wid_name = res['Name']
    default = res['default']
    if res.bool('UseTimer'):
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    inst.fixup[res['ResultVar']] = vbsp_options.get_itemconf(
        (group_id, wid_name),
        default,
        timer_delay,
    )
Exemplo n.º 28
0
def get_itemconf(inst: Entity, res: Property) -> str | None:
    """Implement ItemConfig and GetItemConfig shared logic."""
    timer_delay: int | None

    group_id = res['ID']
    wid_name = inst.fixup.substitute(res['Name']).casefold()

    match = BRACE_RE.match(wid_name)
    if match is not None:  # Match name[timer], after $fixup substitution.
        wid_name, timer_str = match.groups()
        # Should not fail, we matched it above.
        timer_delay = int(timer_str)
    elif res.bool('UseTimer'):
        LOGGER.warning(
            'UseTimer is deprecated, use name = "{}[$timer_delay]".',
            wid_name,
        )
        timer_delay = inst.fixup.int('$timer_delay')
    else:
        timer_delay = None

    return options.get_itemconf((group_id, wid_name), None, timer_delay)
Exemplo n.º 29
0
def res_piston_plat_setup(res: Property):
    # Allow reading instances direct from the ID.
    # But use direct ones first.
    item_id = res['itemid', None]
    inst = {}
    for name in INST_NAMES:
        if name in res:
            lookup = res[name]
            if lookup == '':
                # Special case, allow blank for no instance.
                inst[name] = ''
                continue
        elif item_id is not None:
            lookup = '<{}:bee2_pist_{}>'.format(item_id, name)
        else:
            raise ValueError('No "{}" specified!'.format(name))
        inst[name] = resolve_single(lookup, error=True)

    template = template_brush.get_template(res['template'])

    visgroup_names = [
        res['visgroup_1', 'pist_1'],
        res['visgroup_2', 'pist_2'],
        res['visgroup_3', 'pist_3'],
        res['visgroup_top', 'pist_4'],
    ]

    return (
        template,
        visgroup_names,
        inst,
        res.bool('has_dn_fizz'),
        res['auto_var', ''],
        res['color_var', ''],
        res['source_ent', ''],
        res['snd_start', ''],
        res['snd_loop', ''],
        res['snd_stop', ''],
    )
Exemplo n.º 30
0
def res_track_plat(vmf: VMF, res: Property):
    """Logic specific to Track Platforms.

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

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

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

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

    if not track_instances:
        return RES_EXHAUSTED

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

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

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

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

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

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

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

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

        # Now figure out which way the track faces:

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

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

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

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

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

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

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

    return RES_EXHAUSTED  # Don't re-run
Exemplo n.º 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
Exemplo n.º 32
0
def res_add_overlay_inst(inst: Entity, res: Property):
    """Add another instance on top of this one.

    If a single value, this sets only the filename.
    Values:
        `file`: The filename.
        `fixup_style`: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
        `copy_fixup`: If true, all the $replace values from the original
            instance will be copied over.
        `move_outputs`: If true, outputs will be moved to this instance.
        `offset`: The offset (relative to the base) that the instance
            will be placed. Can be set to '<piston_top>' and
            '<piston_bottom>' to offset based on the configuration.
            '<piston_start>' will set it to the starting position, and
            '<piston_end>' will set it to the ending position.
            of piston platform handles.
        `angles`: If set, overrides the base instance angles. This does
            not affect the offset property.
        `fixup`/`localfixup`: Keyvalues in this block will be copied to the
            overlay entity.
            If the value starts with $, the variable will be copied over.
            If this is present, copy_fixup will be disabled.
    """

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

    angle = res['angles', inst['angles', '0 0 0']]

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

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

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

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

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

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

    return overlay_inst
Exemplo n.º 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,
            )
Exemplo n.º 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'
Exemplo n.º 35
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

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

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

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

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

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

    marker_names = set()

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

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

    item_id = res['markerItem']

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

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

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

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

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

        inst1 = mark1.inst
        inst2 = mark2.inst

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return RES_EXHAUSTED
Exemplo n.º 36
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,
        )
Exemplo n.º 37
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',
        )
Exemplo n.º 38
0
def res_import_template(vmf: VMF, coll: Collisions, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

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

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

    force = res['force', ''].casefold().split()
    if 'white' in force:
        conf_force_colour = texturing.Portalable.white
    elif 'black' in force:
        conf_force_colour = texturing.Portalable.black
    elif 'invert' in force:
        conf_force_colour = 'INVERT'
    else:
        conf_force_colour = None

    if 'world' in force:
        force_type = template_brush.TEMP_TYPES.world
    elif 'detail' in force:
        force_type = template_brush.TEMP_TYPES.detail
    else:
        force_type = template_brush.TEMP_TYPES.default

    force_grid: texturing.TileSize | None
    size: texturing.TileSize
    for size in texturing.TileSize:
        if size in force:
            force_grid = size
            break
    else:
        force_grid = None

    if 'bullseye' in force:
        surf_cat = texturing.GenCat.BULLSEYE
    elif 'special' in force or 'panel' in force:
        surf_cat = texturing.GenCat.PANEL
    else:
        surf_cat = texturing.GenCat.NORMAL

    replace_tex: dict[str, list[str]] = {}
    for prop in res.find_block('replace', or_blank=True):
        replace_tex.setdefault(prop.name, []).append(prop.value)

    if 'replaceBrush' in res:
        LOGGER.warning(
            'replaceBrush command used for template "{}", which is no '
            'longer used.',
            orig_temp_id,
        )
    bind_tile_pos = [
        # So it's the floor block location.
        Vec.from_str(value) - (0, 0, 128)
        for value in res.find_key('BindOverlay', or_blank=True).as_array()
    ]
    align_bind_overlay = res.bool('alignBindOverlay')

    key_values = res.find_block("Keys", or_blank=True)
    if key_values:
        key_block = Property("", [
            key_values,
            res.find_block("LocalKeys", or_blank=True),
        ])
        # Ensure we have a 'origin' keyvalue - we automatically offset that.
        if 'origin' not in key_values:
            key_values['origin'] = '0 0 0'

        # Spawn everything as detail, so they get put into a brush
        # entity.
        force_type = template_brush.TEMP_TYPES.detail
        outputs = [Output.parse(prop) for prop in res.find_children('Outputs')]
    else:
        key_block = None
        outputs = []

    # None = don't add any more.
    visgroup_func: Callable[[Random, list[str]], Iterable[str]] | None = None

    try:  # allow both spellings.
        visgroup_prop = res.find_key('visgroups')
    except NoKeyError:
        visgroup_prop = res.find_key('visgroup', 'none')
    if visgroup_prop.has_children():
        visgroup_instvars = list(visgroup_prop)
    else:
        visgroup_instvars = []
        visgroup_mode = res['visgroup', 'none'].casefold()
        # Generate the function which picks which visgroups to add to the map.
        if visgroup_mode == 'none':
            pass
        elif visgroup_mode == 'choose':

            def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]:
                """choose = add one random group."""
                return [rng.choice(groups)]
        else:
            percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00)
            if percent > 0.0:

                def visgroup_func(rng: Random,
                                  groups: list[str]) -> Iterable[str]:
                    """Number = percent chance for each to be added"""
                    for group in sorted(groups):
                        if rng.uniform(0, 100) <= percent:
                            yield group

    picker_vars = [(prop.real_name, prop.value)
                   for prop in res.find_children('pickerVars')]
    try:
        ang_override = to_matrix(Angle.from_str(res['angles']))
    except LookupError:
        ang_override = None
    try:
        rotation = to_matrix(Angle.from_str(res['rotation']))
    except LookupError:
        rotation = Matrix()

    offset = res['offset', '0 0 0']
    invert_var = res['invertVar', '']
    color_var = res['colorVar', '']
    if color_var.casefold() == '<editor>':
        color_var = '<editor>'

    # If true, force visgroups to all be used.
    visgroup_force_var = res['forceVisVar', '']

    sense_offset = res.vec('senseOffset')

    def place_template(inst: Entity) -> None:
        """Place a template."""
        temp_id = inst.fixup.substitute(orig_temp_id)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return place_template
Exemplo n.º 39
0
def res_add_brush(vmf: VMF, inst: Entity, res: Property) -> None:
    """Spawn in a brush at the indicated points.

    - `point1` and `point2` are locations local to the instance, with `0 0 0`
      as the floor-position.
    - `type` is either `black` or `white`.
    - detail should be set to `1/0`. If true the brush will be a
      func_detail instead of a world brush.

    The sides will be textured with 1x1, 2x2 or 4x4 wall, ceiling and floor
    textures as needed.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Angle.from_str(inst['angles'])

    point1 = Vec.from_str(res['point1'])
    point2 = Vec.from_str(res['point2'])

    point1.z -= 64  # Offset to the location of the floor
    point2.z -= 64

    # Rotate to match the instance
    point1 = point1 @ angles + origin
    point2 = point2 @ angles + origin

    try:
        tex_type = texturing.Portalable(res['type', 'black'])
    except ValueError:
        LOGGER.warning(
            'AddBrush: "{}" is not a valid brush '
            'color! (white or black)',
            res['type'],
        )
        tex_type = texturing.Portalable.BLACK

    dim = round(point2 - point1, 6)
    dim.max(-dim)

    # Figure out what grid size and scale is needed
    # Check the dimensions in two axes to figure out the largest
    # tile size that can fit in it.
    tile_grids = {
        'x': tiling.TileSize.TILE_4x4,
        'y': tiling.TileSize.TILE_4x4,
        'z': tiling.TileSize.TILE_4x4,
    }

    for axis in 'xyz':
        u, v = Vec.INV_AXIS[axis]
        max_size = min(dim[u], dim[v])
        if max_size % 128 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_1x1
        elif dim[u] % 64 == 0 and dim[v] % 128 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_2x1
        elif max_size % 64 == 0:
            tile_grids[axis] = tiling.TileSize.TILE_2x2
        else:
            tile_grids[axis] = tiling.TileSize.TILE_4x4

    solids = vmf.make_prism(point1, point2)

    solids.north.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.N),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['y'])
    solids.south.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.S),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['y'])
    solids.east.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.E),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['x'])
    solids.west.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.W),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['x'])
    solids.top.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.T),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['z'])
    solids.bottom.mat = texturing.gen(
        texturing.GenCat.NORMAL,
        Vec(Vec.B),
        tex_type,
    ).get(solids.north.get_origin(), tile_grids['z'])

    if res.bool('detail'):
        # Add the brush to a func_detail entity
        vmf.create_ent(classname='func_detail').solids = [solids.solid]
    else:
        # Add to the world
        vmf.add_brush(solids.solid)
Exemplo n.º 40
0
def res_add_overlay_inst(inst: Entity, res: Property):
    """Add another instance on top of this one.

    Values:
        File: The filename.
        Fixup Style: The Fixup style for the instance. '0' (default) is
            Prefix, '1' is Suffix, and '2' is None.
        Copy_Fixup: If true, all the $replace values from the original
            instance will be copied over.
        move_outputs: If true, outputs will be moved to this instance.
        offset: The offset (relative to the base) that the instance
            will be placed. Can be set to '<piston_top>' and
            '<piston_bottom>' to offset based on the configuration.
            '<piston_start>' will set it to the starting position, and
            '<piston_end>' will set it to the ending position.
            of piston platform handles.
        angles: If set, overrides the base instance angles. This does
            not affect the offset property.
        fixup/localfixup: Keyvalues in this block will be copied to the
            overlay entity.
            If the value starts with $, the variable will be copied over.
            If this is present, copy_fixup will be disabled.
    """

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

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

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

    if "offset" in res:
        folded_off = res["offset"].casefold()
        # Offset the overlay by the given distance
        # Some special placeholder values:
        if folded_off == "<piston_start>":
            if srctools.conv_bool(inst.fixup["$start_up", ""]):
                folded_off = "<piston_top>"
            else:
                folded_off = "<piston_bottom>"
        elif folded_off == "<piston_end>":
            if srctools.conv_bool(inst.fixup["$start_up", ""]):
                folded_off = "<piston_bottom>"
            else:
                folded_off = "<piston_top>"

        if folded_off == "<piston_bottom>":
            offset = Vec(z=srctools.conv_int(inst.fixup["$bottom_level"]) * 128)
        elif folded_off == "<piston_top>":
            offset = Vec(z=srctools.conv_int(inst.fixup["$top_level"], 1) * 128)
        else:
            # Regular vector
            offset = Vec.from_str(conditions.resolve_value(inst, res["offset"]))

        offset.rotate_by_str(inst["angles", "0 0 0"])
        overlay_inst["origin"] = (offset + Vec.from_str(inst["origin"])).join(" ")
    return overlay_inst
def res_resizeable_trigger(res: Property):
    """Replace two markers with a trigger brush.

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

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

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

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

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

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

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

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

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

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

    preview_var = res['previewVar', ''].casefold()

    # Display preview overlays if it's preview mode, and the style var is true
    # or does not exist
    if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]):
        preview_mat = res['previewMat', '']
        preview_inst_file = res['previewInst', '']
        pre_act_name, pre_act_inp = Output.parse_name(
            res['previewActivate', ''])
        pre_deact_name, pre_deact_inp = Output.parse_name(
            res['previewDeactivate', ''])
        preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25)
    else:
        # Deactivate the preview_ options when publishing.
        preview_mat = preview_inst_file = ''
        pre_act_name = pre_deact_name = None
        pre_act_inp = pre_deact_inp = ''
        preview_scale = 0.25

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

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

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

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

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

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

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

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

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

        if preview_mat:
            preview_brush = vbsp.VMF.create_ent(
                classname='func_brush',
                parentname=targ,
                origin=inst['origin'],

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

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

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

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

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

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

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

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

    return RES_EXHAUSTED
Exemplo n.º 42
0
def generate_music_script(data: Property, pack_list: PackList) -> bytes:
    """Generate a soundscript file for music."""
    # We also pack the filenames used for the tracks - that way funnel etc
    # only get packed when needed. Stock sounds are in VPKS or in aperturetag/,
    # we don't check there.
    # The voice attrs used in the map - we can skip tracks
    voice_attr = CONF['VoiceAttr', ''].casefold().split(';')

    funnel = data.find_key('tbeam', '')
    bounce = data.find_key('bouncegel', '')
    speed = data.find_key('speedgel', '')

    sync_funnel = data.bool('sync_funnel')

    if 'base' not in data:
        base = Property('base', 'bee2/silent_lp.wav')
        # Don't sync to a 2-second sound.
        sync_funnel = False
    else:
        base = data.find_key('base')

    # The sounds must be present, and the items should be in the map.
    has_funnel = funnel.value and (
        'funnel' in voice_attr or
        'excursionfunnel' in voice_attr
    )
    has_bounce = bounce.value and (
        'bouncegel' in voice_attr or
        'bluegel' in voice_attr
    )
    # Speed-gel sounds also play when flinging, so keep it always.

    file = StringIO()

    # Write the base music track
    file.write(MUSIC_START.format(name='', vol='1'))
    write_sound(file, base, pack_list, snd_prefix='#*')
    file.write(MUSIC_BASE)
    # The 'soundoperators' section is still open now.

    # Add the operators to play the auxilluary sounds..
    if has_funnel:
        file.write(MUSIC_FUNNEL_MAIN)
    if has_bounce:
        file.write(MUSIC_GEL_BOUNCE_MAIN)
    if speed.value:
        file.write(MUSIC_GEL_SPEED_MAIN)

    # End the main sound block
    file.write(MUSIC_END)

    if has_funnel:
        # Write the 'music.BEE2_funnel' sound entry
        file.write('\n')
        file.write(MUSIC_START.format(name='_funnel', vol='1'))
        write_sound(file, funnel, pack_list, snd_prefix='*')
        # Some tracks want the funnel music to sync with the normal
        # track, others randomly choose a start.
        file.write(
            MUSIC_FUNNEL_SYNC_STACK
            if sync_funnel else
            MUSIC_FUNNEL_RAND_STACK
        )
        file.write(MUSIC_FUNNEL_UPDATE_STACK)

    if has_bounce:
        file.write('\n')
        file.write(MUSIC_START.format(name='_gel_bounce', vol='0.5'))
        write_sound(file, bounce, pack_list, snd_prefix='*')
        # Fade in fast (we never get false positives, but fade out slow
        # since this disables when falling back..
        file.write(MUSIC_GEL_STACK.format(fadein=0.25, fadeout=1.5))

    if speed.value:
        file.write('\n')
        file.write(MUSIC_START.format(name='_gel_speed', vol='0.5'))
        write_sound(file, speed, pack_list, snd_prefix='*')
        # We need to shut off the sound fast, so portals don't confuse it.
        # Fade in slow so it doesn't make much sound (and also as we get
        # up to speed). We stop almost immediately on gel too.
        file.write(MUSIC_GEL_STACK.format(fadein=0.5, fadeout=0.1))

    return file.getvalue().encode()
Exemplo n.º 43
0
def res_set_tile(inst: Entity, res: Property) -> None:
    """Set 4x4 parts of a tile to the given values.

    `Offset` defines the position of the upper-left tile in the grid.
    Each `Tile` section defines a row of the positions to edit like so:
        "Tile" "bbbb"
        "Tile" "b..b"
        "Tile" "b..b"
        "Tile" "bbbb"
    If `Force` is true, the specified tiles will override any existing ones
    and create the tile if necessary.
    Otherwise they will be merged in - white/black tiles will not replace
    tiles set to nodraw or void for example.
    `chance`, if specified allows producing irregular tiles by randomly not
    changing the tile.

    If you need less regular placement (other orientation, precise positions)
    use a bee2_template_tilesetter in a template.

    Allowed tile characters:
    - `W`: White tile.
    - `w`: White 4x4 only tile.
    - `B`: Black tile.
    - `b`: Black 4x4 only tile.
    - `g`: The side/bottom of goo pits.
    - `n`: Nodraw surface.
    - `i`: Invert the tile surface, if black/white.
    - `1`: Convert to a 1x1 only tile, if a black/white tile.
    - `4`: Convert to a 4x4 only tile, if a black/white tile.
    - `.`: Void (remove the tile in this position).
    - `_` or ` `: Placeholder (don't modify this space).
    - `x`: Cutout Tile (Broken)
    - `o`: Cutout Tile (Partial)
    """
    origin = Vec.from_str(inst['origin'])
    orient = Matrix.from_angle(Angle.from_str(inst['angles']))

    offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin

    norm = round(orient.up(), 6)

    force_tile = res.bool('force')

    tiles: list[str] = [
        row.value for row in res if row.name in ('tile', 'tiles')
    ]
    if not tiles:
        raise ValueError('No "tile" parameters in SetTile!')

    chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0)
    if chance < 100.0:
        rng = rand.seed(b'tile', inst, res['seed', ''])
    else:
        rng = None

    for y, row in enumerate(tiles):
        for x, val in enumerate(row):
            if val in '_ ':
                continue

            if rng is not None and rng.uniform(0, 100) > chance:
                continue

            pos = Vec(32 * x, -32 * y, 0) @ orient + offset

            if val == '4':
                size = tiling.TileSize.TILE_4x4
            elif val == '1':
                size = tiling.TileSize.TILE_1x1
            elif val == 'i':
                size = None
            else:
                try:
                    new_tile = tiling.TILETYPE_FROM_CHAR[val]
                except KeyError:
                    LOGGER.warning('Unknown tiletype "{}"!', val)
                else:
                    tiling.edit_quarter_tile(pos, norm, new_tile, force_tile)
                continue

            # Edit the existing tile.
            try:
                tile, u, v = tiling.find_tile(pos, norm, force_tile)
            except KeyError:
                LOGGER.warning(
                    'Expected tile, but none found: {}, {}',
                    pos,
                    norm,
                )
                continue

            if size is None:
                # Invert the tile.
                tile[u, v] = tile[u, v].inverted
                continue

            # Unless forcing is enabled don't alter the size of GOO_SIDE.
            if tile[u, v].is_tile and tile[u,
                                           v] is not tiling.TileType.GOO_SIDE:
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tile[u, v].color)
            elif force_tile:
                # If forcing, make it black. Otherwise no need to change.
                tile[u, v] = tiling.TileType.with_color_and_size(
                    size, tiling.Portalable.BLACK)
Exemplo n.º 44
0
def brush_at_loc(
    inst: Entity,
    props: Property,
) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]:
    """Common code for posIsSolid and ReadSurfType.

    This returns the average tiletype, if both colors were found,
    and a set of all types found.
    """
    origin = Vec.from_str(inst['origin'])
    angles = Vec.from_str(inst['angles'])

    # Allow using pos1 instead, to match pos2.
    pos = props.vec('pos1' if 'pos1' in props else 'pos')
    pos.z -= 64  # Subtract so origin is the floor-position

    pos.localise(origin, angles)

    norm = props.vec('dir', 0, 0, 1).rotate(*angles)

    if props.bool('gridpos') and norm is not None:
        for axis in 'xyz':
            # Don't realign things in the normal's axis -
            # those are already fine.
            if norm[axis] == 0:
                pos[axis] = pos[axis] // 128 * 128 + 64

    result_var = props['setVar', '']
    # RemoveBrush is the pre-tiling name.
    should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False))

    tile_types: Set[tiling.TileType] = set()
    both_colors = False

    if 'pos2' in props:
        pos2 = props.vec('pos2')
        pos2.z -= 64  # Subtract so origin is the floor-position
        pos2.localise(origin, angles)

        bbox_min, bbox_max = Vec.bbox(pos, pos2)

        white_count = black_count = 0

        for pos in Vec.iter_grid(bbox_min, bbox_max, 32):
            try:
                tiledef, u, v = tiling.find_tile(pos, norm)
            except KeyError:
                continue

            tile_type = tiledef[u, v]
            tile_types.add(tile_type)
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
            if tile_type.is_tile:
                if tile_type.color is tiling.Portalable.WHITE:
                    white_count += 1
                else:
                    black_count += 1

        both_colors = white_count > 0 and black_count > 0

        if white_count == black_count == 0:
            tile_type = tiling.TileType.VOID
            tile_types.add(tiling.TileType.VOID)
        elif white_count > black_count:
            tile_type = tiling.TileType.WHITE
        else:
            tile_type = tiling.TileType.BLACK
    else:
        # Single tile.
        try:
            tiledef, u, v = tiling.find_tile(pos, norm)
        except KeyError:
            tile_type = tiling.TileType.VOID
        else:
            tile_type = tiledef[u, v]
            if should_remove:
                tiledef[u, v] = tiling.TileType.VOID
        tile_types.add(tile_type)

    if result_var:
        if tile_type.is_tile:
            # Don't distinguish between 4x4, goo sides
            inst.fixup[result_var] = tile_type.color.value
        elif tile_type is tiling.TileType.VOID:
            inst.fixup[result_var] = 'none'
        else:
            inst.fixup[result_var] = tile_type.name.casefold()

    return tile_type, both_colors, tile_types
Exemplo n.º 45
0
def res_resizeable_trigger(vmf: VMF, res: Property):
    """Replace two markers with a trigger brush.  

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

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

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

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

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

    marker_names = set()

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

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

    item_id = res['markerItem']

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

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

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

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

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

        inst1 = mark1.inst
        inst2 = mark2.inst

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return RES_EXHAUSTED