Beispiel #1
0
def load_conf(prop_block: Property):
    """Read the config and build our dictionaries."""
    global INST_SPECIAL

    # Extra definitions: key -> filename.
    # Make sure to do this first, so numbered instances are set in
    # ITEM_FOR_FILE.
    for prop in prop_block.find_key('CustInstances', []):
        CUST_INST_FILES[prop.name] = special_inst = {}
        for inst in prop:
            file = inst.value.casefold()
            special_inst[inst.name] = file
            ITEM_FOR_FILE[file] = (prop.name, inst.name)

    # Normal instances: index -> filename
    for prop in prop_block.find_key('Allinstances', []):
        INSTANCE_FILES[prop.name] = inst_list = []
        for ind, inst in enumerate(prop):
            file = inst.value.casefold()
            inst_list.append(file)
            ITEM_FOR_FILE[file] = (prop.name, ind)

    INST_SPECIAL = {
        key.casefold(): resolve(val_string, silent=True)
        for key, val_string in
        SPECIAL_INST.items()
    }
Beispiel #2
0
    def parse(prop: Property):
        """Parse from property values.

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

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

        return AntTex(tex, scale, static)
Beispiel #3
0
def set_ent_keys(
    ent: Entity,
    inst: Entity,
    prop_block: Property,
    block_name: str='Keys',
) -> None:
    """Copy the given key prop block to an entity.

    This uses the keys and 'localkeys' properties on the prop_block.
    Values with $fixup variables will be treated appropriately.
    LocalKeys keys will be changed to use instance-local names, where needed.
    block_name lets you change the 'keys' suffix on the prop_block name.
    ent can be any mapping.
    """
    for prop in prop_block.find_key(block_name, []):
        ent[prop.real_name] = resolve_value(inst, prop.value)
    for prop in prop_block.find_key('Local' + block_name, []):
        if prop.value.startswith('$'):
            val = inst.fixup[prop.value]
        else:
            val = prop.value
        if val.startswith('@'):
            ent[prop.real_name] = val
        else:
            ent[prop.real_name] = local_name(inst, val)
Beispiel #4
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS in the entry corridor.

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

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

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

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

    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #5
0
def get_curr_settings() -> Property:
    """Return a property tree defining the current options."""
    props = Property('', [])

    for opt_id, opt_func in option_handler.items():
        opt_prop = opt_func()  # type: Property
        opt_prop.name = opt_id.title()
        props.append(opt_prop)

    return props
Beispiel #6
0
def res_make_tag_coop_spawn(vmf: VMF, inst: Entity, res: Property):
    """Create the spawn point for ATLAS, in Aperture Tag.

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

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

    offset = res.vec('origin').rotate_by_str(inst['angles'])
    normal = res.vec('facing', z=1).rotate_by_str(
        inst['angles'],
    )

    origin = Vec.from_str(inst['origin'])
    origin += offset
    angles = normal.to_angle()

    if is_tag:
        vmf.create_ent(
            classname='func_instance',
            targetname='paint_gun',
            origin=origin - (0, 0, 16),
            angles=angles,
            # Generated by the BEE2 app.
            file='instances/bee2/tag_coop_gun.vmf',
        )
        # Blocks ATLAS from having a gun
        vmf.create_ent(
            classname='info_target',
            targetname='supress_blue_portalgun_spawn',
            origin=origin,
            angles='0 0 0',
        )
        # Allows info_target to work
        vmf.create_ent(
            classname='env_global',
            targetname='no_spawns',
            globalstate='portalgun_nospawn',
            initialstate=1,
            spawnflags=1,  # Use initial state
            origin=origin,
        )
    vmf.create_ent(
        classname='info_coop_spawn',
        targetname='@coop_spawn_blue',
        ForceGunOnSpawn=int(not is_tag),
        origin=origin,
        angles=angles,
        enabled=1,
        StartingTeam=3,  # ATLAS
    )
    return RES_EXHAUSTED
Beispiel #7
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.
Beispiel #8
0
def widget_slider(parent: tk.Frame, var: tk.StringVar, conf: Property) -> tk.Widget:
    """Provides a slider for setting a number in a range."""
    scale = tk.Scale(
        parent,
        orient='horizontal',
        from_=conf.float('min'),
        to=conf.float('max', 100),
        resolution=conf.float('step', 1),
        variable=var,
        command=widget_sfx,
    )
    return scale
Beispiel #9
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.
Beispiel #10
0
    def save(self, ignore_readonly=False):
        """Save the palette file into the specified location.

        If ignore_readonly is true, this will ignore the `prevent_overwrite`
        property of the palette (allowing resaving those properties over old
        versions). Otherwise those palettes always create a new file.
        """
        LOGGER.info('Saving "{}"!', self.name)
        props = Property(None, [
            Property('Name', self.name),
            Property('TransName', self.trans_name),
            Property('ReadOnly', srctools.bool_as_int(self.prevent_overwrite)),
            Property('Items', [
                Property(item_id, str(subitem))
                for item_id, subitem in self.pos
            ])
        ])
        # If default, don't include in the palette file.
        # Remove the translated name, in case it's not going to write
        # properly to the file.
        if self.trans_name:
            props['Name'] = ''
        else:
            del props['TransName']

        if not self.prevent_overwrite:
            del props['ReadOnly']

        if self.settings is not None:
            self.settings.name = 'Settings'
            props.append(self.settings.copy())

        # We need to write a new file, determine a valid path.
        # Use a hash to ensure it's a valid path (without '-' if negative)
        # If a conflict occurs, add ' ' and hash again to get a different
        # value.
        if self.filename is None or (self.prevent_overwrite and not ignore_readonly):
            hash_src = self.name
            while True:
                hash_filename = str(abs(hash(hash_src))) + PAL_EXT
                if os.path.isfile(hash_filename):
                    # Add a random character to iterate the hash.
                    hash_src += chr(random.randrange(0x10ffff))
                else:
                    file = open(os.path.join(PAL_DIR, hash_filename), 'w', encoding='utf8')
                    self.filename = os.path.join(PAL_DIR, hash_filename)
                    break
        else:
            file = open(os.path.join(PAL_DIR, self.filename), 'w', encoding='utf8')
        with file:
            for line in props.export():
                file.write(line)
Beispiel #11
0
def res_breakable_glass_setup(res: Property):
    item_id = res['item']
    conf = {
        'template': template_brush.get_scaling_template(res['template']),
        'offset': res.float('offset', 0.5),
        # Distance inward from the frames the glass should span.
        'border_size': res.float('border_size', 0),
        'thickness': res.float('thickness', 4),
        }

    glass_item_setup(conf, item_id, BREAKABLE_GLASS_CONF)

    return res.value
Beispiel #12
0
def res_rand_inst_shift_setup(res: Property) -> tuple:
    min_x = res.float('min_x')
    max_x = res.float('max_x')
    min_y = res.float('min_y')
    max_y = res.float('max_y')
    min_z = res.float('min_z')
    max_z = res.float('max_z')

    return (
        min_x, max_x,
        min_y, max_y,
        min_z, max_z,
        'f' + res['seed', 'randomshift']
    )
Beispiel #13
0
    def init_trans(self):
        """Try and load a copy of basemodui from Portal 2 to translate.

        Valve's items use special translation strings which would look ugly
        if we didn't convert them.
        """
        # Already loaded
        if TRANS_DATA:
            return

        # We need to first figure out what language is used (if not English),
        # then load in the file. This is saved in the 'appmanifest',

        try:
            appman_file = open(self.abs_path("../../appmanifest_620.acf"))
        except FileNotFoundError:
            # Portal 2 isn't here...
            return
        with appman_file:
            appman = Property.parse(appman_file, "appmanifest_620.acf")
        try:
            lang = appman.find_key("AppState").find_key("UserConfig")["language"]
        except NoKeyError:
            return

        basemod_loc = self.abs_path("../Portal 2/portal2_dlc2/resource/basemodui_" + lang + ".txt")

        # Basemod files are encoded in UTF-16.
        try:
            basemod_file = open(basemod_loc, encoding="utf16")
        except FileNotFoundError:
            return
        with basemod_file:
            if lang == "english":

                def filterer(file):
                    """The English language has some unused language text.

                    This needs to be skipped since it has invalid quotes."""
                    for line in file:
                        if line.count('"') <= 4:
                            yield line

                basemod_file = filterer(basemod_file)

            trans_prop = Property.parse(basemod_file, "basemodui.txt")

        for item in trans_prop.find_key("lang", []).find_key("tokens", []):
            TRANS_DATA[item.real_name] = item.value
Beispiel #14
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', '']),
        )
Beispiel #15
0
def load_settings(pit: Property):
    if not pit:
        SETTINGS.clear()
        # No pits are permitted..
        return

    SETTINGS.update({
        'use_skybox': srctools.conv_bool(pit['teleport', '0']),
        'tele_dest': pit['tele_target', '@goo_targ'],
        'tele_ref': pit['tele_ref', '@goo_ref'],
        'off_x': srctools.conv_int(pit['off_x', '0']),
        'off_y': srctools.conv_int(pit['off_y', '0']),
        'skybox': pit['sky_inst', ''],
        'skybox_ceil': pit['sky_inst_ceil', ''],
        'targ': pit['targ_inst', ''],
        'blend_light': pit['blend_light', '']
    })
    for inst_type in (
        'support',
        'side',
        'corner',
        'double',
        'triple',
        'pillar',
    ):
        vals = [
            prop.value
            for prop in
            pit.find_all(inst_type + '_inst')
        ]
        if len(vals) == 0:
            vals = [""]
        PIT_INST[inst_type] = vals
Beispiel #16
0
def clean_text(file_path):
    # Try and parse as a property file. If it succeeds,
    # write that out - it removes excess whitespace between lines
    with open(file_path, 'r') as f:
        try: 
            props = Property.parse(f)
        except KeyValError:
            pass
        else:
            for line in props.export():
                yield line.lstrip()
            return
    
    with open(file_path, 'r') as f:
        for line in f:
            if line.isspace():
                continue
            if line.lstrip().startswith('//'):
                continue
            # Remove // comments, but only if the comment doesn't have
            # a quote char after it - it could be part of the string,
            # so leave it just to be safe.
            if '//' in line and '"' not in line:
                yield line.split('//')[0] + '\n'
            else:
                yield line.lstrip()
Beispiel #17
0
def res_monitor_setup(res: Property):
    return (
        res['breakInst', None],
        res['bullseye_name', ''],
        res.vec('bullseye_loc'),
        res['bullseye_parent', ''],
    )
Beispiel #18
0
def parse_packlists(props: Property) -> None:
    """Parse the packlists.cfg file, to load our packing lists."""
    for prop in props.find_children('Packlist'):
        PACKLISTS[prop.name] = {
            file.value
            for file in prop
        }
Beispiel #19
0
def write_sound(
    file: StringIO,
    snds: Property,
    pack_list: PackList,
    snd_prefix: str='*',
) -> None:
    """Write either a single sound, or multiple rndsound.

    snd_prefix is the prefix for each filename - *, #, @, etc.
    """
    if snds.has_children():
        file.write('"rndwave"\n\t{\n')
        for snd in snds:
            file.write(
                '\t"wave" "{sndchar}{file}"\n'.format(
                    file=snd.value.lstrip(SOUND_CHARS),
                    sndchar=snd_prefix,
                )
            )
            pack_list.pack_file('sound/' + snd.value.casefold())
        file.write('\t}\n')
    else:
        file.write(
            '"wave" "{sndchar}{file}"\n'.format(
                file=snds.value.lstrip(SOUND_CHARS),
                sndchar=snd_prefix,
            )
        )
        pack_list.pack_file('sound/' + snds.value.casefold())
Beispiel #20
0
def make_static_pist_setup(res: Property):
    instances = (
        'bottom_0', 'bottom_1', 'bottom_2', 'bottom_3',
        'logic_0', 'logic_1', 'logic_2', 'logic_3',
        'static_0', 'static_1', 'static_2', 'static_3', 'static_4',
        'grate_low', 'grate_high',
    )

    if res.has_children():
        # Pull from config
        return {
            name: instanceLocs.resolve_one(
                res[name, ''],
                error=False,
            ) for name in instances
        }
    else:
        # Pull from editoritems
        if ':' in res.value:
            from_item, prefix = res.value.split(':', 1)
        else:
            from_item = res.value
            prefix = ''
        return {
            name: instanceLocs.resolve_one(
                '<{}:bee2_{}{}>'.format(from_item, prefix, name),
                error=False,
            ) for name in instances
        }
Beispiel #21
0
def res_calc_opposite_wall_dist(inst: Entity, res: Property):
    """Calculate the distance between this item and the opposing wall.

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

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

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

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

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

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

    inst.fixup[result_var] = (origin - opposing_pos).mag() + dist_off
Beispiel #22
0
def res_cust_antline_setup(res: Property):
    if 'wall' in res:
        wall_type = antlines.AntType.parse(res.find_key('wall'))
    else:
        wall_type = None

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

    return (
        wall_type,
        floor_type,
        res.bool('remove_signs'),
        res['toggle_var', ''],
    )
Beispiel #23
0
def read_settings() -> None:
    """Read and apply the settings from disk."""
    try:
        file = open(utils.conf_location('config/config.vdf'), encoding='utf8')
    except FileNotFoundError:
        return
    with file:
        props = Property.parse(file)
    apply_settings(props)
Beispiel #24
0
    def export(
        self
    ) -> Tuple[List[EditorItem], Dict[RenderableType, Renderable], Property]:
        """Export this style, returning the vbsp_config and editoritems.

        This is a special case, since styles should go first in the lists.
        """
        vbsp_config = Property(None, [])
        vbsp_config += self.config.copy()

        return self.items, self.renderables, vbsp_config
Beispiel #25
0
def load_config():
    global CONF
    LOGGER.info('Loading Settings...')
    try:
        with open("bee2/vrad_config.cfg", encoding='utf8') as config:
            CONF = Property.parse(config, 'bee2/vrad_config.cfg').find_key(
                'Config', []
            )
    except FileNotFoundError:
        pass
    LOGGER.info('Config Loaded!')
Beispiel #26
0
def build_itemclass_dict(prop_block: Property):
    """Load in the dictionary mapping item classes to item ids"""
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class
Beispiel #27
0
 def parse(cls, prop: Property):
     """Parse this from a property block."""
     broken_chance = prop.float('broken_chance')
     tex_straight = []
     tex_corner = []
     brok_straight = []
     brok_corner = []
     for ant_list, name in zip(
         [tex_straight, tex_corner, brok_straight, brok_corner],
         ('straight', 'corner', 'broken_straight', 'broken_corner'),
     ):
         for sub_prop in prop.find_all(name):
             ant_list.append(AntTex.parse(sub_prop))
     return cls(
         tex_straight,
         tex_corner,
         brok_straight,
         brok_corner,
         broken_chance,
     )
Beispiel #28
0
def flag_blockpos_type(inst: Entity, flag: Property):
    """Determine the type of a grid position.

    If the value is single value, that should be the type.
    Otherwise, the value should be a block with 'offset' and 'type' values.
    The offset is in block increments, with 0 0 0 equal to the mounting surface.
    If 'offset2' is also provided, all positions in the bounding box will
    be checked.

    The type should be a space-seperated list of locations:
    * `VOID` (Outside the map)
    * `SOLID` (Full wall cube)
    * `EMBED` (Hollow wall cube)
    * `AIR` (Inside the map, may be occupied by items)
    * `OCCUPIED` (Known to be occupied by items)
    * `PIT` (Bottomless pits, any)
      * `PIT_SINGLE` (one-high)
      * `PIT_TOP`
      * `PIT_MID`
      * `PIT_BOTTOM`
    * `GOO`
      * `GOO_SINGLE` (one-deep goo)
      * `GOO_TOP` (goo surface)
      * `GOO_MID`
      * `GOO_BOTTOM` (floor)
    """
    pos2 = None

    if flag.has_children():
        pos1 = resolve_offset(inst, flag['offset', '0 0 0'], scale=128, zoff=-128)
        types = flag['type'].split()
        if 'offset2' in flag:
            pos2 = resolve_offset(inst, flag.value, scale=128, zoff=-128)
    else:
        types = flag.value.split()
        pos1 = Vec()

    if pos2 is not None:
        bbox = Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128)
    else:
        bbox = [pos1]

    for pos in bbox:
        block = brushLoc.POS['world': pos]
        for block_type in types:
            try:
                allowed = brushLoc.BLOCK_LOOKUP[block_type.casefold()]
            except KeyError:
                raise ValueError('"{}" is not a valid block type!'.format(block_type))
            if block in allowed:
                break  # To next position
        else:
            return False  # Didn't match any in this list.
    return True  # Matched all positions.
Beispiel #29
0
def res_make_tag_fizzler_setup(res: Property):
    """We need this to pre-parse the fizzler type."""
    if 'ioconf' in res:
        fizz_type = ItemType.parse('<TAG_FIZZER>', res.find_key('ioconf'))
    else:
        fizz_type = None

    # The distance from origin the double signs are seperated by.
    sign_offset = res.int('signoffset', 16)

    return (
        sign_offset,
        fizz_type,
        res['frame_double'],
        res['frame_single'],
        res['blue_sign', ''],
        res['blue_off_sign', ''],
        res['oran_sign', ''],
        res['oran_off_sign', ''],
    )
Beispiel #30
0
def build_itemclass_dict(prop_block: Property):
    """Load in the dictionary mapping item classes to item ids"""
    for prop in prop_block.find_children('ItemClasses'):
        try:
            it_class = consts.ItemClass(prop.value)
        except KeyError:
            LOGGER.warning('Unknown item class "{}"', prop.value)
            continue

        ITEMS_WITH_CLASS[it_class].append(prop.name)
        CLASS_FOR_ITEM[prop.name] = it_class
Beispiel #31
0
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

    Options:

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

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

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

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

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

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

    inst_normal = from_dir.rotate_by_str(angle)

    if normal == 'WALL':
        # Special case - it's not on the floor or ceiling
        return not (inst_normal == (0, 0, 1) or inst_normal == (0, 0, -1))
    else:
        return inst_normal == normal or (allow_inverse
                                         and -inst_normal == normal)
Beispiel #33
0
    def _parse_block(tok: Tokenizer, name: str) -> Property:
        """Parse a block into a block of properties."""
        prop = Property(name, [])

        for token, param_name in tok:
            # End of our block
            if token is Tok.BRACE_CLOSE:
                return prop
            elif token is Tok.NEWLINE:
                continue
            elif token is not Tok.STRING:
                raise tok.error(token)

            token, param_value = tok()

            if token is Tok.STRING:
                # We have the value.
                pass
            elif token is Tok.NEWLINE:
                # Name by itself: '%compilenodraw' etc...
                # We need to check there's a newline after that - for subblocks.
                token, ignored = tok()
                while token is Tok.NEWLINE:
                    token, ignored = tok()

                if token is Tok.BRACE_OPEN:
                    prop.append(Material._parse_block(tok, param_name))
                    continue
                elif token is Tok.NEWLINE:
                    pass
                elif token is Tok.BRACE_CLOSE:
                    # End of us after single name.
                    prop.append(Property(param_name, ''))
                    break
                else:
                    raise tok.error(token)
            else:
                raise tok.error(token)
            prop.append(Property(param_name, param_value))

        raise tok.error('EOF without closed block!')
Beispiel #34
0
def gen_sound_manifest(additional, excludes):
    """Generate a new game_sounds_manifest.txt file.

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

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

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

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

    for script in additional:
        scripts.append(script)

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

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

    inject_loc = os.path.join('bee2', 'inject', 'soundscript_manifest.txt')
    with open(inject_loc, 'w') as f:
        for line in new_props.export():
            f.write(line)
    LOGGER.info('Written new soundscripts_manifest..')
Beispiel #35
0
def res_pre_cache_model(vmf: VMF, res: Property) -> None:
    """Precache the given model for switching.

    This places it as a `prop_dynamic_override`.
    """
    if res.has_children():
        model = res['model']
        skins = [int(skin) for skin in res['skinset', ''].split()]
    else:
        model = res.value
        skins = []
    precache_model(vmf, model, skins)
Beispiel #36
0
    def clean_editor_models(self, editoritems: Property):
        """The game is limited to having 1024 models loaded at once.

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

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

        mdl_count = 0

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

                mdl_count += 1

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

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

        LOGGER.info('{}/{} editor models used.', len(used_models), mdl_count)
Beispiel #37
0
def res_set_voice_attr(res: Property):
    """Sets a number of Voice Attributes.

    Each child property will be set. The value is ignored, but must
    be present for syntax reasons.
    """
    if res.has_children():
        for opt in res.value:
            vbsp.settings['has_attr'][opt.name] = True
    else:
        vbsp.settings['has_attr'][res.value.casefold()] = True
    return RES_EXHAUSTED
Beispiel #38
0
def res_timed_relay_setup(res: Property):
    var = res['variable', consts.FixupVars.TIM_DELAY]
    name = res['targetname']
    disabled = res['disabled', '0']
    flags = res['spawnflags', '0']

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

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

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

    return var, name, disabled, flags, final_outs, rep_outs
Beispiel #39
0
def res_set_voice_attr(res: Property):
    """Sets a number of Voice Attributes.

        Each child property will be set. The value is ignored, but must
        be present for syntax reasons.
    """
    if res.has_children():
        for opt in res.value:
            vbsp.settings['has_attr'][opt.name] = True
    else:
        vbsp.settings['has_attr'][res.value.casefold()] = 1
    return RES_EXHAUSTED
Beispiel #40
0
def res_set_faith_setup(res: Property) -> tuple:
    temp_name = res['template', '']

    if temp_name:
        template = template_brush.get_template(temp_name)
    else:
        template = None

    return (
        template,
        res.vec('offset'),
    )
Beispiel #41
0
def read_configs(conf: Property):
    """Build our connection configuration from the config files."""
    for prop in conf.find_children('Connections'):
        if prop.name in ITEM_TYPES:
            raise ValueError('Duplicate item type "{}"'.format(prop.real_name))
        ITEM_TYPES[prop.name] = ItemType.parse(prop.real_name, prop)

    if 'item_indicator_panel' not in ITEM_TYPES:
        raise ValueError('No checkmark panel item type!')

    if 'item_indicator_panel_timer' not in ITEM_TYPES:
        raise ValueError('No timer panel item type!')
Beispiel #42
0
    def get(self, expected_type: type, name: str) -> Any:
        """Get the given option.
        expected_type should be the class of the value that's expected.
        The value can be None if unset, except for Property types (which
        will always have an empty block).

        If expected_type is an Enum, this will be used to convert the output.
        If it fails, a warning is produced and the first value in the enum is
        returned.
        """
        try:
            val = self.settings[name.casefold()]
        except KeyError:
            raise TypeError(
                'Option "{}" does not exist!'.format(name)) from None

        if val is None:
            if expected_type is Property:
                return Property(name, [])
            else:
                return None

        if issubclass(expected_type, Enum):
            enum_type = expected_type  # type: Optional[Type[Enum]]
            expected_type = str
        else:
            enum_type = None

        # Don't allow subclasses (bool/int)
        if type(val) is not expected_type:
            raise ValueError('Option "{}" is {} (code expected {})'.format(
                name,
                type(val),
                expected_type,
            ))

        if enum_type is not None:
            try:
                return enum_type(val)  # type: ignore
            except ValueError:
                LOGGER.warning(
                    'Option "{}" is not a valid value. '
                    'Allowed values are:\n{}', name,
                    '\n'.join([mem.value for mem in enum_type]))
                return next(iter(enum_type))  # type: ignore

        # Vec is mutable, don't allow modifying the original.
        if expected_type is Vec or expected_type is Property:
            assert isinstance(val, Vec) or isinstance(val, Property)
            return val.copy()
        else:
            assert isinstance(val, expected_type)
            return val
Beispiel #43
0
    def parse(cls, conf: Property) -> 'FizzlerBrush':
        """Parse from a config file."""
        if 'side_color' in conf:
            side_color = conf.vec('side_color')
        else:
            side_color = None

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

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

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

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

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

        return FizzlerBrush(
            name=conf['name'],
            textures=textures,
            keys=keys,
            local_keys=local_keys,
            outputs=outputs,
            thickness=conf.float('thickness', 2.0),
            stretch_center=conf.bool('stretch_center', True),
            side_color=side_color,
            singular=conf.bool('singular'),
            mat_mod_name=conf['mat_mod_name', None],
            mat_mod_var=conf['mat_mod_var', None],
            set_axis_var=conf.bool('set_axis_var'),
        )
Beispiel #44
0
    def __init__(
        self,
        style_id: str,
        selitem_data: 'SelitemData',
        items: List[EditorItem],
        renderables: Dict[RenderableType, Renderable],
        config=None,
        base_style: Optional[str] = None,
        suggested: Tuple[str, str, str, str] = None,
        has_video: bool = True,
        vpk_name: str = '',
        corridors: Dict[Tuple[str, int], CorrDesc] = None,
    ) -> None:
        self.id = style_id
        self.selitem_data = selitem_data
        self.items = items
        self.renderables = renderables
        self.base_style = base_style
        # Set by post_parse() after all objects are read.
        # this is a list of this style, plus parents in order.
        self.bases: List[Style] = []
        self.suggested = suggested or ('<NONE>', '<NONE>', 'SKY_BLACK',
                                       '<NONE>')
        self.has_video = has_video
        self.vpk_name = vpk_name
        self.corridors = {}

        for group, length in CORRIDOR_COUNTS.items():
            for i in range(1, length + 1):
                try:
                    self.corridors[group, i] = corridors[group, i]
                except KeyError:
                    self.corridors[group, i] = CorrDesc('', '', '')

        if config is None:
            self.config = Property(None, [])
        else:
            self.config = config

        set_cond_source(self.config, 'Style <{}>'.format(style_id))
Beispiel #45
0
def res_add_global_inst(res: Property):
    """Add one instance in a specific location.

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

    if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES:
        # By default we will skip adding the instance
        # if was already added - this is helpful for
        # items that add to original items, or to avoid
        # bugs.
        new_inst = vbsp.VMF.create_ent(
            classname="func_instance",
            targetname=res['name', ''],
            file=instanceLocs.resolve_one(res['file'], error=True),
            angles=res['angles', '0 0 0'],
            fixup_style=res['fixup_style', '0'],
        )
        try:
            new_inst['origin'] = res['position']
        except IndexError:
            new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc')
        GLOBAL_INSTANCES.add(res['file'])
        if new_inst['targetname'] == '':
            new_inst['targetname'] = "inst_"
            new_inst.make_unique()
    return RES_EXHAUSTED
Beispiel #46
0
    def __init__(self, path: Union[str, Path]):
        """Parse a game from a folder."""
        if isinstance(path, Path):
            self.path = path
        else:
            self.path = Path(path)
        with open(self.path / GINFO) as f:
            gameinfo = Property.parse(f).find_key('GameInfo')
        fsystems = gameinfo.find_key('Filesystem', [])

        self.game_name = gameinfo['Game']
        self.app_id = fsystems['SteamAppId']
        self.tools_id = fsystems['ToolsAppId', None]
        self.additional_content = fsystems['AdditionalContentId', None]
        self.fgd_loc = gameinfo['GameData', 'None']
        self.search_paths = []  # type: List[Path]

        for path in fsystems.find_children('SearchPaths'):
            exp_path = self.parse_search_path(path)
            # Expand /* if at the end of paths.
            if exp_path.name == '*':
                try:
                    self.search_paths.extend(
                        map(exp_path.parent.joinpath,
                            os.listdir(exp_path.parent)))
                except FileNotFoundError:
                    pass
            # Handle folder_* too.
            elif exp_path.name.endswith('*'):
                exp_path = exp_path.with_name(exp_path.name[:-1])
                self.search_paths.extend(
                    filter(Path.is_dir, exp_path.glob(exp_path.name)))
            else:
                self.search_paths.append(exp_path)

        # Add DLC folders based on the first/bin folder.
        try:
            first_search = self.search_paths[0]
        except IndexError:
            pass
        else:
            folder = first_search.parent
            stem = first_search.name + '_dlc'
            for ind in itertools.count(1):
                path = folder / (stem + str(ind))
                if path.exists():
                    self.search_paths.insert(0, path)
                else:
                    break

            # Force including 'platform', for Hammer assets.
            self.search_paths.append(self.path.parent / 'platform')
Beispiel #47
0
def get_curr_settings(*, is_palette: bool) -> Property:
    """Return a property tree defining the current options."""
    props = Property.root()

    for opt_id, opt_func in OPTION_SAVE.items():
        # Skip if it opts out of being on the palette.
        if is_palette and not getattr(opt_func, 'to_palette', True):
            continue
        opt_prop = opt_func()
        opt_prop.name = opt_id.title()
        props.append(opt_prop)

    return props
Beispiel #48
0
    def save(self, ignore_readonly=False):
        """Save the palette file into the specified location.

        If ignore_readonly is true, this will ignore the `prevent_overwrite`
        property of the palette (allowing resaving those properties over old
        versions). Otherwise those palettes always create a new file.
        """
        LOGGER.info('Saving "{}"!', self.name)
        props = Property(None, [
            Property('Name', self.name),
            Property('TransName', self.trans_name),
            Property('ReadOnly', srctools.bool_as_int(self.prevent_overwrite)),
            Property('Items', [
                Property(item_id, str(subitem))
                for item_id, subitem in self.pos
            ])
        ])
        # If default, don't include in the palette file.
        # Remove the translated name, in case it's not going to write
        # properly to the file.
        if self.trans_name:
            props['Name'] = ''
        else:
            del props['TransName']

        if not self.prevent_overwrite:
            del props['ReadOnly']

        # We need to write a new file, determine a valid path.
        # Use a hash to ensure it's a valid path (without '-' if negative)
        # If a conflict occurs, add ' ' and hash again to get a different
        # value.
        if self.filename is None or (self.prevent_overwrite
                                     and not ignore_readonly):
            hash_src = self.name
            while True:
                hash_filename = str(abs(hash(hash_src))) + PAL_EXT
                if os.path.isfile(hash_filename):
                    # Add a random character to iterate the hash.
                    hash_src += chr(random.randrange(0x10ffff))
                else:
                    file = open(os.path.join(PAL_DIR, hash_filename), 'w')
                    self.filename = os.path.join(PAL_DIR, hash_filename)
                    break
        else:
            file = open(os.path.join(PAL_DIR, self.filename), 'w')
        with file:
            for line in props.export():
                file.write(line)
Beispiel #49
0
    def __init__(
        self,
        name: str,
        sounds: List[str],
        volume: Union[Tuple[Union[float, VOLUME], Union[float, VOLUME]], float, VOLUME]=VOL_NORM,
        channel: Channel=Channel.DEFAULT,
        level: Union[Tuple[Union[float, Level], Union[float, Level]], float, Level]=Level.SNDLVL_NORM,
        pitch: Union[Tuple[Union[float, Pitch], Union[float, Pitch]], float, Pitch]=Pitch.PITCH_NORM,
        
        # Operator stacks
        stack_start: Optional[Property]=None,
        stack_update: Optional[Property]=None,
        stack_stop: Optional[Property]=None,
        use_v2: bool=False,
    ) -> None:
        """Create a soundscript."""
        self.name = name
        self.sounds = sounds
        self.channel = channel
        self.force_v2 = use_v2

        if isinstance(volume, tuple):
            self.volume = volume
        else:
            self.volume = volume, volume

        if isinstance(level, tuple):
            self.level = level
        else:
            self.level = level, level

        if isinstance(pitch, tuple):
            self.pitch = pitch
        else:
            self.pitch = pitch, pitch
        
        self.stack_start = Property('', []) if stack_start is None else stack_start
        self.stack_update = Property('', []) if stack_update is None else stack_update
        self.stack_stop = Property('', []) if stack_stop is None else stack_stop
Beispiel #50
0
 def setup_result(vmf: VMF, res_list: List[Property], result: Property, source: Optional[str]='') -> None:
     """Helper method to perform result setup."""
     func = RESULT_SETUP.get(result.name)
     if func:
         # noinspection PyBroadException
         try:
             result.value = func(vmf, result)
         except Exception:
             # Print the source of the condition if if fails...
             LOGGER.exception(
                 'Error in {} setup:',
                 source or 'condition',
             )
             if utils.DEV_MODE:
                 # Crash so this is immediately noticeable..
                 utils.quit_app(1)
             else:
                 # In release, just skip this one - that way it's
                 # still hopefully possible to run the game.
                 result.value = None
         if result.value is None:
             # This result is invalid, remove it.
             res_list.remove(result)
Beispiel #51
0
def flag_random(inst: Entity, res: Property) -> bool:
    """Randomly is either true or false."""
    if res.has_children():
        chance = res['chance', '100']
        seed = 'a' + res['seed', '']
    else:
        chance = res.value
        seed = 'a'

    # Allow ending with '%' sign
    chance = srctools.conv_int(chance.rstrip('%'), 100)

    set_random_seed(inst, seed)
    return random.randrange(100) < chance
def load_conf(prop_block: Property):
    """Read the config and build our dictionaries."""
    global INST_SPECIAL

    for prop in prop_block.find_key('Allinstances', []):
        INSTANCE_FILES[prop.name] = [
            inst.value.casefold()
            for inst in
            prop
        ]

    for prop in prop_block.find_key('CustInstances', []):
        CUST_INST_FILES[prop.name] = {
            inst.name: inst.value.casefold()
            for inst in
            prop
        }

    INST_SPECIAL = {
        key.casefold(): resolve(val_string, silent=True)
        for key, val_string in
        SPECIAL_INST.items()
    }
Beispiel #53
0
def get_config(
    prop_block: Property,
    fsys: FileSystem,
    folder: str,
    pak_id='',
    prop_name='config',
    extension='.cfg',
):
    """Extract a config file referred to by the given property block.

    Looks for the prop_name key in the given prop_block.
    If the keyvalue has a value of "", an empty tree is returned.
    If it has children, a copy of them is returned.
    Otherwise the value is a filename in the zip which will be parsed.
    """
    prop_block = prop_block.find_key(prop_name, "")
    if prop_block.has_children():
        prop = prop_block.copy()
        prop.name = None
        return prop

    if prop_block.value == '':
        return Property(None, [])

    # Zips must use '/' for the separator, even on Windows!
    path = folder + '/' + prop_block.value
    if len(path) < 3 or path[-4] != '.':
        # Add extension
        path += extension
    try:
        return fsys.read_prop(path)
    except FileNotFoundError:
        LOGGER.warning('"{id}:{path}" not in zip!', id=pak_id, path=path)
        return Property(None, [])
    except UnicodeDecodeError:
        LOGGER.exception('Unable to read "{id}:{path}"', id=pak_id, path=path)
        raise
Beispiel #54
0
def apply_replacements(conf: Property, item_id: str) -> Property:
    """Apply a set of replacement values to a config file, returning a new copy.

    The replacements are found in a 'Replacements' block in the property.
    These replace %values% starting and ending with percents. A double-percent
    allows literal percents. Unassigned values are an error.
    """
    replace: dict[str, str] = {}
    new_conf = Property.root() if conf.is_root() else Property(
        conf.real_name, [])

    # Strip the replacement blocks from the config, and save the values.
    for prop in conf:
        if prop.name == 'replacements':
            for rep_prop in prop:
                replace[rep_prop.name.strip('%')] = rep_prop.value
        else:
            new_conf.append(prop)

    def rep_func(match: Match) -> str:
        """Does the replacement."""
        var = match.group(1)
        if not var:  # %% becomes %.
            return '%'
        try:
            return replace[var.casefold()]
        except KeyError:
            raise ValueError(
                f'Unresolved variable in "{item_id}": {var!r}\nValid vars: {replace}'
            )

    for prop in new_conf.iter_tree(blocks=True):
        prop.name = RE_PERCENT_VAR.sub(rep_func, prop.real_name)
        if not prop.has_children():
            prop.value = RE_PERCENT_VAR.sub(rep_func, prop.value)

    return new_conf
Beispiel #55
0
def read_configs(conf: Property) -> None:
    """Read in the fizzler data."""
    for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'):
        fizz = FizzlerType.parse(fizz_conf)

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

        FIZZ_TYPES[fizz.id] = fizz

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

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

        # Add a paint fizzler brush to these fizzlers.
        fizz.brushes.append(
            FizzlerBrush(
                brush_name,
                textures={
                    TexGroup.TRIGGER: const.Tools.TRIGGER,
                },
                keys={
                    'classname': 'trigger_paint_cleanser',
                    'startdisabled': brush_start_disabled or '0',
                    'spawnflags': '9',
                },
                local_keys={},
                outputs=[],
                singular=True,
            ))
Beispiel #56
0
def debug_flag(inst: Entity, props: Property):
    """Displays text when executed, for debugging conditions.

    If the text ends with an '=', the instance will also be displayed.
    As a flag, this always evaluates as true.
    """
    # Mark as a warning so it's more easily seen.
    if props.has_children():
        LOGGER.warning('Debug:\n{!s}\n{!s}', props, inst)
    else:
        LOGGER.warning('Debug: {props}{inst!s}'.format(
            inst=inst,
            props=props.value,
        ))
    return True  # The flag is always true
Beispiel #57
0
    def export(exp_data: ExportData) -> None:
        """Export the selected signage to the config."""
        # Timer value -> sign ID.
        sel_ids: List[Tuple[str, str]] = exp_data.selected

        # Special case, arrow is never selectable.
        sel_ids.append(('arrow', 'SIGN_ARROW'))

        conf = Property('Signage', [])

        for tim_id, sign_id in sel_ids:
            try:
                sign = Signage.by_id(sign_id)
            except KeyError:
                LOGGER.warning('Signage "{}" does not exist!', sign_id)
                continue
            prop_block = Property(str(tim_id), [])

            sign._serialise(prop_block, exp_data.selected_style)

            for sub_name, sub_id in [
                ('primary', sign.prim_id),
                ('secondary', sign.sec_id),
            ]:
                if sub_id:
                    try:
                        sub_sign = Signage.by_id(sub_id)
                    except KeyError:
                        LOGGER.warning(
                            'Signage "{}"\'s {} "{}" '
                            'does not exist!', sign_id, sub_name, sub_id)
                    else:
                        sub_block = Property(sub_name, [])
                        sub_sign._serialise(sub_block, exp_data.selected_style)
                        if sub_block:
                            prop_block.append(sub_block)

            if prop_block:
                conf.append(prop_block)

        exp_data.vbsp_conf.append(conf)
Beispiel #58
0
def from_file(path: utils.PackagePath, missing_ok: bool=False, source: str= '') -> LazyConf:
	"""Lazily load the specified config."""
	try:
		fsys = packages.PACKAGE_SYS[path.package]
	except KeyError:
		if not missing_ok:
			LOGGER.warning('Package does not exist: "{}"', path)
		return BLANK
	try:
		file = fsys[path.path]
	except FileNotFoundError:
		if not missing_ok:
			LOGGER.warning('File does not exist: "{}"', path)
		return BLANK

	def loader() -> Property:
		"""Load and parse the specified file when called."""
		try:
			with file.open_str() as f:
				props = Property.parse(f)
		except (KeyValError, FileNotFoundError, UnicodeDecodeError):
			LOGGER.exception('Unable to read "{}"', path)
			raise
		if source:
			packages.set_cond_source(props, source)
		return props

	if DEV_MODE.get():
		# Parse immediately, to check syntax.
		try:
			with file.open_str() as f:
				Property.parse(f)
		except (KeyValError, FileNotFoundError, UnicodeDecodeError):
			LOGGER.exception('Unable to read "{}"', path)

	return loader
Beispiel #59
0
    def parse(
        cls: Type[Handle],
        prop: Property,
        pack: str,
        width: int,
        height: int,
        *,
        subkey: str='',
        subfolder: str='',
    ) -> Handle:
        """Parse a property into an image handle.

        If a package isn't specified, the given package will be used.
        Optionally, 'subkey' can be used to specifiy that the property is a subkey.
        An error icon will then be produced automatically.
        If subfolder is specified, files will be relative to this folder.
        The width/height may be zero to indicate it should not be resized.
        """
        if subkey:
            try:
                prop = prop.find_key(subkey)
            except LookupError:
                return cls.error(width, height)
        if prop.has_children():
            children = []
            for child in prop:
                if child.name not in ('image', 'img', 'layer'):
                    raise ValueError(f'Unknown compound type "{child}"!')
                children.append(cls.parse(
                    child, pack,
                    width, height,
                    subfolder=subfolder
                ))
            return cls.composite(children, width, height)

        return cls.parse_uri(utils.PackagePath.parse(prop.value, pack), width, height, subfolder=subfolder)
Beispiel #60
0
def save_load_stylevars(props: Property = None):
    """Save and load variables from configs."""
    if props is None:
        props = Property('', [])
        for var_id, var in sorted(tk_vars.items()):
            props[var_id] = str(int(var.get()))
        return props
    else:
        # Loading
        for prop in props:
            try:
                tk_vars[prop.real_name].set(prop.value)
            except KeyError:
                LOGGER.warning('No stylevar "{}", skipping.', prop.real_name)
        update_filter()