def save_connectionpoint(item: Item, vmf: VMF) -> None: """Write connectionpoints to a VMF.""" for side, points in item.antline_points.items(): yaw = side.yaw inv_orient = Matrix.from_yaw(-yaw) for point in points: ant_pos = Vec(point.pos.x, -point.pos.y, -64) sign_pos = Vec(point.sign_off.x, -point.sign_off.y, -64) offset = (ant_pos - sign_pos) @ inv_orient try: skin = CONN_OFFSET_TO_SKIN[offset.as_tuple()] except KeyError: LOGGER.warning( 'Pos=({}), Sign=({}) -> ({}) is not a valid offset for signs!', point.pos, point.sign_off, offset) continue pos: Vec = round((ant_pos + sign_pos) / 2.0 * 16.0, 0) vmf.create_ent( 'bee2_editor_connectionpoint', origin=Vec(pos.x - 56, pos.y + 56, -64), angles=f'0 {yaw} 0', skin=skin, priority=point.priority, group_id='' if point.group is None else point.group, )
def save_embeddedvoxel(item: Item, vmf: VMF) -> None: """Save embedded voxel volumes.""" for bbox_min, bbox_max in bounding_boxes(item.embed_voxels): vmf.create_ent('bee2_editor_embeddedvoxel').solids.append( vmf.make_prism( Vec(bbox_min) * 128 + (-64.0, -64.0, -192.0), Vec(bbox_max) * 128 + (+64.0, +64.0, -64.0), # Entirely ignored, but makes it easier to distinguish. 'tools/toolshint', ).solid)
def save_occupiedvoxel(item: Item, vmf: VMF) -> None: """Save occupied voxel volumes.""" for voxel in item.occupy_voxels: pos = Vec(voxel.pos) * 128 if voxel.subpos is not None: pos += Vec(voxel.subpos) * 32 - (48, 48, 48) p1 = pos - (16.0, 16.0, 16.0) p2 = pos + (16.0, 16.0, 16.0) norm_dist = 32.0 - 4.0 else: p1 = pos - (64.0, 64.0, 64.0) p2 = pos + (64.0, 64.0, 64.0) norm_dist = 128.0 - 4.0 if voxel.normal is not None: for axis in ['x', 'y', 'z']: val = getattr(voxel.normal, axis) if val == +1: p2[axis] -= norm_dist elif val == -1: p1[axis] += norm_dist if voxel.against is not None: against = str(voxel.against).replace('COLLIDE_', '') else: against = '' vmf.create_ent( 'bee2_editor_occupiedvoxel', coll_type=str(voxel.type).replace('COLLIDE_', ''), coll_against=against, ).solids.append( vmf.make_prism( p1, p2, # Use clip for voxels, invisible for normals. # Entirely ignored, but makes it easier to use. 'tools/toolsclip' if voxel.normal is None else 'tools/toolsinvisible', ).solid)
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={}, )
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={}, )