Exemplo n.º 1
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(
                        'Too many closing brackets after {} ents'.format(
                            len(vmf.entities)))
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf
            else:
                # Line is of the form <"key" "val">
                key, value = line.split(b'" "')
                decoded_key = key[1:].decode('ascii')
                decoded_val = value[:-1].decode('ascii')
                if 27 in value:
                    # All outputs use the comma_sep, so we can ID them.
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_val)))
                else:
                    # Normal keyvalue.
                    cur_ent[decoded_key] = decoded_val

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
Exemplo n.º 2
0
    def read_ent_data(self) -> VMF:
        """Parse in entity data.
        
        This returns a VMF object, with entities mirroring that in the BSP. 
        No brushes are read.
        """
        ent_data = self.get_lump(BSP_LUMPS.ENTITIES)
        vmf = VMF()
        cur_ent = None  # None when between brackets.
        seen_spawn = False  # The first entity is worldspawn.

        # This code performs the same thing as property_parser, but simpler
        # since there's no nesting, comments, or whitespace, except between
        # key and value. We also operate directly on the (ASCII) binary.
        for line in ent_data.splitlines():
            if line == b'{':
                if cur_ent is not None:
                    raise ValueError(
                        '2 levels of nesting after {} ents'.format(
                            len(vmf.entities)))
                if not seen_spawn:
                    cur_ent = vmf.spawn
                    seen_spawn = True
                else:
                    cur_ent = Entity(vmf)
                continue
            elif line == b'}':
                if cur_ent is None:
                    raise ValueError(f'Too many closing brackets after'
                                     f' {len(vmf.entities)} ents!')
                if cur_ent is vmf.spawn:
                    if cur_ent['classname'] != 'worldspawn':
                        raise ValueError('No worldspawn entity!')
                else:
                    # The spawn ent is stored in the attribute, not in the ent
                    # list.
                    vmf.add_ent(cur_ent)
                cur_ent = None
                continue
            elif line == b'\x00':  # Null byte at end of lump.
                if cur_ent is not None:
                    raise ValueError("Last entity didn't end!")
                return vmf

            if cur_ent is None:
                raise ValueError("Keyvalue outside brackets!")

            # Line is of the form <"key" "val">
            key, value = line.split(b'" "')
            decoded_key = key[1:].decode('ascii')
            decoded_value = value[:-1].decode('ascii')

            # Now, we need to figure out if this is a keyvalue,
            # or connection.
            # If we're L4D+, this is easy - they use 0x1D as separator.
            # Before, it's a comma which is common in keyvalues.
            # Assume it's an output if it has exactly 4 commas, and the last two
            # successfully parse as numbers.
            if 27 in value:
                # All outputs use the comma_sep, so we can ID them.
                cur_ent.add_out(
                    Output.parse(Property(decoded_key, decoded_value)))
            elif value.count(b',') == 4:
                try:
                    cur_ent.add_out(
                        Output.parse(Property(decoded_key, decoded_value)))
                except ValueError:
                    cur_ent[decoded_key] = decoded_value
            else:
                # Normal keyvalue.
                cur_ent[decoded_key] = decoded_value

        # This keyvalue needs to be stored in the VMF object too.
        # The one in the entity is ignored.
        vmf.map_ver = conv_int(vmf.spawn['mapversion'], vmf.map_ver)

        return vmf
Exemplo n.º 3
0
def import_template(
    vmf: VMF,
    temp_name: Union[str, Template],
    origin: Vec,
    angles: Optional[Union[Angle, Matrix]] = None,
    targetname: str = '',
    force_type: TEMP_TYPES = TEMP_TYPES.default,
    add_to_map: bool = True,
    additional_visgroups: Iterable[str] = (),
    bind_tile_pos: Iterable[Vec] = (),
    align_bind: bool = False,
    coll: collisions.Collisions = None,
    coll_add: Optional[
        collisions.CollideType] = collisions.CollideType.NOTHING,
    coll_mask: collisions.CollideType = collisions.CollideType.EVERYTHING,
) -> ExportedTemplate:
    """Import the given template at a location.

    * `temp_name` can be a string, or a template instance.
    * `visgroups` is a list of additional visgroups to use after the ones in the name string (if given).
    * If `force_type` is set to 'detail' or 'world', all brushes will be converted
      to the specified type instead. A list of world brushes and the func_detail
      entity will be returned. If there are no detail brushes, None will be
      returned instead of an invalid entity.
    * If `targetname` is set, it will be used to localise overlay names.
      add_to_map sets whether to add the brushes and func_detail to the map.
    * IF `coll` is provided, the template may have bee2_collision volumes. The targetname must be
      provided in this case.
    * If any `bound_tile_pos` are provided, these are offsets to tiledefs which
      should have all the overlays in this template bound to them, and vice versa.
    * If `align_bind` is set, these will be first aligned to grid.
    * `coll_mask` and `coll_force` allow modifying the collision types added. `coll_mask` is AND-ed
      with the bbox type, then `coll_add` is OR-ed in. If the collide type ends up being NOTHING, it
      is skipped.
    """
    import vbsp
    if isinstance(temp_name, Template):
        template, temp_name = temp_name, temp_name.id
        chosen_groups: set[str] = set()
    else:
        temp_name, chosen_groups = parse_temp_name(temp_name)
        template = get_template(temp_name)

    chosen_groups.update(additional_visgroups)
    chosen_groups.add('')

    orig_world, orig_detail, orig_over = template.visgrouped(chosen_groups)

    new_world: list[Solid] = []
    new_detail: list[Solid] = []
    new_over: list[Entity] = []

    # A map of the original -> new face IDs.
    id_mapping: dict[int, int] = {}
    orient = to_matrix(angles)

    for orig_list, new_list in [(orig_world, new_world),
                                (orig_detail, new_detail)]:
        for old_brush in orig_list:
            brush = old_brush.copy(
                vmf_file=vmf,
                side_mapping=id_mapping,
                keep_vis=False,
            )
            brush.localise(origin, orient)
            new_list.append(brush)

    for overlay in orig_over:
        new_overlay = overlay.copy(
            vmf_file=vmf,
            keep_vis=False,
        )
        del new_overlay[
            'template_id']  # Remove this, it's not part of overlays
        new_overlay['classname'] = 'info_overlay'

        sides = overlay['sides'].split()
        new_overlay['sides'] = ' '.join(
            str(id_mapping[int(side)]) for side in sides
            if int(side) in id_mapping)

        srctools.vmf.localise_overlay(new_overlay, origin, orient)
        orig_target = new_overlay['targetname']

        # Only change the targetname if the overlay is not global, and we have
        # a passed name.
        if targetname and orig_target and orig_target[0] != '@':
            new_overlay['targetname'] = targetname + '-' + orig_target

        vmf.add_ent(new_overlay)
        new_over.append(new_overlay)

        # Don't let the overlays get retextured too!
        vbsp.IGNORED_OVERLAYS.add(new_overlay)

    if force_type is TEMP_TYPES.detail:
        new_detail.extend(new_world)
        new_world.clear()
    elif force_type is TEMP_TYPES.world:
        new_world.extend(new_detail)
        new_detail.clear()

    if add_to_map:
        vmf.add_brushes(new_world)

    detail_ent: Optional[Entity] = None

    if new_detail:
        detail_ent = vmf.create_ent(classname='func_detail')
        detail_ent.solids = new_detail
        if not add_to_map:
            detail_ent.remove()

    if bind_tile_pos:
        # Bind all our overlays without IDs to a set of tiles,
        # and add any marked faces to those tiles to be given overlays.
        new_overlay_faces = set(
            map(id_mapping.get, map(int, template.overlay_faces)))
        new_overlay_faces.discard(None)
        bound_overlay_faces = [
            face for brush in (new_world + new_detail) for face in brush.sides
            if face.id in new_overlay_faces
        ]

        tile_norm = orient.up()
        for tile_off in bind_tile_pos:
            tile_off = tile_off.copy()
            tile_off.localise(origin, orient)
            for axis in ('xyz' if align_bind else ''):
                # Don't realign things in the normal's axis -
                # those are already fine.
                if abs(tile_norm[axis]) < 1e-6:
                    tile_off[axis] = tile_off[axis] // 128 * 128 + 64
            try:
                tile = tiling.TILES[tile_off.as_tuple(), tile_norm.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'No tile to bind at {} for "{}"!',
                    tile_off,
                    template.id,
                )
                continue
            for over in new_over:
                if over['sides'] == '':
                    tile.bind_overlay(over)
            tile.brush_faces.extend(bound_overlay_faces)

    if template.collisions:
        if coll is None:
            LOGGER.warning(
                'Template "{}" has collisions, but unable to apply these!',
                template.id)
        elif targetname:
            for coll_def in template.collisions:
                if not coll_def.visgroups.issubset(chosen_groups):
                    continue
                contents = (coll_def.bbox.contents & coll_mask) | coll_add
                if contents is not contents.NOTHING:
                    bbox = coll_def.bbox @ orient + origin
                    coll.add(
                        bbox.with_attrs(name=targetname, contents=contents))
        else:
            LOGGER.warning(
                'With collisions provided, the instance name must not be blank!'
            )

    return ExportedTemplate(
        world=new_world,
        detail=detail_ent,
        overlay=new_over,
        orig_ids=id_mapping,
        template=template,
        origin=origin,
        orient=orient,
        visgroups=chosen_groups,
        picker_results={},  # Filled by retexture_template.
        picker_type_results={},
    )
Exemplo n.º 4
0
def import_template(
        vmf: VMF,
        temp_name: Union[str, Template],
        origin: Vec,
        angles: Optional[Union[Angle, Matrix]] = None,
        targetname: str = '',
        force_type: TEMP_TYPES = TEMP_TYPES.default,
        add_to_map: bool = True,
        additional_visgroups: Iterable[str] = (),
        visgroup_choose: Callable[[Iterable[str]], Iterable[str]] = lambda x:
    (),
        bind_tile_pos: Iterable[Vec] = (),
) -> ExportedTemplate:
    """Import the given template at a location.

    temp_name can be a string, or a template instance. visgroups is a list
    of additional visgroups to use after the ones in the name string (if given).

    If force_type is set to 'detail' or 'world', all brushes will be converted
    to the specified type instead. A list of world brushes and the func_detail
    entity will be returned. If there are no detail brushes, None will be
    returned instead of an invalid entity.

    If targetname is set, it will be used to localise overlay names.
    add_to_map sets whether to add the brushes and func_detail to the map.
    visgroup_choose is a callback used to determine if visgroups should be
    added - it's passed a list of names, and should return a list of ones to use.

    If any bound_tile_pos are provided, these are offsets to tiledefs which
    should have all the overlays in this template bound to them, and vice versa.
    """
    import vbsp
    if isinstance(temp_name, Template):
        template, temp_name = temp_name, temp_name.id
        chosen_groups = set()  # type: Set[str]
    else:
        temp_name, chosen_groups = parse_temp_name(temp_name)
        template = get_template(temp_name)

    chosen_groups.update(additional_visgroups)
    chosen_groups.update(visgroup_choose(template.visgroups))
    chosen_groups.add('')

    orig_world, orig_detail, orig_over = template.visgrouped(chosen_groups)

    new_world = []  # type: List[Solid]
    new_detail = []  # type: List[Solid]
    new_over = []  # type: List[Entity]

    # A map of the original -> new face IDs.
    id_mapping = {}  # type: Dict[int, int]
    orient = to_matrix(angles)

    for orig_list, new_list in [(orig_world, new_world),
                                (orig_detail, new_detail)]:
        for old_brush in orig_list:
            brush = old_brush.copy(
                vmf_file=vmf,
                side_mapping=id_mapping,
                keep_vis=False,
            )
            brush.localise(origin, orient)
            new_list.append(brush)

    for overlay in orig_over:  # type: Entity
        new_overlay = overlay.copy(
            vmf_file=vmf,
            keep_vis=False,
        )
        del new_overlay[
            'template_id']  # Remove this, it's not part of overlays
        new_overlay['classname'] = 'info_overlay'

        sides = overlay['sides'].split()
        new_overlay['sides'] = ' '.join(
            str(id_mapping[int(side)]) for side in sides
            if int(side) in id_mapping)

        srctools.vmf.localise_overlay(new_overlay, origin, orient)
        orig_target = new_overlay['targetname']

        # Only change the targetname if the overlay is not global, and we have
        # a passed name.
        if targetname and orig_target and orig_target[0] != '@':
            new_overlay['targetname'] = targetname + '-' + orig_target

        vmf.add_ent(new_overlay)
        new_over.append(new_overlay)

        # Don't let the overlays get retextured too!
        vbsp.IGNORED_OVERLAYS.add(new_overlay)

    if force_type is TEMP_TYPES.detail:
        new_detail.extend(new_world)
        new_world.clear()
    elif force_type is TEMP_TYPES.world:
        new_world.extend(new_detail)
        new_detail.clear()

    if add_to_map:
        vmf.add_brushes(new_world)

    detail_ent: Optional[Entity] = None

    if new_detail:
        detail_ent = vmf.create_ent(classname='func_detail')
        detail_ent.solids = new_detail
        if not add_to_map:
            detail_ent.remove()

    if bind_tile_pos:
        # Bind all our overlays without IDs to a set of tiles,
        # and add any marked faces to those tiles to be given overlays.
        new_overlay_faces = set(map(id_mapping.get, template.overlay_faces))
        new_overlay_faces.discard(None)
        bound_overlay_faces = [
            face for brush in (new_world + new_detail) for face in brush.sides
            if face.id in new_overlay_faces
        ]

        tile_norm = orient.up()
        for tile_off in bind_tile_pos:
            tile_off = tile_off.copy()
            tile_off.localise(origin, orient)
            try:
                tile = tiling.TILES[tile_off.as_tuple(), tile_norm.as_tuple()]
            except KeyError:
                LOGGER.warning(
                    'No tile to bind at {} for "{}"!',
                    tile_off,
                    template.id,
                )
                continue
            for over in new_over:
                if over['sides'] == '':
                    tile.bind_overlay(over)
            tile.brush_faces.extend(bound_overlay_faces)

    return ExportedTemplate(
        world=new_world,
        detail=detail_ent,
        overlay=new_over,
        orig_ids=id_mapping,
        template=template,
        origin=origin,
        orient=orient,
        visgroups=chosen_groups,
        picker_results={},  # Filled by retexture_template.
        picker_type_results={},
    )