Ejemplo n.º 1
0
def test_vmf() -> None:
    """Verify the positions by generating a VMF."""
    vmf = VMF()
    for shape in SHAPES:
        group = vmf.create_visgroup(
            shape.name.replace('/', '_').replace('fizzler',
                                                 '').replace('__', '_'))
        for pos, angle in shape.points:
            ent = vmf.create_ent(
                'prop_static',
                model='models/props_map_editor/fizzler.mdl',
                origin=pos @ angle,
                angles=angle,
            )
            ent.visgroup_ids.add(group.id)
            ent.vis_shown = False
    print('Dumping shape.vmf')
    with open('shape.vmf', 'w') as f:
        vmf.export(f)
Ejemplo n.º 2
0
main_vmf = VMF()

main_vmf.add_brushes(
    main_vmf.make_hollow(
        Vec(-256, -256, -256),
        Vec(+256, +256, +256),
    ))

# We can't be more accurate, we'll blow the ent limit.
for pitch in range(0, 360, 45):
    for yaw in range(0, 360, 45):
        for roll in range(0, 360, 45):
            inst = main_vmf.create_ent(
                'func_instance',
                file='instances/rot_inst.vmf',
                angles=Vec(pitch, yaw, roll),
                origin='0 0 0',
            )
            inst.fixup['$rot_pitch'] = pitch
            inst.fixup['$rot_yaw'] = yaw
            inst.fixup['$rot_roll'] = roll

with open('instances/rot_inst.vmf', 'w') as f:
    inst_vmf.export(f)

with open('rot_main.vmf', 'w') as f:
    main_vmf.export(f)

print('Done.')
Ejemplo n.º 3
0
def combine(
    bsp: BSP,
    bsp_ents: VMF,
    pack: PackList,
    game: Game,
    *,
    studiomdl_loc: Path = None,
    qc_folders: List[Path] = None,
    crowbar_loc: Optional[Path] = None,
    decomp_cache_loc: Path = None,
    auto_range: float = 0,
    min_cluster: int = 2,
    debug_tint: bool = False,
    debug_dump: bool = False,
) -> None:
    """Combine props in this map."""

    # First parse out the bbox ents, so they are always removed.
    bbox_ents = list(bsp_ents.by_class['comp_propcombine_set'])
    for ent in bbox_ents:
        ent.remove()

    if not studiomdl_loc.exists():
        LOGGER.warning('No studioMDL! Cannot propcombine!')
        return

    if not qc_folders and decomp_cache_loc is None:
        # If gameinfo is blah/game/hl2/gameinfo.txt,
        # QCs should be in blah/content/ according to Valve's scheme.
        # But allow users to override this.
        # If Crowbar's path is provided, that means they may want to just supply nothing.
        qc_folders = [game.path.parent.parent / 'content']

    # Parse through all the QC files.
    LOGGER.info('Parsing QC files. Paths: \n{}',
                '\n'.join(map(str, qc_folders)))
    qc_map: Dict[str, Optional[QC]] = {}
    for qc_folder in qc_folders:
        load_qcs(qc_map, qc_folder)
    LOGGER.info('Done! {} props.', len(qc_map))

    map_name = Path(bsp.filename).stem

    # Don't re-parse models continually.
    mdl_map: Dict[str, Optional[Model]] = {}
    # Wipe these, if they're being used again.
    _mesh_cache.clear()
    _coll_cache.clear()
    missing_qcs: Set[str] = set()

    def get_model(filename: str) -> Union[Tuple[QC, Model], Tuple[None, None]]:
        """Given a filename, load/parse the QC and MDL data.

        Either both are returned, or neither are.
        """
        key = unify_mdl(filename)
        try:
            model = mdl_map[key]
        except KeyError:
            try:
                mdl_file = pack.fsys[filename]
            except FileNotFoundError:
                # We don't have this model, we can't combine...
                return None, None
            model = mdl_map[key] = Model(pack.fsys, mdl_file)
            if 'no_propcombine' in model.keyvalues.casefold():
                mdl_map[key] = qc_map[key] = None
                return None, None
        if model is None or key in missing_qcs:
            return None, None

        try:
            qc = qc_map[key]
        except KeyError:
            if crowbar_loc is None:
                missing_qcs.add(key)
                return None, None
            qc = decompile_model(pack.fsys, decomp_cache_loc, crowbar_loc,
                                 filename, model.checksum)
            qc_map[key] = qc

        if qc is None:
            return None, None
        else:
            return qc, model

    # Ignore these two, they don't affect our new prop.
    relevant_flags = ~(StaticPropFlags.HAS_LIGHTING_ORIGIN
                       | StaticPropFlags.DOES_FADE)

    def get_grouping_key(prop: StaticProp) -> Optional[tuple]:
        """Compute a grouping key for this prop.

        Only props with matching key can be possibly combined.
        If None it cannot be combined.
        """
        qc, model = get_model(prop.model)

        if model is None or qc is None:
            return None

        return (
            # Must be first, we pull this out later.
            frozenset({
                tex.casefold().replace('\\', '/')
                for tex in model.iter_textures([prop.skin])
            }),
            model.flags.value,
            (prop.flags & relevant_flags).value,
            model.contents,
            model.surfaceprop,
            prop.renderfx,
            *prop.tint,
        )

    prop_count = 0

    # First, construct groups of props that can possibly be combined.
    prop_groups = defaultdict(
        list)  # type: Dict[Optional[tuple], List[StaticProp]]

    # This holds the list of all props we want in the map -
    # combined ones, and any we reject for whatever reason.
    final_props: List[StaticProp] = []
    rejected: List[StaticProp] = []

    if bbox_ents:
        LOGGER.info('Propcombine sets present ({}), combining...',
                    len(bbox_ents))
        grouper = group_props_ent(
            prop_groups,
            rejected,
            get_model,
            bbox_ents,
            min_cluster,
        )
    elif auto_range > 0:
        LOGGER.info('Automatically finding propcombine sets...')
        grouper = group_props_auto(
            prop_groups,
            rejected,
            auto_range,
            min_cluster,
        )
    else:
        # No way provided to choose props.
        LOGGER.info('No propcombine groups provided.')
        return

    for prop in bsp.static_props():
        prop_groups[get_grouping_key(prop)].append(prop)
        prop_count += 1

    # These are models we cannot merge no matter what -
    # no source files etc.
    cannot_merge = prop_groups.pop(None, [])
    final_props.extend(cannot_merge)

    LOGGER.debug(
        'Prop groups: \n{}', '\n'.join([
            f'{group}: {len(props)}'
            for group, props in sorted(prop_groups.items(),
                                       key=operator.itemgetter(0))
        ]))

    group_count = 0
    with ModelCompiler(
            game,
            studiomdl_loc,
            pack,
            map_name,
            'propcombine',
    ) as compiler:
        for group in grouper:
            grouped_prop = combine_group(compiler, group, get_model)
            if debug_tint:
                # Compute a random hue, and convert back to RGB 0-255.
                r, g, b = colorsys.hsv_to_rgb(random.random(), 1, 1)
                grouped_prop.tint = Vec(round(r * 255), round(g * 255),
                                        round(b * 255))
            final_props.append(grouped_prop)
            group_count += 1

    final_props.extend(rejected)

    if debug_dump:
        dump_vmf = VMF()
        for prop in rejected:
            dump_vmf.create_ent(
                'prop_static',
                model=prop.model,
                origin=prop.origin,
                angles=prop.angles,
                solid=prop.solidity,
                rendercolor=prop.tint,
            )
        dump_fname = Path(bsp.filename).with_name(map_name +
                                                  '_propcombine_reject.vmf')
        LOGGER.info('Dumping uncombined props to {}...', dump_fname)
        with dump_fname.open('w') as f:
            dump_vmf.export(f)

    LOGGER.info(
        'Combined {} props into {}:\n - {} grouped models\n - {} ineligable\n - {} had no group',
        prop_count,
        len(final_props),
        group_count,
        len(cannot_merge),
        len(rejected),
    )
    LOGGER.debug('Models with unknown QCs: \n{}',
                 '\n'.join(sorted(missing_qcs)))
    # If present, delete old cache file. We'll have cleaned up the models.
    try:
        os.remove(compiler.model_folder_abs / 'cache.vdf')
    except FileNotFoundError:
        pass

    bsp.write_static_props(final_props)