Example #1
0
def sceneset(ctx: Context):
    """Chains a set of choreographed scenes together."""
    for ent in ctx.vmf.by_class['comp_choreo_sceneset']:
        scenes = [
            ent['scene{:02}'.format(i)] for i in range(1, 21)
            if ent['scene{:02}'.format(i)]
        ]
        if not scenes:
            LOGGER.warning(
                '"{}" at ({}) has no scenes!',
                ent['targetname'],
                ent['origin'],
            )
            continue

        if conv_bool(ent['play_dings']):
            scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd')
            scenes.append('scenes/npc/glados_manual/ding_off.vcd')
        delay = conv_float(ent['delay'], 0.1)
        only_once = conv_bool(ent['only_once'])

        ent.remove()

        start_ent = None

        name = ent['targetname'] or '_choreo_{}'.format(ent.id)
        for i, scene in enumerate(scenes):
            part = ctx.vmf.create_ent(
                classname='logic_choreographed_scene',
                targetname=('{}_{}'.format(name, i) if i > 0 else name),
                origin=ent['origin'],
                scenefile=scene,
            )
            if i + 1 < len(scenes):
                part.add_out(
                    Output(
                        'OnCompletion',
                        '{}_{}'.format(name, i + 1),
                        'Start',
                        delay=delay,
                    ))
            if only_once:
                # When started blank the name so it can't be triggered,
                # then clean up after finished
                part.add_out(
                    Output('OnStart', '!self', 'AddOutput', 'targetname '),
                    Output('OnCompletion', '!self', 'Kill'),
                )
            if start_ent is None:
                start_ent = part

        assert start_ent is not None, "Has scenes but none made?"

        for out in ent.outputs:
            if out.output.casefold() == 'onstart':
                start_ent.add_out(out)
            elif out.output.casefold() == 'onfinish':
                # Part is the last in the loop.
                out.output = 'OnCompletion'
                part.add_out(out)
Example #2
0
def flag_brush_at_loc(inst: Entity, flag: Property):
    """Checks to see if a wall is present at the given location.

    - Pos is the position of the brush, where `0 0 0` is the floor-position
       of the brush.
    - Dir is the normal the face is pointing. (0 0 -1) is 'up'.
    - Type defines the type the brush must be:
      - "Any" requires either a black or white brush.
      - "None" means that no brush must be present.
      - "White" requires a portalable surface.
      - "Black" requires a non-portalable surface.
    - SetVar defines an instvar which will be given a value of "black",
      "white" or "none" to allow the result to be reused.
    - If gridPos is true, the position will be snapped so it aligns with
      the 128 brushes (Useful with fizzler/light strip items).
    - RemoveBrush: If set to 1, the brush will be removed if found.
      Only do this to EmbedFace brushes, since it will remove the other
      sides as well.
    """
    from conditions import VMF
    pos = Vec.from_str(flag['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos = pos.rotate_by_str(inst['angles', '0 0 0'])

    # Relative to the instance origin
    pos += Vec.from_str(inst['origin', '0 0 0'])

    norm = flag['dir', None]
    if norm is not None:
        norm = Vec.from_str(norm).rotate_by_str(inst['angles', '0 0 0'], )

    if srctools.conv_bool(flag['gridpos', '0']) 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 = flag['setVar', '']
    should_remove = srctools.conv_bool(flag['RemoveBrush', False], False)
    des_type = flag['type', 'any'].casefold()

    brush = SOLIDS.get(pos.as_tuple(), None)

    if brush is None or (norm is not None and abs(brush.normal) != abs(norm)):
        br_type = 'none'
    else:
        br_type = str(brush.color)
        if should_remove:
            VMF.remove_brush(brush.solid, )

    if result_var:
        inst.fixup[result_var] = br_type

    if des_type == 'any' and br_type != 'none':
        return True

    return des_type == br_type
Example #3
0
def res_unst_scaffold_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

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

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

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

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

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

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

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

    return group  # We look up the group name to find the values.
Example #4
0
def res_unst_scaffold_setup(res: Property):
    group = res["group", "DEFAULT_GROUP"]

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

    for block in res.find_all("Instance"):
        conf = {
            # If set, adjusts the offset appropriately
            "is_piston": srctools.conv_bool(block["isPiston", "0"]),
            "rotate_logic": srctools.conv_bool(block["AlterAng", "1"], True),
            "off_floor": Vec.from_str(block["FloorOff", "0 0 0"]),
            "off_wall": Vec.from_str(block["WallOff", "0 0 0"]),
            "logic_start": block["startlogic", ""],
            "logic_end": block["endLogic", ""],
            "logic_mid": block["midLogic", ""],
            "logic_start_rev": block["StartLogicRev", None],
            "logic_end_rev": block["EndLogicRev", None],
            "logic_mid_rev": block["EndLogicRev", None],
            "inst_wall": block["wallInst", ""],
            "inst_floor": block["floorInst", ""],
            "inst_offset": block["offsetInst", None],
            # Specially rotated to face the next track!
            "inst_end": block["endInst", None],
        }
        for logic_type in ("logic_start", "logic_mid", "logic_end"):
            if conf[logic_type + "_rev"] is None:
                conf[logic_type + "_rev"] = conf[logic_type]

        for inst in resolve_inst(block["file"]):
            targ_inst[inst] = conf

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

    return group  # We look up the group name to find the values.
Example #5
0
def test_conv_bool():
    """Test srctools.conv_bool()"""
    for val in true_strings:
        assert srctools.conv_bool(val)
    for val in false_strings:
        assert not srctools.conv_bool(val)

    # Check that bools pass through
    assert srctools.conv_bool(True)
    assert not srctools.conv_bool(False)

    # None passes through the default
    for val in def_vals:
        assert srctools.conv_bool(None, val) is val
Example #6
0
def test_conv_bool():
    """Test srctools.conv_bool()"""
    for val in true_strings:
        assert srctools.conv_bool(val)
    for val in false_strings:
        assert not srctools.conv_bool(val)

    # Check that bools pass through
    assert srctools.conv_bool(True)
    assert not srctools.conv_bool(False)

    # None passes through the default
    for val in def_vals:
        assert srctools.conv_bool(None, val) is val
Example #7
0
def do_item_optimisation(vmf: VMF):
    """Optimise redundant logic items."""
    needs_global_toggle = False

    for item in list(ITEMS.values()):
        # We can't remove items that have functionality, or don't have IO.
        if item.item_type is None or not item.item_type.input_type.is_logic:
            continue

        prim_inverted = conv_bool(
            conditions.resolve_value(
                item.inst,
                item.item_type.invert_var,
            ))

        sec_inverted = conv_bool(
            conditions.resolve_value(
                item.inst,
                item.item_type.sec_invert_var,
            ))

        # Don't optimise if inverted.
        if prim_inverted or sec_inverted:
            continue
        inp_count = len(item.inputs)
        if inp_count == 0:
            # Totally useless, remove.
            # We just leave the panel entities, and tie all the antlines
            # to the same toggle.
            needs_global_toggle = True
            for ent in item.antlines:
                ent['targetname'] = '_static_ind'

            del ITEMS[item.name]
            item.inst.remove()
        elif inp_count == 1:
            # Only one input, so AND or OR are useless.
            # Transfer input item to point to the output(s).
            collapse_item(item)

    # The antlines need a toggle entity, otherwise they'll copy random other
    # overlays.
    if needs_global_toggle:
        vmf.create_ent(
            classname='env_texturetoggle',
            origin=vbsp_options.get(Vec, 'global_ents_loc'),
            targetname='_static_ind_tog',
            target='_static_ind',
        )
Example #8
0
def res_rand_vec(inst: Entity, res: Property) -> None:
    """A modification to RandomNum which generates a random vector instead.

    'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z
    are for each section. If the min and max are equal that number will be used
    instead.
    """
    is_float = srctools.conv_bool(res['decimal'])
    var = res['resultvar', '$random']

    set_random_seed(inst, 'e' + res['seed', 'random'])

    if is_float:
        func = random.uniform
    else:
        func = random.randint

    value = Vec()

    for axis in 'xyz':
        max_val = srctools.conv_float(res['max_' + axis, 0.0])
        min_val = srctools.conv_float(res['min_' + axis, 0.0])
        if min_val == max_val:
            value[axis] = min_val
        else:
            value[axis] = func(min_val, max_val)

    inst.fixup[var] = value.join(' ')
def res_add_output_setup(res: Property):
    output = res['output']
    input_name = res['input']
    inst_in = res['inst_in', '']
    inst_out = res['inst_out', '']
    targ = res['target']
    only_once = srctools.conv_bool(res['only_once', None])
    times = 1 if only_once else srctools.conv_int(res['times', None], -1)
    delay = res['delay', '0.0']
    parm = res['parm', '']

    if output.startswith('<') and output.endswith('>'):
        out_id, out_type = output.strip('<>').split(':', 1)
        out_id = out_id.casefold()
        out_type = out_type.strip().casefold()
    else:
        out_id, out_type = output, 'const'

    return (
        out_type,
        out_id,
        targ,
        input_name,
        parm,
        delay,
        times,
        inst_in,
        inst_out,
    )
def load_settings(pit: Property):
    if not pit:
        SETTINGS.clear()
        # No pits are permitted..
        return

    SETTINGS.update({
        'use_skybox': srctools.conv_bool(pit['teleport', '0']),
        'tele_dest': pit['tele_target', '@goo_targ'],
        'tele_ref': pit['tele_ref', '@goo_ref'],
        'off_x': srctools.conv_int(pit['off_x', '0']),
        'off_y': srctools.conv_int(pit['off_y', '0']),
        'skybox': pit['sky_inst', ''],
        'skybox_ceil': pit['sky_inst_ceil', ''],
        'targ': pit['targ_inst', ''],
        'blend_light': pit['blend_light', '']
    })
    for inst_type in (
        'support',
        'side',
        'corner',
        'double',
        'triple',
        'pillar',
    ):
        vals = [
            prop.value
            for prop in
            pit.find_all(inst_type + '_inst')
        ]
        if len(vals) == 0:
            vals = [""]
        PIT_INST[inst_type] = vals
def res_rand_vec(inst: Entity, res: Property):
    """A modification to RandomNum which generates a random vector instead.

    'decimal', 'seed' and 'ResultVar' work like RandomNum. min/max x/y/z
    are for each section. If the min and max are equal that number will be used
    instead.
    """
    is_float = srctools.conv_bool(res['decimal'])
    var = res['resultvar', '$random']
    seed = res['seed', 'random']

    random.seed(inst['origin'] + inst['angles'] + 'random_' + seed)

    if is_float:
        func = random.uniform
    else:
        func = random.randint

    value = Vec()

    for axis in 'xyz':
        max_val = srctools.conv_float(res['max_' + axis, 0.0])
        min_val = srctools.conv_float(res['min_' + axis, 0.0])
        if min_val == max_val:
            value[axis] = min_val
        else:
            value[axis] = func(min_val, max_val)

    inst.fixup[var] = value.join(' ')
Example #12
0
def generate_resp_script(file, allow_dings):
    """Write the responses section into a file."""
    use_dings = allow_dings

    config = ConfigFile('resp_voice.cfg', root='bee2')
    file.write("BEE2_RESPONSES <- {\n")
    for section in QUOTE_DATA.find_key('CoopResponses', []):
        if not section.has_children() and section.name == 'use_dings':
            # Allow overriding specifically for the response script
            use_dings = srctools.conv_bool(section.value, allow_dings)
            continue

        voice_attr = RESP_HAS_NAMES.get(section.name, '')
        if voice_attr and not map_attr[voice_attr]:
            continue
            # This response catagory isn't present

        section_data = ['\t{} = [\n'.format(section.name)]
        for index, line in enumerate(section):
            if not config.getboolean(section.name, "line_" + str(index), True):
                # It's disabled!
                continue
            section_data.append('\t\tCreateSceneEntity("{}"),\n'.format(
                line['choreo']))
        if len(section_data) != 1:
            for line in section_data:
                file.write(line)
            file.write('\t],\n')
    file.write('}\n')

    file.write(
        'BEE2_PLAY_DING = {};\n'.format('true' if use_dings else 'false'))
Example #13
0
def res_rand_vec(inst: Entity, res: Property) -> None:
    """A modification to RandomNum which generates a random vector instead.

    `decimal`, `seed` and `ResultVar` work like RandomNum. `min_x`, `max_y` etc
    are used to define the boundaries. If the min and max are equal that number
    will be always used instead.
    """
    is_float = srctools.conv_bool(res['decimal'])
    var = res['resultvar', '$random']

    set_random_seed(inst, 'e' + res['seed', 'random'])

    if is_float:
        func = random.uniform
    else:
        func = random.randint

    value = Vec()

    for axis in 'xyz':
        max_val = srctools.conv_float(res['max_' + axis, 0.0])
        min_val = srctools.conv_float(res['min_' + axis, 0.0])
        if min_val == max_val:
            value[axis] = min_val
        else:
            value[axis] = func(min_val, max_val)

    inst.fixup[var] = value.join(' ')
Example #14
0
    def from_ent(cls, ent: Entity) -> Iterator[BBox]:
        """Parse keyvalues on a VMF entity. One bounding box is produced for each brush."""
        coll = CollideType.NOTHING
        for key, value in ent.keys.items():
            if key.casefold().startswith('coll_') and conv_bool(value):
                coll_name = key[5:].upper()
                try:
                    coll |= CollideType[coll_name]
                except KeyError:
                    LOGGER.warning('Invalid collide type: "{}"!', key)
        tags = frozenset(ent['tags'].split())

        for solid in ent.solids:
            mins, maxes = solid.get_bbox()
            non_skip_faces = [
                face
                for face in solid
                if face.mat != consts.Tools.SKIP
            ]
            try:
                # Only one non-skip face, "flatten" along its plane.
                face: Side
                [face] = non_skip_faces
            except ValueError:
                pass  # Regular bbox.
            else:
                plane_norm = face.normal()
                plane_point = face.planes[0]
                for point in [mins, maxes]:
                    # Get the offset from the plane, then subtract to force it onto the plane.
                    point -= plane_norm * Vec.dot(point - plane_point, plane_norm)

            yield cls(mins, maxes, contents=coll, tags=tags)
Example #15
0
def res_replace_instance(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.
    """
    import vbsp

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

    if not srctools.conv_bool(res['keep_instance', '0'], False):
        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 acess 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'

    vbsp.VMF.add_ent(new_ent)

    conditions.set_ent_keys(new_ent, inst, res)

    origin += Vec.from_str(new_ent['origin']).rotate_by_str(angles)
    new_ent['origin'] = origin
    new_ent['angles'] = angles
    new_ent['targetname'] = inst['targetname']
Example #16
0
    def set_opt(self, opt_name: str, value: str) -> None:
        """Set an option to a specific value."""
        folded_name = opt_name.casefold()
        for opt in self.defaults:
            if folded_name == opt.id:
                break
        else:
            LOGGER.warning('Invalid option name "{}"!', opt_name)
            return

        if opt.type is TYPE.RAW:
            if not isinstance(value, Property):
                raise ValueError('The value must be a Property '
                                 'for property blocks!')
            self.settings[opt.id] = value
        elif opt.type is TYPE.VEC:
            # Pass nones so we can check if it failed..
            parsed_vals = parse_vec_str(value, x=None)
            if parsed_vals[0] is None:
                return
            self.settings[opt.id] = Vec(*parsed_vals)
        elif opt.type is TYPE.BOOL:
            self.settings[opt.id] = conv_bool(value, self.settings[opt.id])
        else:  # int, float, str - no special handling...
            try:
                self.settings[opt.id] = opt.type.convert(value)
            except (ValueError, TypeError):
                pass
Example #17
0
def laser_catcher_skins(ctx: Context):
    """Fix Valve's bug where reloading saves causes lasers to get their skin wrong."""
    for ent in ctx.vmf.by_class['prop_laser_catcher']:
        if not conv_bool(ent['src_fix_skins'], True):
            continue

        deact_skin, act_skin = '23' if ent['SkinType'] == '1' else '01'

        # Look for outputs which do this already.
        name = ent['targetname']

        has_act = has_deact = False
        for out in ent.outputs:
            if has_act and has_deact:
                break
            if out.target == name or out.target == '!self':
                if out.input.casefold() == 'skin':
                    if out.params == act_skin:
                        has_act = True
                    elif out.params == act_skin:
                        has_act = True

        if not has_act:
            ent.add_out(Output('OnPowered', '!self', 'Skin', act_skin))
        if not has_deact:
            ent.add_out(Output('OnUnPowered', '!self', 'Skin', deact_skin))
Example #18
0
def laser_catcher_skins(ctx: Context):
    """Fix Valve's bug where reloading saves causes lasers to get their skin wrong."""
    for ent in ctx.vmf.by_class['prop_laser_catcher']:
        if not conv_bool(ent['src_fix_skins'], True):
            continue

        deact_skin, act_skin = '23' if ent['SkinType'] == '1' else '01'

        # Look for outputs which do this already.
        name = ent['targetname']

        has_act = has_deact = False
        for out in ent.outputs:
            if has_act and has_deact:
                break
            if out.target == name or out.target == '!self':
                if out.input.casefold() == 'skin':
                    if out.params == act_skin:
                        has_act = True
                    elif out.params == act_skin:
                        has_act = True

        if not has_act:
            ent.add_out(Output('OnPowered', '!self', 'Skin', act_skin))
        if not has_deact:
            ent.add_out(Output('OnUnPowered', '!self', 'Skin', deact_skin))
Example #19
0
def res_vactube_setup(res: Property):
    group = res['group', 'DEFAULT_GROUP']

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

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

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

            # Supports attach to the 4 sides of the straight part,
            # if there's a brush there.
            'support':
            block['support_inst', ''],
            'is_tsection':
            srctools.conv_bool(block['is_tsection', '0']),
            ('entry', 'wall'):
            block['entry_inst'],
            ('entry', 'floor'):
            block['entry_floor_inst'],
            ('entry', 'ceiling'):
            block['entry_ceil_inst'],
            'exit':
            block['exit_inst'],
        }

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

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

    return group
Example #20
0
def load(opt_blocks: Iterator[Property]):
    """Read settings from the given property block."""
    SETTINGS.clear()
    set_vals = {}
    for opt_block in opt_blocks:
        for prop in opt_block:
            set_vals[prop.name] = prop.value

    options = {opt.id: opt for opt in DEFAULTS}
    if len(options) != len(DEFAULTS):
        from collections import Counter

        # Find ids used more than once..
        raise Exception(
            "Duplicate option(s)! ({})".format(
                ", ".join(k for k, v in Counter(opt.id for opt in DEFAULTS).items() if v > 1)
            )
        )

    fallback_opts = []

    for opt in DEFAULTS:
        try:
            val = set_vals.pop(opt.id)
        except KeyError:
            if opt.fallback is not None:
                fallback_opts.append(opt)
                assert opt.fallback in options, "Invalid fallback in " + opt.id
            else:
                SETTINGS[opt.id] = opt.default
            continue
        if opt.type is TYPE.VEC:
            # Pass nones so we can check if it failed..
            parsed_vals = parse_vec_str(val, x=None)
            if parsed_vals[0] is None:
                SETTINGS[opt.id] = opt.default
            else:
                SETTINGS[opt.id] = Vec(*parsed_vals)
        elif opt.type is TYPE.BOOL:
            SETTINGS[opt.id] = srctools.conv_bool(val, opt.default)
        else:  # int, float, str - no special handling...
            try:
                SETTINGS[opt.id] = opt.type.value(val)
            except (ValueError, TypeError):
                SETTINGS[opt.id] = opt.default

    for opt in fallback_opts:
        try:
            SETTINGS[opt.id] = SETTINGS[opt.fallback]
        except KeyError:
            raise Exception('Bad fallback for "{}"!'.format(opt.id))
        # Check they have the same type.
        assert opt.type is options[opt.fallback].type

    if set_vals:
        LOGGER.warning("Extra config options: {}", set_vals)
def res_water_splash_setup(res: Property):
    parent = res['parent']
    name = res['name']
    scale = srctools.conv_float(res['scale', ''], 8.0)
    pos1 = Vec.from_str(res['position', ''])
    calc_type = res['type', '']
    pos2 = res['position2', '']
    fast_check = srctools.conv_bool(res['fast_check', ''])

    return name, parent, scale, pos1, pos2, calc_type, fast_check
Example #22
0
def flag_is_preview(flag: Property) -> bool:
    """Checks if the preview mode status equals the given value.

    If preview mode is enabled, the player will start before the entry
    door, and restart the map after reaching the exit door. If `False`,
    they start in the elevator.

    Preview mode is always `False` when publishing.
    """
    return vbsp.IS_PREVIEW == conv_bool(flag.value, False)
Example #23
0
def flag_is_preview(flag: Property) -> bool:
    """Checks if the preview mode status equals the given value.

    If preview mode is enabled, the player will start before the entry
    door, and restart the map after reaching the exit door. If `False`,
    they start in the elevator.

    Preview mode is always `False` when publishing.
    """
    return global_bool(vbsp.IS_PREVIEW == conv_bool(flag.value, False))
Example #24
0
def comp_relay(ctx: Context):
    """Implements comp_relay, allowing zero-overhead relay ents for managing outputs.

    These are collapsed into their callers.
    """
    # Output -> input that we convert.
    out_names = {
        'ontrigger': 'trigger',
        'onturnedon': 'turnon',
        'onturnedoff': 'turnoff',
    }
    # Add user outputs as well.
    for i in '12345678':
        out_names['onuser' + i] = 'fireuser' + i

    for relay in ctx.vmf.by_class['comp_relay']:
        # First, see if any entities exist with the same name that aren't
        # comp_relays. In that case, we need to keep the inputs.
        relay_name = relay['targetname']
        should_remove = not any(ent['classname'].casefold() != 'comp_relay'
                                for ent in ctx.vmf.by_target[relay_name])
        # If ctrl_type is 0, ctrl_value needs to be 1 to be enabled.
        # If ctrl_type is 1, ctrl_value needs to be 0 to be enabled.
        enabled = conv_bool(relay['ctrl_type']) != conv_bool(
            relay['ctrl_value'])
        for out in relay.outputs:
            try:
                inp_name = out_names[out.output.casefold()]
            except KeyError:
                LOGGER.warning(
                    'Unknown output "{}" on comp_relay "{}"!\n'
                    'This will be discarded.',
                    out.output,
                    relay_name,
                )
                continue
            if enabled:
                out.output = inp_name
                ctx.add_io_remap(relay_name, out, remove=should_remove)
            elif should_remove:  # Still add a remap, to remove the outputs.
                ctx.add_io_remap_removal(relay_name, inp_name)
        relay.remove()
Example #25
0
def force_paintinmap(ctx: Context):
    """If paint entities are present, set paint in map to true."""
    # Already set, don't bother confirming.
    if conv_bool(ctx.vmf.spawn['paintinmap']):
        return

    if needs_paint(ctx.vmf):
        ctx.vmf.spawn['paintinmap'] = '1'
        # Ensure we have some blobs.
        if conv_int(ctx.vmf.spawn['maxblobcount']) == 0:
            ctx.vmf.spawn['maxblobcount'] = '250'
Example #26
0
def force_paintinmap(ctx: Context):
    """If paint entities are present, set paint in map to true."""
    # Already set, don't bother confirming.
    if conv_bool(ctx.vmf.spawn['paintinmap']):
        return

    if needs_paint(ctx.vmf):
        ctx.vmf.spawn['paintinmap'] = '1'
        # Ensure we have some blobs.
        if conv_int(ctx.vmf.spawn['maxblobcount']) == 0:
            ctx.vmf.spawn['maxblobcount'] = '250'
Example #27
0
def load(opt_blocks: Iterator[Property]):
    """Read settings from the given property block."""
    SETTINGS.clear()
    set_vals = {}
    for opt_block in opt_blocks:
        for prop in opt_block:
            set_vals[prop.name] = prop.value

    options = {opt.id: opt for opt in DEFAULTS}
    if len(options) != len(DEFAULTS):
        from collections import Counter
        # Find ids used more than once..
        raise Exception('Duplicate option(s)! ({})'.format(', '.join(
            k for k, v in Counter(opt.id for opt in DEFAULTS).items()
            if v > 1)))

    fallback_opts = []

    for opt in DEFAULTS:
        try:
            val = set_vals.pop(opt.id)
        except KeyError:
            if opt.fallback is not None:
                fallback_opts.append(opt)
                assert opt.fallback in options, 'Invalid fallback in ' + opt.id
            else:
                SETTINGS[opt.id] = opt.default
            continue
        if opt.type is TYPE.VEC:
            # Pass nones so we can check if it failed..
            parsed_vals = parse_vec_str(val, x=None)
            if parsed_vals[0] is None:
                SETTINGS[opt.id] = opt.default
            else:
                SETTINGS[opt.id] = Vec(*parsed_vals)
        elif opt.type is TYPE.BOOL:
            SETTINGS[opt.id] = srctools.conv_bool(val, opt.default)
        else:  # int, float, str - no special handling...
            try:
                SETTINGS[opt.id] = opt.type.value(val)
            except (ValueError, TypeError):
                SETTINGS[opt.id] = opt.default

    for opt in fallback_opts:
        try:
            SETTINGS[opt.id] = SETTINGS[opt.fallback]
        except KeyError:
            raise Exception('Bad fallback for "{}"!'.format(opt.id))
        # Check they have the same type.
        assert opt.type is options[opt.fallback].type

    if set_vals:
        LOGGER.warning('Extra config options: {}', set_vals)
Example #28
0
def comp_trigger_coop(ctx: Context):
    """Creates a trigger which only activates with both players."""
    for trig in ctx.vmf.by_class['comp_trigger_coop']:
        trig['classname'] = 'trigger_playerteam'
        trig['target_team'] = 0

        only_once = conv_bool(trig['trigger_once'])
        trig['trigger_once'] = 0

        trig_name = trig['targetname']
        if not trig_name:
            # Give it something unique
            trig['targetname'] = trig_name = '_comp_trigger_coop_' + str(
                trig['hammer_id'])

        man_name = trig_name + '_man'

        manager = ctx.vmf.create_ent(
            classname='logic_coop_manager',
            origin=trig['origin'],
            targetname=man_name,
            # Should make it die if the trigger does.
            parentname=trig_name,
        )
        for out in list(trig.outputs):
            folded_out = out.output.casefold()
            if folded_out == 'onstarttouchboth':
                out.output = 'OnChangeToAllTrue'
            elif folded_out == 'onendtouchboth':
                out.output = 'OnChangeToAnyFalse'
            else:
                continue
            trig.outputs.remove(out)
            manager.add_out(out)
        trig.add_out(
            Output('OnStartTouchBluePlayer', man_name, 'SetStateATrue'),
            Output('OnStartTouchOrangePlayer', man_name, 'SetStateBrue'),
            Output('OnEndTouchBluePlayer', man_name, 'SetStateAFalse'),
            Output('OnEndTouchOrangePlayer', man_name, 'SetStateBFalse'),
        )

        if only_once:
            manager.add_out(
                Output('OnChangeToAllTrue', man_name, 'Kill'),
                Output('OnChangeToAllTrue', trig_name, 'Kill'),
            )
            # Only keep OnChangeToAllTrue outputs, and remove
            # them once they've fired.
            for out in list(manager):
                if out.output.casefold() == 'onchangetoalltrue':
                    out.only_once = True
                else:
                    manager.outputs.remove(out)
Example #29
0
def comp_trigger_coop(ctx: Context):
    """Creates a trigger which only activates with both players."""
    for trig in ctx.vmf.by_class['comp_trigger_coop']:
        trig['classname'] = 'trigger_playerteam'
        trig['target_team'] = 0
        
        only_once = conv_bool(trig['trigger_once'])
        trig['trigger_once'] = 0
        
        trig_name = trig['targetname']
        if not trig_name:
            # Give it something unique
            trig['targetname'] = trig_name = '_comp_trigger_coop_' + str(trig['hammer_id'])
            
        man_name = trig_name + '_man'
        
        manager = ctx.vmf.create_ent(
            classname='logic_coop_manager',
            origin=trig['origin'],
            targetname=man_name,
            # Should make it die if the trigger does.
            parentname=trig_name,
        )
        for out in list(trig.outputs):
            folded_out = out.output.casefold()
            if folded_out == 'onstarttouchboth':
                out.output = 'OnChangeToAllTrue'
            elif folded_out == 'onendtouchboth':
                out.output = 'OnChangeToAnyFalse'
            else:
                continue
            trig.outputs.remove(out)
            manager.add_out(out)
        trig.add_out(
            Output('OnStartTouchBluePlayer', man_name, 'SetStateATrue'),
            Output('OnStartTouchOrangePlayer', man_name, 'SetStateBrue'),
            Output('OnEndTouchBluePlayer', man_name, 'SetStateAFalse'),
            Output('OnEndTouchOrangePlayer', man_name, 'SetStateBFalse'),
        )
        
        if only_once:
            manager.add_out(
                Output('OnChangeToAllTrue', man_name, 'Kill'),
                Output('OnChangeToAllTrue', trig_name, 'Kill'),
            )
            # Only keep OnChangeToAllTrue outputs, and remove
            # them once they've fired.
            for out in list(manager):
                if out.output.casefold() == 'onchangetoalltrue':
                    out.only_once = True
                else:
                    manager.outputs.remove(out)
Example #30
0
def get_itemconf(
    name: Union[str, Tuple[str, str]],
    default: Optional[OptionType],
    timer_delay: int = None,
) -> Optional[OptionType]:
    """Get an itemconfig value.

    The name should be an 'ID:Section', or a tuple of the same.
    The type of the default sets what value it will be converted to.
    None returns the string, or None if not present.
    If set, timer_value is the value used for the timer.
    """
    if name == '':
        return default

    try:
        if isinstance(name, tuple):
            group_id, wid_id = name
        else:
            group_id, wid_id = name.split(':')
    except ValueError:
        LOGGER.warning('Invalid item config: {!r}!', name)
        return default

    wid_id = wid_id.casefold()

    if timer_delay is not None:
        if timer_delay < 3 or timer_delay > 30:
            wid_id += '_inf'
        else:
            wid_id += '_{}'.format(timer_delay)

    value = ITEM_CONFIG.get_val(group_id, wid_id, '')
    if not value:
        return default

    if isinstance(default, str) or default is None:
        return value
    elif isinstance(default, Vec):
        return Vec.from_str(value, default.x, default.y, default.z)
    elif isinstance(default, bool):
        return srctools.conv_bool(value, default)
    elif isinstance(default, float):
        return srctools.conv_int(value, default)
    elif isinstance(default, int):
        return srctools.conv_int(value, default)
    else:
        raise TypeError('Invalid default type "{}"!'.format(
            type(default).__name__))
Example #31
0
def res_change_inputs_setup(res: Property):
    vals = {}
    for prop in res:
        out_key = Output.parse_name(prop.real_name)
        if prop.has_children():
            vals[out_key] = (
                prop['inst_in', None],
                prop['input'],
                prop['params', ''],
                srctools.conv_float(prop['delay', 0.0]),
                1 if srctools.conv_bool(prop['only_once', '0']) else -1,
            )
        else:
            vals[out_key] = None
    return vals
Example #32
0
    def from_file(cls, path, zip_file):
        """Initialise from a file.

        path is the file path for the map inside the zip, without extension.
        zip_file is either a ZipFile or FakeZip object.
        """
        # Some P2Cs may have non-ASCII characters in descriptions, so we
        # need to read it as bytes and convert to utf-8 ourselves - zips
        # don't convert encodings automatically for us.
        try:
            with zip_open_bin(zip_file, path + '.p2c') as file:
                props = Property.parse(
                    # Decode the P2C as UTF-8, and skip unknown characters.
                    # We're only using it for display purposes, so that should
                    # be sufficent.
                    TextIOWrapper(
                        file,
                        encoding='utf-8',
                        errors='replace',
                    ),
                    path,
                )
        except KeyValError:
            # Silently fail if we can't parse the file. That way it's still
            # possible to backup.
            LOGGER.warning('Failed parsing puzzle file!', path, exc_info=True)
            props = Property('portal2_puzzle', [])
            title = None
            desc = _('Failed to parse this puzzle file. It can still be backed up.')
        else:
            props = props.find_key('portal2_puzzle', [])
            title = props['title', None]
            desc = props['description', _('No description found.')]



        if title is None:
            title = '<' + path.rsplit('/', 1)[-1] + '.p2c>'

        return cls(
            filename=os.path.basename(path),
            zip_file=zip_file,
            title=title,
            desc=desc,
            is_coop=srctools.conv_bool(props['coop', '0']),
            create_time=Date(props['timestamp_created', '']),
            mod_time=Date(props['timestamp_modified', '']),
        )
Example #33
0
    def from_file(cls, path, zip_file):
        """Initialise from a file.

        path is the file path for the map inside the zip, without extension.
        zip_file is either a ZipFile or FakeZip object.
        """
        # Some P2Cs may have non-ASCII characters in descriptions, so we
        # need to read it as bytes and convert to utf-8 ourselves - zips
        # don't convert encodings automatically for us.
        try:
            with zip_open_bin(zip_file, path + '.p2c') as file:
                props = Property.parse(
                    # Decode the P2C as UTF-8, and skip unknown characters.
                    # We're only using it for display purposes, so that should
                    # be sufficent.
                    TextIOWrapper(
                        file,
                        encoding='utf-8',
                        errors='replace',
                    ),
                    path,
                )
        except KeyValError:
            # Silently fail if we can't parse the file. That way it's still
            # possible to backup.
            LOGGER.warning('Failed parsing puzzle file!', path, exc_info=True)
            props = Property('portal2_puzzle', [])
            title = None
            desc = _('Failed to parse this puzzle file. It can still be backed up.')
        else:
            props = props.find_key('portal2_puzzle', [])
            title = props['title', None]
            desc = props['description', _('No description found.')]



        if title is None:
            title = '<' + path.rsplit('/', 1)[-1] + '.p2c>'

        return cls(
            filename=os.path.basename(path),
            zip_file=zip_file,
            title=title,
            desc=desc,
            is_coop=srctools.conv_bool(props['coop', '0']),
            create_time=Date(props['timestamp_created', '']),
            mod_time=Date(props['timestamp_modified', '']),
        )
Example #34
0
def widget_checkmark(parent: tk.Frame, var: tk.StringVar, conf: Property):
    """Allows ticking a box."""
    # Ensure it's a bool value.
    if conv_bool(var.get()):
        var.set('1')
    else:
        var.set('0')

    return ttk.Checkbutton(
        parent,
        text='',
        variable=var,
        onvalue='1',
        offvalue='0',
        command=widget_sfx,
    )
Example #35
0
def widget_checkmark(parent: tk.Frame, var: tk.StringVar, conf: Property):
    """Allows ticking a box."""
    # Ensure it's a bool value.
    if conv_bool(var.get()):
        var.set('1')
    else:
        var.set('0')

    return ttk.Checkbutton(
        parent,
        text='',
        variable=var,
        onvalue='1',
        offvalue='0',
        command=widget_sfx,
    )
Example #36
0
def flag_instvar(inst: Entity, flag: Property) -> bool:
    """Checks if the $replace value matches the given value.

    The flag value follows the form `A == B`, with any of the three permitted
    to be variables.
    The operator can be any of `=`, `==`, `<`, `>`, `<=`, `>=`, `!=`.
    If omitted, the operation is assumed to be `==`.
    If only a single value is present, it is tested as a boolean flag.
    """
    values = flag.value.split(' ', 3)
    if len(values) == 3:
        val_a, op, val_b = values
        op = inst.fixup.substitute(op)
        comp_func = INSTVAR_COMP.get(op, operator.eq)
    elif len(values) == 2:
        val_a, val_b = values
        op = '=='
        comp_func = operator.eq
    else:
        # For just a name.
        return conv_bool(inst.fixup.substitute(values[0]))
    if '$' not in val_a and '$' not in val_b:
        # Handle pre-substitute behaviour, where val_a is always a var.
        LOGGER.warning(
            'Comparison "{}" has no $var, assuming first value. '
            'Please use $ when referencing vars.',
            flag.value,
        )
        val_a = '$' + val_a

    val_a = inst.fixup.substitute(val_a, default='')
    val_b = inst.fixup.substitute(val_b, default='')
    try:
        # Convert to floats if possible, otherwise handle both as strings.
        # That ensures we normalise different number formats (1 vs 1.0)
        val_a, val_b = float(val_a), float(val_b)
    except ValueError:
        pass
    try:
        return comp_func(val_a, val_b)
    except (TypeError, ValueError) as e:
        LOGGER.warning('InstVar comparison failed: {} {} {}',
                       val_a,
                       op,
                       val_b,
                       exc_info=e)
        return False
Example #37
0
def res_cust_output_setup(res: Property):
    conds = [
        Condition.parse(sub_res) for sub_res in res
        if sub_res.name == 'targcondition'
    ]
    outputs = list(res.find_all('addOut'))
    dec_con_count = srctools.conv_bool(res["decConCount", '0'], False)
    sign_type = IND_PANEL_TYPES.get(res['sign_type', None], None)

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

    return outputs, dec_con_count, conds, sign_type, sign_act, sign_deact
Example #38
0
def res_hollow_brush(inst: Entity, res: Property):
    """Hollow out the attached brush, as if EmbeddedVoxel was set.

    This just removes the surface if it's already an embeddedVoxel. This allows
    multiple items to embed thinly in the same block without affecting each
    other.
    """
    loc = Vec(0, 0, -64).rotate_by_str(inst['angles'])
    loc += Vec.from_str(inst['origin'])

    try:
        group = SOLIDS[loc.as_tuple()]
    except KeyError:
        LOGGER.warning('No brush for hollowing at ({})', loc)
        return  # No brush here?

    conditions.hollow_block(group,
                            remove_orig_face=srctools.conv_bool(
                                res['RemoveFace', False]))
def main():
    global OPTIMISE

    OPTIMISE = conv_bool(input('Optimise zips? '))

    print('Optimising: ', OPTIMISE)

    zip_path = os.path.join(
        os.getcwd(),
        'zips',
        'sml' if OPTIMISE else 'lrg',
    )
    if os.path.isdir(zip_path):
        for file in os.listdir(zip_path):
            print('Deleting', file)
            os.remove(os.path.join(zip_path, file))
    else:
        os.makedirs(zip_path, exist_ok=True)

    shutil.rmtree('zips/hammer/', ignore_errors=True)

    path = os.path.join(
        os.getcwd(),
        'packages\\',
    )

    # A list of all the package zips.
    for package in search_folder(zip_path, path):
        build_package(*package)

    print('Building main zip...')

    pack_name = 'BEE{}_packages.zip'.format(input('Version: '))

    with ZipFile(os.path.join('zips', pack_name),
                 'w',
                 compression=ZIP_DEFLATED) as zip_file:
        for file in os.listdir(zip_path):
            zip_file.write(os.path.join(zip_path, file),
                           os.path.join('packages/', file))
            print('.', end='', flush=True)
    print('Done!')
Example #40
0
def main():
    global OPTIMISE

    OPTIMISE = conv_bool(input('Optimise zips? '))

    print('Optimising: ', OPTIMISE)

    zip_path = os.path.join(
        os.getcwd(),
        'zips',
        'sml' if OPTIMISE else 'lrg',
    )
    if os.path.isdir(zip_path):
        for file in os.listdir(zip_path):
            print('Deleting', file)
            os.remove(os.path.join(zip_path, file))
    else:
        os.makedirs(zip_path, exist_ok=True)

    path = os.path.join(
        os.getcwd(),
        'packages\\',
    )

    # A list of all the package zips.
    packages = list(search_folder(zip_path, path))

    with futures.ThreadPoolExecutor(10) as future:
        list(future.map(build_package, packages))

    print('Building main zip...')

    pack_name = 'BEE{}_packages.zip'.format(input('Version: '))

    with ZipFile(os.path.join('zips', pack_name),
                 'w',
                 compression=ZIP_DEFLATED) as zip_file:
        for file in os.listdir(zip_path):
            zip_file.write(os.path.join(zip_path, file),
                           os.path.join('packages/', file))
            print('.', end='', flush=True)
    print('Done!')
Example #41
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:
    - `Angle`: 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 = srctools.conv_bool(flag['allow_inverse', '0'])
    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
        )
Example #42
0
def res_rand_num(inst: Entity, res: Property) -> None:
    """Generate a random number and save in a fixup value.

    If 'decimal' is true, the value will contain decimals. 'max' and 'min' are
    inclusive. 'ResultVar' is the variable the result will be saved in.
    If 'seed' is set, it will be used to keep the value constant across
    map recompiles. This should be unique.
    """
    is_float = srctools.conv_bool(res['decimal'])
    max_val = srctools.conv_float(res['max', 1.0])
    min_val = srctools.conv_float(res['min', 0.0])
    var = res['resultvar', '$random']
    seed = 'd' + res['seed', 'random']

    set_random_seed(inst, seed)

    if is_float:
        func = random.uniform
    else:
        func = random.randint

    inst.fixup[var] = str(func(min_val, max_val))
Example #43
0
def precache_light_bridge(ctx: Context):
    """Ensure light bridges have the particle precached."""

    for bridge in ctx.vmf.by_class['prop_wall_projector']:
        if conv_bool(bridge['StartEnabled', '0']):
            return  # Starts on, no need.
        break
    else:
        # No bridges in the map.
        return

    for part in ctx.vmf.by_class['info_particle_system']:
        # Check for users already fixing the problem.
        if part['effect_name'].casefold() == 'projected_wall_impact':
            return

    ctx.vmf.create_ent(
        classname='info_particle_system',
        origin='-15872 -15872 -15872',
        effect_name='projected_wall_impact',
        start_active='0',
    )
Example #44
0
def main():
    global OPTIMISE
    
    gen_vpks()
    
    OPTIMISE = conv_bool(input('Optimise zips? '))
    
    print('Optimising: ', OPTIMISE)

    zip_path = os.path.join(
        os.getcwd(),
        'zips',
        'sml' if OPTIMISE else 'lrg',
    )
    if os.path.isdir(zip_path):
        for file in os.listdir(zip_path):
            print('Deleting', file)
            os.remove(os.path.join(zip_path, file))
    else:
        os.makedirs(zip_path, exist_ok=True)

    path = os.path.join(os.getcwd(), 'packages\\', )
    
    # A list of all the package zips.
    packages = list(search_folder(zip_path, path))

    with futures.ThreadPoolExecutor(10) as future:
        list(future.map(build_package, packages))

    print('Building main zip...')

    pack_name = 'BEE{}_packages.zip'.format(input('Version: '))
    
    with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file:
        for file in os.listdir(zip_path):
            zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file))
            print('.', end='', flush=True)
    print('Done!')
Example #45
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 srctools.conv_bool(res["allow_multiple", "0"]) 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 = Entity(
                vbsp.VMF,
                keys={
                    "classname": "func_instance",
                    "targetname": res["name", ""],
                    "file": resolve_inst(res["file"])[0],
                    "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()
            vbsp.VMF.add_ent(new_inst)
    return RES_EXHAUSTED
def main():
    global OPTIMISE
    
    OPTIMISE = conv_bool(input('Optimise zips? '))
    
    print('Optimising: ', OPTIMISE)

    zip_path = os.path.join(
        os.getcwd(),
        'zips',
        'sml' if OPTIMISE else 'lrg',
    )
    if os.path.isdir(zip_path):
        for file in os.listdir(zip_path):
            print('Deleting', file)
            os.remove(os.path.join(zip_path, file))
    else:
        os.makedirs(zip_path, exist_ok=True)

    shutil.rmtree('zips/hammer/', ignore_errors=True)

    path = os.path.join(os.getcwd(), 'packages\\', )
    
    # A list of all the package zips.
    for package in search_folder(zip_path, path):
        build_package(*package)

    print('Building main zip...')

    pack_name = 'BEE{}_packages.zip'.format(input('Version: '))
    
    with ZipFile(os.path.join('zips', pack_name), 'w', compression=ZIP_DEFLATED) as zip_file:
        for file in os.listdir(zip_path):
            zip_file.write(os.path.join(zip_path, file), os.path.join('packages/', file))
            print('.', end='', flush=True)
    print('Done!')
Example #47
0
def needs_paint(vmf: VMF) -> bool:
    """Check if we have paint."""
    for ent_cls in [
        'prop_paint_bomb',
        'paint_sphere',
        'weapon_paintgun',  # Not in retail but someone might add it.
    ]:
        if vmf.by_class[ent_cls]:
            return True

    for ent in vmf.by_class['info_paint_sprayer']:
        # Special case, this makes sprayers only render visually, which
        # works even without the value set.
        if not conv_bool(ent['DrawOnly']):
            return True

    for ent_cls in [
        'prop_weighted_cube',
        'prop_physics_paintable',
    ]:
        for ent in vmf.by_class[ent_cls]:
            # If the cube is bouncy, enable paint.
            if conv_int(ent['paintpower', '4'], 4) != 4:
                return True
Example #48
0
def res_translate_inst(inst: Entity, res: Property):
    """Translate the instance locally by the given amount.

    The special values <piston>, <piston_bottom> and <piston_top> can be
    used to offset it based on the starting position, bottom or top position
    of a piston platform.
    """
    folded_val = res.value.casefold()
    if folded_val == '<piston>':
        folded_val = (
            '<piston_top>' if
            srctools.conv_bool(inst.fixup['$start_up'])
            else '<piston_bottom>'
        )

    if folded_val == '<piston_top>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$top_level', '1'], 1))
    elif folded_val == '<piston_bottom>':
        val = Vec(z=128 * srctools.conv_int(inst.fixup['$bottom_level', '0'], 0))
    else:
        val = Vec.from_str(res.value)

    offset = val.rotate_by_str(inst['angles'])
    inst['origin'] = (offset + Vec.from_str(inst['origin'])).join(' ')
Example #49
0
def res_cutout_tile(res: Property):
    """Generate random quarter tiles, like in Destroyed or Retro maps.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    # Now count boundries near tiles, then generate them.

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

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

        max_x = max_y = 0

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

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

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

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

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

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

    add_floor_sides(floor_edges)

    conditions.reallocate_overlays(overlay_ids)

    return conditions.RES_EXHAUSTED
Example #50
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
Example #51
0
def res_fizzler_pair(begin_inst: Entity, res: Property):
    """Modify the instance of a fizzler to link with its pair.

    Each pair will be given a name along the lines of "fizz_name-model1334".
    Values:
        - StartInst, EndInst: The instances used for each end
        - MidInst: An instance placed every 128 units between emitters.
        - SingleInst: If the models are 1 block apart, replace both with this
            instance.
    """
    orig_target = begin_inst['targetname']

    if 'modelEnd' in orig_target:
        return  # We only execute starting from the start side.

    orig_target = orig_target[:-11]  # remove "_modelStart"
    end_name = orig_target + '_modelEnd'  # What we search for

    # The name all these instances get
    if srctools.conv_bool(res['uniqueName', '1'], True):
        pair_name = orig_target + '-model' + str(begin_inst.id)
    else:
        pair_name = orig_target

    orig_file = begin_inst['file']

    begin_file = res['StartInst', orig_file]
    end_file = res['EndInst', orig_file]
    mid_file = res['MidInst', '']
    single_file = res['SingleInst', '']

    begin_inst['file'] = begin_file
    begin_inst['targetname'] = pair_name

    direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles'])

    begin_pos = Vec.from_str(begin_inst['origin'])
    axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()]
    for end_inst in vbsp.VMF.by_class['func_instance']:
        if end_inst['targetname', ''] != end_name:
            # Only examine this barrier hazard's instances!
            continue
        if end_inst['file'] != orig_file:
            # Allow adding overlays or other instances at the ends.
            continue
        end_pos = Vec.from_str(end_inst['origin'])
        if (
                begin_pos[axis_1] == end_pos[axis_1] and
                begin_pos[axis_2] == end_pos[axis_2]
        ):
            length = int(end_pos[main_axis] - begin_pos[main_axis])
            break
    else:
        LOGGER.warning('No matching pair for {}!!', orig_target)
        return

    if single_file and length == 0:
        end_inst.remove()
        begin_inst['file'] = single_file
        return

    end_inst['targetname'] = pair_name
    end_inst['file'] = end_file

    if mid_file != '':
        # Go 64 from each side, and always have at least 1 section
        # A 128 gap will have length = 0
        for dis in range(0, abs(length) + 1, 128):
            new_pos = begin_pos + direction * dis
            vbsp.VMF.create_ent(
                classname='func_instance',
                targetname=pair_name,
                angles=begin_inst['angles'],
                file=mid_file,
                origin=new_pos.join(' '),
            )
Example #52
0
def res_unst_scaffold(res: Property):
    """The condition to generate Unstationary Scaffolds.

    This is executed once to modify all instances.
    """
    # The instance types we're modifying
    if res.value not in SCAFFOLD_CONFIGS:
        # We've already executed this config group
        return RES_EXHAUSTED

    LOGGER.info("Running Scaffold Generator ({})...", res.value)
    TARG_INST, LINKS = SCAFFOLD_CONFIGS[res.value]
    del SCAFFOLD_CONFIGS[res.value]  # Don't let this run twice

    instances = {}
    # Find all the instances we're wanting to change, and map them to
    # targetnames
    for ent in vbsp.VMF.by_class["func_instance"]:
        file = ent["file"].casefold()
        targ = ent["targetname"]
        if file not in TARG_INST:
            continue
        config = TARG_INST[file]
        next_inst = set(out.target for out in ent.outputs)
        # Destroy these outputs, they're useless now!
        ent.outputs.clear()
        instances[targ] = {"ent": ent, "conf": config, "next": next_inst, "prev": None}

    # Now link each instance to its in and outputs
    for targ, inst in instances.items():
        scaff_targs = 0
        for ent_targ in inst["next"]:
            if ent_targ in instances:
                instances[ent_targ]["prev"] = targ
                inst["next"] = ent_targ
                scaff_targs += 1
            else:
                # If it's not a scaffold, it's probably an indicator_toggle.
                # We want to remove any them as well as the assoicated
                # antlines!
                for toggle in vbsp.VMF.by_target[ent_targ]:
                    conditions.remove_ant_toggle(toggle)
        if scaff_targs > 1:
            raise Exception("A scaffold item has multiple destinations!")
        elif scaff_targs == 0:
            inst["next"] = None  # End instance

    starting_inst = []
    # We need to find the start instances, so we can set everything up
    for inst in instances.values():
        if inst["prev"] is None and inst["next"] is None:
            # Static item!
            continue
        elif inst["prev"] is None:
            starting_inst.append(inst)

    # We need to make the link entities unique for each scaffold set,
    # otherwise the AllVar property won't work.
    group_counter = 0

    # Set all the instances and properties
    for start_inst in starting_inst:
        group_counter += 1
        ent = start_inst["ent"]
        for vals in LINKS.values():
            if vals["all"] is not None:
                ent.fixup[vals["all"]] = SCAFF_PATTERN.format(name=vals["name"], group=group_counter, index="*")

        should_reverse = srctools.conv_bool(ent.fixup["$start_reversed"])

        # Now set each instance in the chain, including first and last
        for index, inst in enumerate(scaff_scan(instances, start_inst)):
            ent, conf = inst["ent"], inst["conf"]
            orient = "floor" if Vec(0, 0, 1).rotate_by_str(ent["angles"]) == (0, 0, 1) else "wall"

            # Find the offset used for the logic ents
            offset = (conf["off_" + orient]).copy()
            if conf["is_piston"]:
                # Adjust based on the piston position
                offset.z += 128 * srctools.conv_int(
                    ent.fixup["$top_level" if ent.fixup["$start_up"] == "1" else "$bottom_level"]
                )
            offset.rotate_by_str(ent["angles"])
            offset += Vec.from_str(ent["origin"])

            if inst["prev"] is None:
                link_type = "start"
            elif inst["next"] is None:
                link_type = "end"
            else:
                link_type = "mid"

            if orient == "floor" and link_type != "mid" and conf["inst_end"] is not None:
                # Add an extra instance pointing in the direction
                # of the connected track. This would be the endcap
                # model.
                other_ent = instances[inst["next" if link_type == "start" else "prev"]]["ent"]

                other_pos = Vec.from_str(other_ent["origin"])
                our_pos = Vec.from_str(ent["origin"])
                link_dir = other_pos - our_pos
                link_ang = math.degrees(math.atan2(link_dir.y, link_dir.x))
                # Round to nearest 90 degrees
                # Add 45 so the switchover point is at the diagonals
                link_ang = (link_ang + 45) // 90 * 90
                vbsp.VMF.create_ent(
                    classname="func_instance",
                    targetname=ent["targetname"],
                    file=conf["inst_end"],
                    origin=offset.join(" "),
                    angles="0 {:.0f} 0".format(link_ang),
                )
                # Don't place the offset instance, this replaces that!
            elif conf["inst_offset"] is not None:
                # Add an additional rotated entity at the offset.
                # This is useful for the piston item.
                vbsp.VMF.create_ent(
                    classname="func_instance",
                    targetname=ent["targetname"],
                    file=conf["inst_offset"],
                    origin=offset.join(" "),
                    angles=ent["angles"],
                )

            logic_inst = vbsp.VMF.create_ent(
                classname="func_instance",
                targetname=ent["targetname"],
                file=conf.get("logic_" + link_type + ("_rev" if should_reverse else ""), ""),
                origin=offset.join(" "),
                angles=("0 0 0" if conf["rotate_logic"] else ent["angles"]),
            )
            for key, val in ent.fixup.items():
                # Copy over fixup values
                logic_inst.fixup[key] = val

            # Add the link-values
            for linkVar, link in LINKS.items():
                logic_inst.fixup[linkVar] = SCAFF_PATTERN.format(name=link["name"], group=group_counter, index=index)
                if inst["next"] is not None:
                    logic_inst.fixup[link["next"]] = SCAFF_PATTERN.format(
                        name=link["name"], group=group_counter, index=index + 1
                    )

            new_file = conf.get("inst_" + orient, "")
            if new_file != "":
                ent["file"] = new_file

    LOGGER.info("Finished Scaffold generation!")
    return RES_EXHAUSTED
Example #53
0
def clean_vmf(vmf_path):
    """Optimise the VMFs, removing unneeded entities or objects."""
    inst = VMF.parse(vmf_path)

    for ent in itertools.chain([inst.spawn], inst.entities[:]):
        editor = ent.editor
        # Remove useless metadata
        for cat in ('comments', 'color', 'logicalpos'):
            if cat in editor:
                del editor[cat]

        # Remove entities that have their visgroups hidden.
        if ent.hidden or not conv_bool(editor.get('visgroupshown', '1'), True):
            print('Removing hidden ent')
            inst.remove_ent(ent)
            continue

        # Remove info_null entities
        if ent['classname'] == 'info_null':
            print('Removing info_null...')
            inst.remove_ent(ent)
            continue
            
        # All instances must be in bee2/, so any reference outside there is a map error!
        # It's ok if it's in p2editor and not in a subfolder though.
        # There's also an exception needed for the Tag gun instance.
        if ent['classname'] == 'func_instance':
            inst_loc = ent['file'].casefold().replace('\\','/')
            if not inst_loc.startswith('instances/bee2/') and not (inst_loc.startswith('instances/p2editor/') and inst_loc.count('/') == 2) and 'alatag' not in inst_loc:
                input('Invalid instance path "{}" in\n"{}"! Press Enter to continue..'.format(ent['file'], vmf_path))
                yield from clean_vmf(vmf_path) # Re-run so we check again..

        for solid in ent.solids[:]:
            if all(face.mat.casefold() == 'tools/toolsskip' for face in solid):
                print('Removing SKIP brush')
                ent.solids.remove(solid)
                continue

            if solid.hidden or not conv_bool(solid.editor.get('visgroupshown', '1'), True):
                print('Removing hidden brush')
                ent.solids.remove(solid)
                continue

    for detail in inst.by_class['func_detail']:
        # Remove several unused default options from func_detail.
        # We're not on xbox!
        del detail['disableX360']
        # These aren't used in any instances, and it doesn't seem as if
        # VBSP preserves these values anyway.
        del detail['maxcpulevel'], detail['mincpulevel']
        del detail['maxgpulevel'], detail['mingpulevel']

    # Since all VMFs are instances or similar (not complete maps), we'll never
    # use worldspawn's settings. Keep mapversion though.
    del inst.spawn['maxblobcount'], inst.spawn['maxpropscreenwidth']
    del inst.spawn['maxblobcount'],
    del inst.spawn['detailvbsp'], inst.spawn['detailmaterial']

    lines = inst.export(inc_version=False, minimal=True).splitlines()
    for line in lines:
        yield line.lstrip()
Example #54
0
def flag_brush_at_loc(vmf: VMF, inst: Entity, flag: Property):
    """Checks to see if a wall is present at the given location.

    - `Pos` is the position of the brush, where `0 0 0` is the floor-position
       of the brush.
    - `Dir` is the normal the face is pointing. `(0 0 -1)` is up.
    - `Type` defines the type the brush must be:
      - `Any` requires either a black or white brush.
      - `None` means that no brush must be present.
      - `White` requires a portalable surface.
      - `Black` requires a non-portalable surface.
    - `SetVar` defines an instvar which will be given a value of `black`,
      `white` or `none` to allow the result to be reused.
    - If `gridPos` is true, the position will be snapped so it aligns with
      the 128 grid (Useful with fizzler/light strip items).
    - `RemoveBrush`: If set to `1`, the brush will be removed if found.
      Only do this to `EmbedFace` brushes, since it will remove the other
      sides as well.
    """
    pos = Vec.from_str(flag['pos', '0 0 0'])
    pos.z -= 64  # Subtract so origin is the floor-position
    pos = pos.rotate_by_str(inst['angles', '0 0 0'])

    # Relative to the instance origin
    pos += Vec.from_str(inst['origin', '0 0 0'])

    norm = flag['dir', None]
    if norm is not None:
        norm = Vec.from_str(norm).rotate_by_str(
            inst['angles', '0 0 0'],
        )

    if srctools.conv_bool(flag['gridpos', '0']) 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 = flag['setVar', '']
    should_remove = srctools.conv_bool(flag['RemoveBrush', False], False)
    des_type = flag['type', 'any'].casefold()

    brush = SOLIDS.get(pos.as_tuple(), None)

    if brush is None or (norm is not None and abs(brush.normal) != abs(norm)):
        br_type = 'none'
    else:
        br_type = str(brush.color)
        if should_remove:
            vmf.remove_brush(
                brush.solid,
            )

    if result_var:
        inst.fixup[result_var] = br_type

    if des_type == 'any' and br_type != 'none':
        return True

    return des_type == br_type
Example #55
0
def res_import_template(inst: Entity, res: Property):
    """Import a template VMF file, retexturing it to match orientation.

    It will be placed overlapping the given instance.  
    Options:  
    - ID: The ID of the template to be inserted. Add visgroups to additionally
            add after a colon, comma-seperated (temp_id:vis1,vis2)
    - 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
            face ID.
    - replaceBrush: The position of a brush to replace (0 0 0=the surface).
            This brush will be removed, and overlays will be fixed to use
            all faces with the same normal. Can alternately be a block:
            - Pos: The position to replace.
            - additionalIDs: Space-separated list of face IDs in the template
              to also fix for overlays. The surface should have close to a
              vertical normal, to prevent rescaling the overlay.
            - removeBrush: If true, the original brush will not be removed.
            - transferOverlay: Allow disabling transferring overlays to this
              template. The IDs will be removed instead. (This can be an instvar).
    - 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. If 'none' (default),
            they are ignored. If 'choose', one is chosen. If a number, that
            is the percentage chance for each visgroup to be added.
    - visgroup_force_var: If set and True, visgroup is ignored and all groups
            are added.
    - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names
            are local to the instance.
    """
    (
        orig_temp_id,
        replace_tex,
        force_colour,
        force_grid,
        force_type,
        replace_brush_pos,
        rem_replace_brush,
        transfer_overlays,
        additional_replace_ids,
        invert_var,
        color_var,
        visgroup_func,
        visgroup_force_var,
        key_block,
        outputs,
    ) = res.value

    if ':' in orig_temp_id:
        # Split, resolve each part, then recombine.
        temp_id, visgroup = orig_temp_id.split(':', 1)
        temp_id = (
            conditions.resolve_value(inst, temp_id) + ':' +
            conditions.resolve_value(inst, visgroup)
        )
    else:
        temp_id = conditions.resolve_value(inst, orig_temp_id)

    if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)):
        def visgroup_func(group):
            """Use all the groups."""
            yield from group

    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

    if color_var.casefold() == '<editor>':
        # Check traits for the colour it should be.
        traits = instance_traits.get(inst)
        if 'white' in traits:
            force_colour = template_brush.MAT_TYPES.white
        elif 'black' in traits:
            force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white
        elif color_val == 'black':
            force_colour = template_brush.MAT_TYPES.black
    # else: no color var

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

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

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

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

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

        # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't
        # modify it.
        vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail)

    try:
        # This is the original brush the template is replacing. We fix overlay
        # face IDs, so this brush is replaced by the faces in the template
        # pointing
        # the same way.
        if replace_brush_pos is None:
            raise KeyError  # Not set, raise to jump out of the try block

        pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z)
        pos += origin
        brush_group = SOLIDS[pos.as_tuple()]
    except KeyError:
        # Not set or solid group doesn't exist, skip..
        pass
    else:
        LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces)
        conditions.steal_from_brush(
            temp_data,
            brush_group,
            rem_replace_brush,
            map(int, additional_replace_ids | template.overlay_faces),
            conv_bool(conditions.resolve_value(inst, transfer_overlays), True),
        )

    template_brush.retexture_template(
        temp_data,
        origin,
        inst.fixup,
        replace_tex,
        force_colour,
        force_grid,
        # Don't allow clumping if using custom keyvalues - then it won't be edited.
        no_clumping=key_block is not None,
    )
Example #56
0
def sceneset(ctx: Context):
    """Chains a set of choreographed scenes together."""
    for ent in ctx.vmf.by_class['comp_choreo_sceneset']:
        scenes = [
            ent['scene{:02}'.format(i)]
            for i in range(1, 21)
            if ent['scene{:02}'.format(i)]
        ]
        if not scenes:
            LOGGER.warning(
                '"{}" at ({}) has no scenes!',
                ent['targetname'],
                ent['origin'],
            )
            continue

        if conv_bool(ent['play_dings']):
            scenes.insert(0, 'scenes/npc/glados_manual/ding_on.vcd')
            scenes.append('scenes/npc/glados_manual/ding_off.vcd')
        delay = conv_float(ent['delay'], 0.1)
        only_once = conv_bool(ent['only_once'])

        ent.remove()

        start_ent = None

        name = ent['targetname'] or '_choreo_{}'.format(ent.id)
        for i, scene in enumerate(scenes):
            part = ctx.vmf.create_ent(
                classname='logic_choreographed_scene',
                targetname=(
                    '{}_{}'.format(name, i)
                    if i > 0 else
                    name
                ),
                origin=ent['origin'],
                scenefile=scene,
            )
            if i + 1 < len(scenes):
                part.add_out(Output(
                    'OnCompletion',
                    '{}_{}'.format(name, i+1),
                    'Start',
                    delay=delay,
                ))
            if only_once:
                # When started blank the name so it can't be triggered,
                # then clean up after finished
                part.add_out(
                    Output('OnStart', '!self', 'AddOutput', 'targetname '),
                    Output('OnCompletion', '!self', 'Kill'),
                )
            if start_ent is None:
                start_ent = part

        assert start_ent is not None, "Has scenes but none made?"

        for out in ent.outputs:
            if out.output.casefold() == 'onstart':
                start_ent.add_out(out)
            elif out.output.casefold() == 'onfinish':
                # Part is the last in the loop.
                out.output = 'OnCompletion'
                part.add_out(out)
Example #57
0
def mod_screenshots() -> None:
    """Modify the map's screenshot."""
    mod_type = CONF['screenshot_type', 'PETI'].lower()

    if mod_type == 'cust':
        LOGGER.info('Using custom screenshot!')
        scr_loc = CONF['screenshot', '']
    elif mod_type == 'auto':
        LOGGER.info('Using automatic screenshot!')
        scr_loc = None
        # The automatic screenshots are found at this location:
        auto_path = os.path.join(
            '..',
            GAME_FOLDER.get(CONF['game_id', ''], 'portal2'),
            'screenshots'
        )
        # We need to find the most recent one. If it's named
        # "previewcomplete", we want to ignore it - it's a flag
        # to indicate the map was playtested correctly.
        try:
            screens = [
                os.path.join(auto_path, path)
                for path in
                os.listdir(auto_path)
            ]
        except FileNotFoundError:
            # The screenshot folder doesn't exist!
            screens = []
        screens.sort(
            key=os.path.getmtime,
            reverse=True,
            # Go from most recent to least
        )
        playtested = False
        for scr_shot in screens:
            filename = os.path.basename(scr_shot)
            if filename.startswith('bee2_playtest_flag'):
                # Previewcomplete is a flag to indicate the map's
                # been playtested. It must be newer than the screenshot
                playtested = True
                continue
            elif filename.startswith('bee2_screenshot'):
                continue  # Ignore other screenshots

            # We have a screenshot. Check to see if it's
            # not too old. (Old is > 2 hours)
            date = datetime.fromtimestamp(
                os.path.getmtime(scr_shot)
            )
            diff = datetime.now() - date
            if diff.total_seconds() > 2 * 3600:
                LOGGER.info(
                    'Screenshot "{scr}" too old ({diff!s})',
                    scr=scr_shot,
                    diff=diff,
                )
                continue

            # If we got here, it's a good screenshot!
            LOGGER.info('Chosen "{}"', scr_shot)
            LOGGER.info('Map Playtested: {}', playtested)
            scr_loc = scr_shot
            break
        else:
            # If we get to the end, we failed to find an automatic
            # screenshot!
            LOGGER.info('No Auto Screenshot found!')
            mod_type = 'peti'  # Suppress the "None not found" error

        if srctools.conv_bool(CONF['clean_screenshots', '0']):
            LOGGER.info('Cleaning up screenshots...')
            # Clean up this folder - otherwise users will get thousands of
            # pics in there!
            for screen in screens:
                if screen != scr_loc and os.path.isfile(screen):
                    os.remove(screen)
            LOGGER.info('Done!')
    else:
        # PeTI type, or something else
        scr_loc = None

    if scr_loc is not None and os.path.isfile(scr_loc):
        # We should use a screenshot!
        for screen in find_screenshots():
            LOGGER.info('Replacing "{}"...', screen)
            # Allow us to edit the file...
            utils.unset_readonly(screen)
            shutil.copy(scr_loc, screen)
            # Make the screenshot readonly, so P2 can't replace it.
            # Then it'll use our own
            utils.set_readonly(screen)

    else:
        if mod_type != 'peti':
            # Error if we were looking for a screenshot
            LOGGER.warning('"{}" not found!', scr_loc)
        LOGGER.info('Using PeTI screenshot!')
        for screen in find_screenshots():
            # Make the screenshot writeable, so P2 will replace it
            LOGGER.info('Making "{}" replaceable...', screen)
            utils.unset_readonly(screen)
Example #58
0
def show_window(used_props, parent, item_name):
    global is_open, last_angle
    propList[:] = [key.casefold() for key in used_props]
    is_open = True
    spec_row = 1

    start_up = srctools.conv_bool(used_props.get('startup', '0'))
    values['startup'] = start_up
    for prop, value in used_props.items():
        if prop not in PROP_TYPES:
            LOGGER.info('Unknown property type {}', prop)
            continue
        if value is None:
            continue

        prop_type = PROP_TYPES[prop][0]
        if prop_type is PropTypes.CHECKBOX:
            values[prop].set(srctools.conv_bool(value))
        elif prop_type is PropTypes.OSCILLATE:
            values[prop].set(srctools.conv_bool(value))
            save_rail(prop)
        elif prop_type is PropTypes.GELS:
            values[prop].set(value)
        elif prop_type is PropTypes.PANEL:
            last_angle = value[5:7]
            values[prop].set(last_angle)
            out_values[prop] = value
        elif prop_type is PropTypes.PISTON:
            values[prop] = value
            try:
                top_level = int(used_props.get('toplevel', 4))
                bot_level = int(used_props.get('bottomlevel', 0))
            except ValueError:
                pass
            else:
                if ((prop == 'toplevel' and start_up) or
                        (prop == 'bottomlevel' and not start_up)):
                    widgets[prop].set(
                        max(
                            top_level,
                            bot_level,
                            )
                        )
                if ((prop == 'toplevel' and not start_up) or
                        (prop == 'bottomlevel' and start_up)):
                    widgets[prop].set(
                        min(
                            top_level,
                            bot_level,
                            )
                        )
        elif prop_type is PropTypes.TIMER:
            try:
                values[prop] = int(value)
                widgets[prop].set(values[prop])
            except ValueError:
                pass
        elif not prop_type.is_editable:
            # Internal or subtype properties, just pass through unchanged.
            values[prop] = value
        else:
            LOGGER.error('Bad prop_type ({}) for {}', prop_type, prop)

    for key in PROP_POS_SPECIAL:
        if key in propList:
            labels[key].grid(
                row=spec_row,
                column=0,
                sticky=E,
                padx=2,
                pady=5,
            )
            widgets[key].grid(
                row=spec_row,
                column=1,
                sticky="EW",
                padx=2,
                pady=5,
                columnspan=9,
                )
            spec_row += 1
        else:
            labels[key].grid_remove()
            widgets[key].grid_remove()
# if we have a 'special' prop, add the divider between the types
    if spec_row > 1:
        widgets['div_h'].grid(
            row=spec_row + 1,
            columnspan=9,
            sticky="EW",
            )
        spec_row += 2
    else:
        widgets['div_h'].grid_remove()
    ind = 0

    for key in PROP_POS:
        # Position each widget
        if key in propList:
            labels[key].grid(
                row=(ind // 3) + spec_row,
                column=(ind % 3) * 3,
                sticky=E,
                padx=2,
                pady=5,
                )
            widgets[key].grid(
                row=(ind // 3) + spec_row,
                column=(ind % 3)*3 + 1,
                sticky="EW",
                padx=2,
                pady=5,
                )
            ind += 1
        else:
            labels[key].grid_remove()
            widgets[key].grid_remove()

    if ind > 1:  # is there more than 1 checkbox? (add left divider)
        widgets['div_1'].grid(
            row=spec_row,
            column=2,
            sticky="NS",
            rowspan=(ind//3) + 1
            )
    else:
        widgets['div_1'].grid_remove()

    if ind > 2:  # are there more than 2 checkboxes? (add right divider)
        widgets['div_2'].grid(
            row=spec_row,
            column=5,
            sticky="NS",
            rowspan=(ind//3) + 1,
            )
    else:
        widgets['div_2'].grid_remove()

    if ind + spec_row == 1:
        # There aren't any items, display error message
        labels['noOptions'].grid(row=1, columnspan=9)
        ind = 1
    else:
        labels['noOptions'].grid_remove()

    widgets['saveButton'].grid(
        row=ind + spec_row,
        columnspan=9,
        sticky="EW",
        )

    # Block sound for the first few millisec to stop excess sounds from
    # playing
    sound.block_fx()

    widgets['titleLabel'].configure(text='Settings for "' + item_name + '"')
    win.title('BEE2 - ' + item_name)
    win.deiconify()
    win.lift(parent)
    win.grab_set()
    win.attributes("-topmost", True)
    win.geometry(
        '+' + str(parent.winfo_rootx() - 30) +
        '+' + str(parent.winfo_rooty() - win.winfo_reqheight() - 30)
        )

    if contextWin.is_open:
        # Temporarily hide the context window while we're open.
        contextWin.prop_window.withdraw()