Ejemplo n.º 1
0
def get_intersect_testcases() -> list:
    """Use a VMF to make it easier to generate the bounding boxes."""
    with Path(__file__, '../bbox_samples.vmf').open() as f:
        vmf = VMF.parse(Property.parse(f))

    def process(brush: Solid | None) -> tuple[tuple[int, ...], tuple[int, ...]] | None:
        """Extract the bounding box from the brush."""
        if brush is None:
            return None
        bb_min, bb_max = brush.get_bbox()
        for vec in [bb_min, bb_max]:
            for ax in 'xyz':
                # If one thick, make zero thick so we can test planes.
                if abs(vec[ax]) == 63:
                    vec[ax] = math.copysign(64, vec[ax])
        return (tuple(map(int, bb_min)), tuple(map(int, bb_max)))

    for ent in vmf.entities:
        test = expected = None
        for solid in ent.solids:
            if solid.sides[0].mat.casefold() == 'tools/toolsskip':
                expected = solid
            if solid.sides[0].mat.casefold() == 'tools/toolstrigger':
                test = solid
        if test is None:
            raise ValueError(ent.id)
        yield (*process(test), process(expected))
Ejemplo n.º 2
0
def make_tag_coop_inst(tag_loc: str):
    """Make the coop version of the tag instances.

    This needs to be shrunk, so all the logic entities are not spread
    out so much (coop tubes are small).

    This way we avoid distributing the logic.
    """
    global TAG_COOP_INST_VMF
    TAG_COOP_INST_VMF = vmf = VMF.parse(
        os.path.join(tag_loc, TAG_GUN_COOP_INST)
    )

    def logic_pos():
        """Put the entities in a nice circle..."""
        while True:
            for ang in range(0, 44):
                ang *= 360/44
                yield Vec(16*math.sin(ang), 16*math.cos(ang), 32)
    pos = logic_pos()
    # Move all entities that don't care about position to the base of the player
    for ent in TAG_COOP_INST_VMF.iter_ents():
        if ent['classname'] == 'info_coop_spawn':
            # Remove the original spawn point from the instance.
            # That way it can overlay over other dropper instances.
            ent.remove()
        elif ent['classname'] in ('info_target', 'info_paint_sprayer'):
            pass
        else:
            ent['origin'] = next(pos)

            # These originally use the coop spawn point, but this doesn't
            # always work. Switch to the name of the player, which is much
            # more reliable.
            if ent['classname'] == 'logic_measure_movement':
                ent['measuretarget'] = '!player_blue'

    # Add in a trigger to start the gel gun, and reset the activated
    # gel whenever the player spawns.
    trig_brush = vmf.make_prism(
        Vec(-32, -32, 0),
        Vec(32, 32, 16),
        mat='tools/toolstrigger',
    ).solid
    start_trig = vmf.create_ent(
        classname='trigger_playerteam',
        target_team=3,  # ATLAS
        spawnflags=1,  # Clients only
        origin='0 0 8',
    )
    start_trig.solids = [trig_brush]
    start_trig.add_out(
        # This uses the !activator as the target player so it must be via trigger.
        Output('OnStartTouchBluePlayer', '@gel_ui', 'Activate', delay=0, only_once=True),
        # Reset the gun to fire nothing.
        Output('OnStartTouchBluePlayer', '@blueisenabled', 'SetValue', 0, delay=0.1),
        Output('OnStartTouchBluePlayer', '@orangeisenabled', 'SetValue', 0, delay=0.1),
    )
Ejemplo n.º 3
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[:]):  # type: Entity
        # Remove comments
        ent.comments = ''

        # Remove entities that have their visgroups hidden.
        if ent.hidden or not ent.vis_shown:
            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 solid.vis_shown:
                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()
Ejemplo n.º 4
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[:]):  # type: Entity
        # Remove comments
        ent.comments = ''

        # Remove entities that have their visgroups hidden.
        if ent.hidden or not ent.vis_shown:
            print('Removing hidden ent')
            inst.remove_ent(ent)
            continue

        # Remove hammer_notes entities
        if ent['classname'] == 'hammer_notes':
            print('Removing hammer_notes...')
            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 solid.vis_shown:
                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()
Ejemplo n.º 5
0
def make_tag_coop_inst(tag_loc: str):
    """Make the coop version of the tag instances.

    This needs to be shrunk, so all the logic entities are not spread
    out so much (coop tubes are small).

    This way we avoid distributing the logic.
    """
    global TAG_COOP_INST_VMF
    TAG_COOP_INST_VMF = vmf = VMF.parse(os.path.join(tag_loc, TAG_GUN_COOP_INST))

    def logic_pos():
        """Put the entities in a nice circle..."""
        while True:
            for ang in range(0, 44):
                ang *= 360 / 44
                yield Vec(16 * math.sin(ang), 16 * math.cos(ang), 32)

    pos = logic_pos()
    # Move all entities that don't care about position to the base of the player
    for ent in TAG_COOP_INST_VMF.iter_ents():
        if ent["classname"] == "info_coop_spawn":
            # Remove the original spawn point from the instance.
            # That way it can overlay over other dropper instances.
            ent.remove()
        elif ent["classname"] in ("info_target", "info_paint_sprayer"):
            pass
        else:
            ent["origin"] = next(pos)

            # These originally use the coop spawn point, but this doesn't
            # always work. Switch to the name of the player, which is much
            # more reliable.
            if ent["classname"] == "logic_measure_movement":
                ent["measuretarget"] = "!player_blue"

    # Add in a trigger to start the gel gun, and reset the activated
    # gel whenever the player spawns.
    trig_brush = vmf.make_prism(Vec(-32, -32, 0), Vec(32, 32, 16), mat="tools/toolstrigger").solid
    start_trig = vmf.create_ent(
        classname="trigger_playerteam", target_team=3, spawnflags=1, origin="0 0 8"  # ATLAS  # Clients only
    )
    start_trig.solids = [trig_brush]
    start_trig.add_out(
        # This uses the !activator as the target player so it must be via trigger.
        Output("OnStartTouchBluePlayer", "@gel_ui", "Activate", delay=0, only_once=True),
        # Reset the gun to fire nothing.
        Output("OnStartTouchBluePlayer", "@blueisenabled", "SetValue", 0, delay=0.1),
        Output("OnStartTouchBluePlayer", "@orangeisenabled", "SetValue", 0, delay=0.1),
    )
Ejemplo n.º 6
0
 def parse(cls, data: ParseData):
     """Read templates from a package."""
     file = get_config(
         prop_block=data.info,
         fsys=data.fsys,
         folder='templates',
         pak_id=data.pak_id,
         prop_name='file',
         extension='.vmf',
     )
     file = VMF.parse(file)
     return cls(
         data.id,
         file,
         force=data.info['force', ''],
         keep_brushes=srctools.conv_bool(data.info['keep_brushes', '1'],
                                         True),
     )
Ejemplo n.º 7
0
def parse_item_folder(
    folders_to_parse: set[str],
    filesystem: FileSystem,
    pak_id: str,
) -> dict[str, ItemVariant]:
    """Parse through the data in item/ folders.

    folders is a dict, with the keys set to the folder names we want.
    The values will be filled in with itemVariant values
    """
    folders: dict[str, ItemVariant] = {}
    for fold in folders_to_parse:
        prop_path = 'items/' + fold + '/properties.txt'
        editor_path = 'items/' + fold + '/editoritems.txt'
        config_path = 'items/' + fold + '/vbsp_config.cfg'

        first_item: EditorItem | None = None
        extra_items: list[EditorItem] = []
        try:
            props = filesystem.read_prop(prop_path).find_key('Properties')
            f = filesystem[editor_path].open_str()
        except FileNotFoundError as err:
            raise IOError('"' + pak_id + ':items/' + fold + '" not valid! '
                          'Folder likely missing! ') from err
        with f:
            tok = Tokenizer(f, editor_path)
            for tok_type, tok_value in tok:
                if tok_type is Token.STRING:
                    if tok_value.casefold() != 'item':
                        raise tok.error('Unknown item option "{}"!', tok_value)
                    if first_item is None:
                        first_item = EditorItem.parse_one(tok)
                    else:
                        extra_items.append(EditorItem.parse_one(tok))
                elif tok_type is not Token.NEWLINE:
                    raise tok.error(tok_type)

        if first_item is None:
            raise ValueError(f'"{pak_id}:items/{fold}/editoritems.txt has no '
                             '"Item" block!')

        try:
            editor_vmf = VMF.parse(
                filesystem.read_prop(editor_path[:-3] + 'vmf'))
        except FileNotFoundError:
            pass
        else:
            editoritems_vmf.load(first_item, editor_vmf)
        first_item.generate_collisions()

        # extra_items is any extra blocks (offset catchers, extent items).
        # These must not have a palette section - it'll override any the user
        # chooses.
        for extra_item in extra_items:
            extra_item.generate_collisions()
            for subtype in extra_item.subtypes:
                if subtype.pal_pos is not None:
                    LOGGER.warning(
                        f'"{pak_id}:items/{fold}/editoritems.txt has '
                        f'palette set for extra item blocks. Deleting.')
                    subtype.pal_icon = subtype.pal_pos = subtype.pal_name = None

        # In files this is specified as PNG, but it's always really VTF.
        try:
            all_icon = FSPath(props['all_icon']).with_suffix('.vtf')
        except LookupError:
            all_icon = None

        folders[fold] = ItemVariant(
            editoritems=first_item,
            editor_extra=extra_items,

            # Add the folder the item definition comes from,
            # so we can trace it later for debug messages.
            source=f'<{pak_id}>/items/{fold}',
            pak_id=pak_id,
            vbsp_config=lazy_conf.BLANK,
            authors=sep_values(props['authors', '']),
            tags=sep_values(props['tags', '']),
            desc=desc_parse(props, f'{pak_id}:{prop_path}', pak_id),
            ent_count=props['ent_count', ''],
            url=props['infoURL', None],
            icons={
                prop.name: img.Handle.parse(
                    prop,
                    pak_id,
                    64,
                    64,
                    subfolder='items',
                )
                for prop in props.find_children('icon')
            },
            all_name=props['all_name', None],
            all_icon=all_icon,
        )

        if Item.log_ent_count and not folders[fold].ent_count:
            LOGGER.warning(
                '"{id}:{path}" has missing entity count!',
                id=pak_id,
                path=prop_path,
            )

        # If we have one of the grouping icon definitions but not both required
        # ones then notify the author.
        has_name = folders[fold].all_name is not None
        has_icon = folders[fold].all_icon is not None
        if (has_name or has_icon or 'all'
                in folders[fold].icons) and (not has_name or not has_icon):
            LOGGER.warning(
                'Warning: "{id}:{path}" has incomplete grouping icon '
                'definition!',
                id=pak_id,
                path=prop_path,
            )
        folders[fold].vbsp_config = lazy_conf.from_file(
            utils.PackagePath(pak_id, config_path),
            missing_ok=True,
            source=folders[fold].source,
        )
    return folders
Ejemplo n.º 8
0
def main(args: List[str]) -> None:
    """Main script."""
    parser = argparse.ArgumentParser(description=__doc__)

    parser.add_argument(
        "-f",
        "--filter",
        help="filter output to only display resources in this subfolder. "
        "This can be used multiple times.",
        type=str.casefold,
        action='append',
        metavar='folder',
        dest='filters',
    )
    parser.add_argument(
        "-u",
        "--unused",
        help="Instead of showing depenencies, show files in the filtered "
        "folders that are unused.",
        action='store_true',
    )
    parser.add_argument(
        "game",
        help="either location of a gameinfo.txt file, or any root folder.",
    )
    parser.add_argument(
        "path",
        help="the files to load. The path can have a single * in the "
        "filename to match files with specific extensions and a prefix.",
    )

    result = parser.parse_args(args)

    if result.unused and not result.filters:
        raise ValueError(
            'At least one filter must be provided in "unused" mode.')

    if result.game:
        try:
            fsys = Game(result.game).get_filesystem()
        except FileNotFoundError:
            fsys = FileSystemChain(RawFileSystem(result.game))
    else:
        fsys = FileSystemChain()

    packlist = PackList(fsys)

    file_path: str = result.path
    print('Finding files...')
    with fsys:
        if '*' in file_path:  # Multiple files
            if file_path.count('*') > 1:
                raise ValueError('Multiple * in path!')
            prefix, suffix = file_path.split('*')
            folder, prefix = os.path.split(prefix)
            prefix = prefix.casefold()
            suffix = suffix.casefold()
            print(f'Prefix: {prefix!r}, suffix: {suffix!r}')
            print(f'Searching folder {folder}...')

            files = []
            for file in fsys.walk_folder(folder):
                file_path = file.path.casefold()
                if not os.path.basename(file_path).startswith(prefix):
                    continue
                if file_path.endswith(suffix):
                    print(' ' + file.path)
                    files.append(file)
        else:  # Single file
            files = [fsys[file_path]]
        for file in files:
            ext = file.path[-4:].casefold()
            if ext == '.vmf':
                with file.open_str() as f:
                    vmf_props = Property.parse(f)
                    vmf = VMF.parse(vmf_props)
                packlist.pack_fgd(vmf, fgd)
                del vmf, vmf_props  # Hefty, don't want to keep.
            elif ext == '.bsp':
                child_sys = fsys.get_system(file)
                if not isinstance(child_sys, RawFileSystem):
                    raise ValueError('Cannot inspect BSPs in VPKs!')
                bsp = BSP(os.path.join(child_sys.path, file.path))
                packlist.pack_from_bsp(bsp)
                packlist.pack_fgd(bsp.read_ent_data(), fgd)
                del bsp
            else:
                packlist.pack_file(file.path)
        print('Evaluating dependencies...')
        packlist.eval_dependencies()
        print('Done.')

        if result.unused:
            print('Unused files:')
            used = set(packlist.filenames())
            for folder in result.filters:
                for file in fsys.walk_folder(folder):
                    if file.path.casefold() not in used:
                        print(' ' + file.path)
        else:
            print('Dependencies:')
            for filename in packlist.filenames():
                if not result.filters or any(
                        map(filename.casefold().startswith, result.filters)):
                    print(' ' + filename)