def test_bbox() -> None: """Test the bounding box behaviour against a brute-force loop.""" rand = Random(1234) # Ensure reproducibility. SIZE = 128.0 # Build a set of points and keys. points = [(Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE)), Vec(rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE)), rand.getrandbits(64).to_bytes(8, 'little')) for _ in range(200)] tree = RTree() for a, b, data in points: tree.insert(a, b, data) # Pick a random bounding box. bb_min, bb_max = Vec.bbox( Vec( rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), ), Vec( rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), rand.uniform(-SIZE, SIZE), )) expected = [ data for a, b, data in points if Vec.bbox_intersect(*Vec.bbox(a, b), bb_min, bb_max) ] found = set(tree.find_bbox(bb_min, bb_max)) # Order is irrelevant, but duplicates must all match. assert sorted(expected) == sorted(found)
def build_itemclass_dict(prop_block: Property): """Load in the item ID database. This maps item IDs to their item class, and their embed locations. """ 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 # Now load in the embed data. for prop in prop_block.find_children('ItemEmbeds'): if prop.name not in CLASS_FOR_ITEM: LOGGER.warning('Unknown item ID with embeds "{}"!', prop.real_name) vecs = EMBED_OFFSETS.setdefault(prop.name, []) if ':' in prop.value: first, last = prop.value.split(':') bbox_min, bbox_max = Vec.bbox(Vec.from_str(first), Vec.from_str(last)) vecs.extend(Vec.iter_grid(bbox_min, bbox_max)) else: vecs.append(Vec.from_str(prop.value)) LOGGER.info( 'Read {} item IDs, with {} embeds!', len(ITEMS_WITH_CLASS), len(EMBED_OFFSETS), )
def calc_fizzler_orient(fizzler: Fizzler): # Figure out how to compare for this fizzler. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) # If it's horizontal, signs should point to the center: if abs(s.z - l.z) == 2: return ( 'z', s.x + l.x / 2, s.y + l.y / 2, s.z + 1, ) # For the vertical directions, we want to compare based on the line segment. if abs(s.x - l.x) == 2: # Y direction return ( 'y', s.y, l.y, s.x + 1, ) else: # Extends in X direction return ( 'x', s.x, l.x, s.y + 1, )
def build_collision(qc: QC, prop: PropPos, ref_mesh: Mesh) -> Optional[Mesh]: """Get the correct collision mesh for this model.""" if prop.solidity is CollType.NONE: # Non-solid return None elif prop.solidity is CollType.VPHYS or prop.solidity is CollType.BSP: if qc.phy_smd is None: return None try: return _coll_cache[qc.phy_smd] except KeyError: LOGGER.info('Parsing coll "{}"', qc.phy_smd) with open(qc.phy_smd, 'rb') as fb: coll = Mesh.parse_smd(fb) rot = Matrix.from_yaw(90) for tri in coll.triangles: for vert in tri: vert.pos @= rot vert.norm @= rot _coll_cache[qc.phy_smd] = coll return coll # Else, it's one of the three bounding box types. # We don't really care about which. bbox_min, bbox_max = Vec.bbox(vert.pos for tri in ref_mesh.triangles for vert in tri) return Mesh.build_bbox('static_prop', 'phy', bbox_min, bbox_max)
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.
def make_straight( origin: Vec, normal: Vec, dist: int, config: dict, is_start=False, ): """Make a straight line of instances from one point to another.""" # 32 added to the other directions, plus extended dist in the direction # of the normal - 1 p1 = origin + (normal * ((dist // 128 * 128) - 96)) # The starting brush needs to # stick out a bit further, to cover the # point_push entity. p2 = origin - (normal * (96 if is_start else 32)) # bbox before +- 32 to ensure the above doesn't wipe it out p1, p2 = Vec.bbox(p1, p2) solid = vbsp.VMF.make_prism( # Expand to 64x64 in the other two directions p1 - 32, p2 + 32, mat='tools/toolstrigger', ).solid motion_trigger(solid.copy()) push_trigger(origin, normal, [solid]) angles = normal.to_angle() support_file = config['support'] straight_file = config['straight'] support_positions = ( SUPPORT_POS[normal.as_tuple()] if support_file else [] ) for off in range(0, int(dist), 128): position = origin + off * normal vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=angles, file=straight_file, ) for supp_ang, supp_off in support_positions: if (position + supp_off).as_tuple() in SOLIDS: vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=supp_ang, file=support_file, )
def make_straight( origin: Vec, normal: Vec, dist: int, config: dict, is_start=False, ): """Make a straight line of instances from one point to another.""" # 32 added to the other directions, plus extended dist in the direction # of the normal - 1 p1 = origin + (normal * ((dist // 128 * 128) - 96)) # The starting brush needs to # stick out a bit further, to cover the # point_push entity. p2 = origin - (normal * (96 if is_start else 32)) # bbox before +- 32 to ensure the above doesn't wipe it out p1, p2 = Vec.bbox(p1, p2) solid = vbsp.VMF.make_prism( # Expand to 64x64 in the other two directions p1 - 32, p2 + 32, mat='tools/toolstrigger', ).solid motion_trigger(solid.copy()) push_trigger(origin, normal, [solid]) angles = normal.to_angle() support_file = config['support'] straight_file = config['straight'] support_positions = (SUPPORT_POS[normal.as_tuple()] if support_file else []) for off in range(0, int(dist), 128): position = origin + off * normal vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=angles, file=straight_file, ) for supp_ang, supp_off in support_positions: if (position + supp_off).as_tuple() in SOLIDS: vbsp.VMF.create_ent( classname='func_instance', origin=position, angles=supp_ang, file=support_file, )
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.
def group_props_auto( prop_groups: Dict[Optional[tuple], List[StaticProp]], rejected: List[StaticProp], dist: float, min_cluster: int, ) -> Iterator[List[StaticProp]]: """Given the groups of props, automatically find close props to merge.""" # Each of these groups cannot be merged with other ones. dist_sq = dist * dist large_dist_sq = 4 * dist_sq for group in prop_groups.values(): # No point merging single/empty groups. if len(group) < 2: rejected.extend(group) continue todo = set(group) while todo: center = todo.pop() cluster = {center} for prop in todo: if (center.origin - prop.origin).mag_sq() <= large_dist_sq: cluster.add(prop) if len(cluster) > MAX_GROUP: # Limit the number of maximum props that can be used. break if len(cluster) < min_cluster: rejected.append(center) continue bbox_min, bbox_max = Vec.bbox(prop.origin for prop in cluster) center_pos = (bbox_min + bbox_max) / 2 cluster_list = [] for prop in cluster: prop_off = (center_pos - prop.origin).mag_sq() if prop_off <= dist_sq: cluster_list.append((prop, prop_off)) cluster_list.sort(key=lambda t: t[1]) selected_props = [prop for prop, off in cluster_list[:MAX_GROUP]] todo.difference_update(selected_props) if len(selected_props) >= min_cluster: yield selected_props else: rejected.extend(selected_props)
def res_set_block(inst: Entity, res: Property) -> None: """Set a block to the given value, overwriting the existing value. - `type` is the type of block to set: * `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_SINGLE` (one-high) * `PIT_TOP` * `PIT_MID` * `PIT_BOTTOM` * `GOO_SINGLE` (one-deep goo) * `GOO_TOP` (goo surface) * `GOO_MID` * `GOO_BOTTOM` (floor) - `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 set. """ try: new_vals = brushLoc.BLOCK_LOOKUP[res['type'].casefold()] except KeyError: raise ValueError('"{}" is not a valid block type!'.format(res['type'])) try: [new_val] = new_vals except ValueError: # TODO: This could spread top/mid/bottom through the bbox... raise ValueError( f'Can\'t use compound block type "{res["type"]}", specify ' "_SINGLE/TOP/MID/BOTTOM" ) pos1 = resolve_offset(inst, res['offset', '0 0 0'], scale=128, zoff=-128) if 'offset2' in res: pos2 = resolve_offset(inst, res['offset2', '0 0 0'], scale=128, zoff=-128) for pos in Vec.iter_grid(*Vec.bbox(pos1, pos2), stride=128): brushLoc.POS['world': pos] = new_val else: brushLoc.POS['world': pos1] = new_val
def test_bbox_rotation( pitch: float, yaw: float, roll: float, ) -> None: """Test the rotation logic against the slow direct approach.""" ang = Angle(pitch, yaw, roll) bb_start = BBox(100, 200, 300, 300, 450, 600, contents=CollideType.ANTLINES, tags='blah') # Directly compute, by rotating all the angles, points = [ Vec(x, y, z) for x in [100, 300] for y in [200, 450] for z in [300, 600] ] result_ang = bb_start @ ang result_mat = bb_start @ Matrix.from_angle(ang) assert result_ang == result_mat bb_min, bb_max = Vec.bbox( point @ ang for point in points ) assert_bbox(result_mat, round(bb_min, 0), round(bb_max, 0), CollideType.ANTLINES, {'blah'})
def build_bbox(cls, root_bone: str, mat: str, bbox_min: Vec, bbox_max: Vec) -> 'Mesh': """Construct a mesh for a bounding box.""" mesh = cls.blank(root_bone) [root] = mesh.bones.values() links = [(root, 1.0)] bbox_min, bbox_max = Vec.bbox(bbox_min, bbox_max) for tri_def in cls._BBOX_MESH_DATA: tri = Triangle( mat, *[ Vertex( Vec( bbox_max.x if x > 0 else bbox_min.x, bbox_max.y if y > 0 else bbox_min.y, bbox_max.z if z > 0 else bbox_min.z, ), Vec(x, y, z).norm(), u, v, links.copy()) for x, y, z, u, v in tri_def ]) mesh.triangles.append(tri) return mesh
def comp_prop_rope(ctx: Context) -> None: """Build static props for ropes.""" compiler = ModelCompiler.from_ctx(ctx, 'ropes') # id -> node. all_nodes: MutableMapping[NodeID, NodeEnt] = {} # Given a targetname, all the nodes with that name. name_to_nodes: MutableMapping[str, List[NodeEnt]] = defaultdict(list) # Group name -> nodes with that group. group_to_node: Dict[str, List[NodeEnt]] = defaultdict(list) # Store the node/next-key pairs for linking after they're all parsed. temp_conns: List[Tuple[NodeEnt, str]] = [] for ent in ctx.vmf.by_class['comp_prop_rope'] | ctx.vmf.by_class[ 'comp_prop_cable']: ent.remove() conf = Config.parse(ent) node = NodeEnt( Vec.from_str(ent['origin']), conf, NodeID(ent['hammerid']), ent['group'].casefold(), ) all_nodes[node.id] = node if node.group: group_to_node[node.group].append(node) if ent['targetname']: name_to_nodes[ent['targetname'].casefold()].append(node) if ent['nextkey']: temp_conns.append((node, ent['nextkey'].casefold())) if not all_nodes: return LOGGER.info('{} rope nodes found.', len(all_nodes)) connections_to: Dict[NodeID, List[NodeEnt]] = defaultdict(list) connections_from: Dict[NodeID, List[NodeEnt]] = defaultdict(list) for node, target in temp_conns: found: List[NodeEnt] = [] if target.endswith('*'): search = target[:-1] for name, nodes in name_to_nodes.items(): if name.startswith(search): found.extend(nodes) else: found.extend(name_to_nodes.get(target, ())) found.sort() for dest in found: connections_from[node.id].append(dest) connections_to[dest.id].append(node) static_props = list(ctx.bsp.static_props()) vis_tree_top = ctx.bsp.vis_tree() # To group nodes, take each group out, then search recursively through # all connections from it to other nodes. todo = set(all_nodes.values()) with compiler: while todo: node = todo.pop() connections: Set[Tuple[NodeID, NodeID]] = set() # We need the set for fast is-in checks, and the list # so we can loop through while modifying it. nodes: Set[NodeEnt] = {node} unchecked: List[NodeEnt] = [node] while unchecked: node = unchecked.pop() # Three links to others - connections to/from, and groups. # We'll only ever follow a path once, so pop from the dicts. if node.group: for subnode in group_to_node.pop(node.group, ()): if subnode not in nodes: nodes.add(subnode) unchecked.append(subnode) for conn_node in connections_from.pop(node.id, ()): connections.add((node.id, conn_node.id)) if conn_node not in nodes: nodes.add(conn_node) unchecked.append(conn_node) for conn_node in connections_to.pop(node.id, ()): connections.add((conn_node.id, node.id)) if conn_node not in nodes: nodes.add(conn_node) unchecked.append(conn_node) todo -= nodes if len(nodes) == 1: LOGGER.warning( 'Node at {} has no connections to it! Skipping.', node.pos) continue bbox_min, bbox_max = Vec.bbox(node.pos for node in nodes) center = (bbox_min + bbox_max) / 2 node = None for node in nodes: node.pos -= center model_name, coll_data = compiler.get_model( (frozenset(nodes), frozenset(connections)), build_rope, center, ) # Use the node closest to the center. That way # it shouldn't be inside walls, and be about representative of # the whole model. light_origin = min( (point for point1, radius1, point2, radius2 in coll_data for point in [point1, point2]), key=lambda pos: (pos - center).mag_sq()) # Compute the flags. Just pick a random node, from above. conf = node.config flags = StaticPropFlags.NONE if conf.prop_light_bounce: flags |= StaticPropFlags.BOUNCED_LIGHTING if conf.prop_no_shadows: flags |= StaticPropFlags.NO_SHADOW if conf.prop_no_vert_light: flags |= StaticPropFlags.NO_PER_VERTEX_LIGHTING if conf.prop_no_self_shadow: flags |= StaticPropFlags.NO_SELF_SHADOWING static_props.append( StaticProp( model=model_name, origin=center, angles=Angle(0, 270, 0), scaling=1.0, visleafs=compute_visleafs(coll_data, vis_tree_top), solidity=0, flags=flags, tint=Vec(conf.prop_rendercolor), renderfx=conf.prop_renderalpha, lighting_origin=light_origin, min_fade=conf.prop_fade_min_dist, max_fade=conf.prop_fade_max_dist, fade_scale=conf.prop_fade_scale, )) LOGGER.info('Built {} models.', len(all_nodes)) ctx.bsp.write_static_props(static_props)
def __init__(self, point1: Vec, point2: Vec) -> None: self.bbox_min, self.bbox_max = Vec.bbox(point1, point2)
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) temp_world, temp_detail, temp_over = template.visgrouped() try: [beam_template] = temp_world + temp_detail # type: Solid except ValueError: raise ValueError('Bad Glass Floorbeam template!') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict # locations to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({id(group): group for group in groups.values()}.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Matrix() else: beam_ax = 'y' side_ax = 'x' rot = Matrix.from_yaw(90) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams: dict[float, tuple[float, float]] = {} # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_pos, max_pos = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """Make a straight line of instances from one point to another.""" # 32 added to the other directions, plus extended dist in the direction # of the normal - 1 p1 = origin + (normal * ((dist // 128 * 128) - 96)) # The starting brush needs to # stick out a bit further, to cover the # point_push entity. p2 = origin - (normal * (96 if is_start else 32)) # bbox before +- 32 to ensure the above doesn't wipe it out p1, p2 = Vec.bbox(p1, p2) solid = vmf.make_prism( # Expand to 64x64 in the other two directions p1 - 32, p2 + 32, mat='tools/toolstrigger', ).solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) angles = normal.to_angle() orient = Matrix.from_angle(angles) for off in range(0, int(dist), 128): position = origin + off * normal vmf.create_ent( classname='func_instance', origin=position, angles=orient.to_angle(), file=config.inst_straight, ) for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[(position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple()] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): vmf.create_ent( classname='func_instance', origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, )
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6) origin = Vec.from_str(inst['origin']) uaxis, vaxis = Vec.INV_AXIS[normal.axis()] points: Set[Tuple[float, float, float]] = set() if 'point' in props: for prop in props.find_all('point'): points.add( conditions.resolve_offset(inst, prop.value, zoff=-64).as_tuple()) elif 'pos1' in props and 'pos2' in props: pos1, pos2 = Vec.bbox( conditions.resolve_offset(inst, props['pos1', '-48 -48 0'], zoff=-64), conditions.resolve_offset(inst, props['pos2', '48 48 0'], zoff=-64), ) points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32))) else: # Default to the full tile. points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple() for u in [-48.0, -16.0, 16.0, 48.0] for v in [-48.0, -16.0, 16.0, 48.0]}) tiles_to_uv: Dict[tiling.TileDef, Set[Tuple[int, int]]] = defaultdict(set) for pos in points: try: tile, u, v = tiling.find_tile(Vec(pos), normal, force=create) except KeyError: continue tiles_to_uv[tile].add((u, v)) if not tiles_to_uv: LOGGER.warning('"{}": No tiles found for panels!', inst['targetname']) return # If bevels is provided, parse out the overall world positions. bevel_world: Optional[Set[Tuple[int, int]]] try: bevel_prop = props.find_key('bevel') except NoKeyError: bevel_world = None else: bevel_world = set() if bevel_prop.has_children(): # Individually specifying offsets. for bevel_str in bevel_prop.as_array(): bevel_point = Vec.from_str(bevel_str) @ orient + origin bevel_world.add( (int(bevel_point[uaxis]), int(bevel_point[vaxis]))) elif srctools.conv_bool(bevel_prop.value): # Fill the bounding box. bbox_min, bbox_max = Vec.bbox(map(Vec, points)) off = Vec.with_axes(uaxis, 32, vaxis, 32) bbox_min -= off bbox_max += off for pos in Vec.iter_grid(bbox_min, bbox_max, 32): if pos.as_tuple() not in points: bevel_world.add((pos[uaxis], pos[vaxis])) # else: No bevels. panels: List[tiling.Panel] = [] for tile, uvs in tiles_to_uv.items(): if create: panel = tiling.Panel( None, inst, tiling.PanelType.NORMAL, thickness=4, bevels=(), ) panel.points = uvs tile.panels.append(panel) else: for panel in tile.panels: if panel.same_item(inst) and panel.points == uvs: break else: LOGGER.warning('No panel to modify found for "{}"!', inst['targetname']) continue panels.append(panel) pan_type = '<nothing?>' try: pan_type = conditions.resolve_value(inst, props['type']) panel.pan_type = tiling.PanelType(pan_type.lower()) except LookupError: pass except ValueError: raise ValueError('Unknown panel type "{}"!'.format(pan_type)) if 'thickness' in props: panel.thickness = srctools.conv_int( conditions.resolve_value(inst, props['thickness'])) if panel.thickness not in (2, 4, 8): raise ValueError( '"{}": Invalid panel thickess {}!\n' 'Must be 2, 4 or 8.', inst['targetname'], panel.thickness, ) if bevel_world is not None: panel.bevels.clear() for u, v in bevel_world: # Convert from world points to UV positions. u = (u - tile.pos[uaxis] + 48) / 32 v = (v - tile.pos[vaxis] + 48) / 32 # Cull outside here, we wont't use them. if -1 <= u <= 4 and -1 <= v <= 4: panel.bevels.add((u, v)) if 'offset' in props: panel.offset = conditions.resolve_offset(inst, props['offset']) panel.offset -= Vec.from_str(inst['origin']) if 'template' in props: # We only want the template inserted once. So remove it from all but one. if len(panels) == 1: panel.template = conditions.resolve_value( inst, props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( conditions.resolve_value(inst, props['nodraw'])) if 'seal' in props: panel.seal = srctools.conv_bool( conditions.resolve_value(inst, props['seal'])) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( conditions.resolve_value(inst, props['move_bullseye'])) if 'keys' in props or 'localkeys' in props: # First grab the existing ent, so we can edit it. # These should all have the same value, unless they were independently # edited with mismatching point sets. In that case destroy all those existing ones. existing_ents: Set[Optional[Entity]] = { panel.brush_ent for panel in panels } try: [brush_ent] = existing_ents except ValueError: LOGGER.warning( 'Multiple independent panels for "{}" were made, then the ' 'brush entity was edited as a group! Discarding ' 'individual ents...', inst['targetname']) for brush_ent in existing_ents: if brush_ent is not None and brush_ent in vmf.entities: brush_ent.remove() brush_ent = None if brush_ent is None: brush_ent = vmf.create_ent('') old_pos = brush_ent.keys.pop('origin', None) conditions.set_ent_keys(brush_ent, inst, props) if not brush_ent['classname']: if create: # This doesn't make sense, you could just omit the prop. LOGGER.warning( 'No classname provided for panel "{}"!', inst['targetname'], ) # Make it a world brush. brush_ent.remove() brush_ent = None else: # We want to do some post-processing. # Localise any origin value. if 'origin' in brush_ent.keys: pos = Vec.from_str(brush_ent['origin']) pos.localise( Vec.from_str(inst['origin']), Vec.from_str(inst['angles']), ) brush_ent['origin'] = pos elif old_pos is not None: brush_ent['origin'] = old_pos # If it's func_detail, clear out all the keys. # Particularly `origin`, but the others are useless too. if brush_ent['classname'] == 'func_detail': brush_ent.clear_keys() brush_ent['classname'] = 'func_detail' for panel in panels: panel.brush_ent = brush_ent
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_conn_conf, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, Optional[connections.Config], str, str, str, str, str, str import vbsp if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = connections.ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) fizz_norm_axis = fizzler.normal().axis() # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min( (sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, )), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str( max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1)) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. `up_axis` should be set to a normal vector pointing in the new 'upward' direction. `default` is the ID of a fizzler type which should be used if no outputs are fired. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS[shape_name] for conn in shape_item.outputs: fizz_name = conn.inp.name try: fizz = fizzler.FIZZLERS[fizz_name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output! Ignoring!') continue break else: # No fizzler - create one. conn = None fizz_type = fizzler.FIZZ_TYPES[res['default']] base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], file=fizz_type.inst[fizzler.FizzInst.BASE][0], ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizz_type, Vec(), base_inst, [], ) # Detach this connection and remove traces of it. if conn: conn.remove() if shape_item.ind_toggle: remove_ant_toggle(shape_item.ind_toggle) fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) shape_angles = Vec.from_str(shape_inst['angles']) fizz.up_axis = res.vec('up_axis').rotate(*shape_angles) fizz.emitters.clear() for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. `up_axis` should be set to a normal vector pointing in the new 'upward' direction. `default` is the ID of a fizzler type which should be used if no outputs are fired. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_angles = Vec.from_str(shape_inst['angles']) up_axis = res.vec('up_axis').rotate(*shape_angles) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output ({})! Ignoring!', fizz_item.name) continue fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], file=resolve_inst('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], shape_item.ant_floor_style, shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Detach this connection and remove traces of it. for conn in list(shape_item.outputs): conn.remove() for coll in [ shape_item.antlines, shape_item.ind_panels, shape_item.shape_signs ]: for ent in coll: ent.remove() coll.clear() for inp in list(shape_item.inputs): inp.to_item = fizz_item fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def flag_check_marker(inst: Entity, flag: Property) -> bool: """Check if markers are present at a position. Parameters: * `name`: The name to look for. This can contain one `*` to match prefixes/suffixes. * `nameVar`: If found, set this variable to the actual name. * `pos`: The position to check. * `pos2`: If specified, the position is a bounding box from 1 to 2. * `radius`: Check markers within this distance. If this is specified, `pos2` is not permitted. * `global`: If true, positions are an absolute position, ignoring this instance. * `removeFound`: If true, remove the found marker. If you don't need it, this will improve performance. * `copyto`: Copies fixup vars from the searching instance to the one which set the marker. The value is in the form `$src $dest`. * `copyfrom`: Copies fixup vars from the one that set the marker to the searching instance. The value is in the form `$src $dest`. """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) name = inst.fixup.substitute(flag['name']).casefold() if '*' in name: try: prefix, suffix = name.split('*') except ValueError: raise ValueError(f'Name "{name}" must only have 1 *!') def match(val: str) -> bool: """Match a prefix or suffix.""" val = val.casefold() return val.startswith(prefix) and val.endswith(suffix) else: def match(val: str) -> bool: """Match an exact name.""" return val.casefold() == name try: is_global = srctools.conv_bool( inst.fixup.substitute(flag['global'], allow_invert=True)) except LookupError: is_global = False pos = Vec.from_str(inst.fixup.substitute(flag['pos'])) if not is_global: pos = pos @ orient + origin radius: float | None if 'pos2' in flag: if 'radius' in flag: raise ValueError('Only one of pos2 or radius must be defined.') pos2 = Vec.from_str(inst.fixup.substitute(flag['pos2'])) if not is_global: pos2 = pos2 @ orient + origin bb_min, bb_max = Vec.bbox(pos, pos2) radius = None LOGGER.debug('Searching for marker "{}" from ({})-({})', name, bb_min, bb_max) elif 'radius' in flag: radius = abs(srctools.conv_float(inst.fixup.substitute( flag['radius']))) bb_min = pos - (radius + 1.0) bb_max = pos + (radius + 1.0) LOGGER.debug('Searching for marker "{}" at ({}), radius={}', name, pos, radius) else: bb_min = pos - (1.0, 1.0, 1.0) bb_max = pos + (1.0, 1.0, 1.0) radius = 1e-6 LOGGER.debug('Searching for marker "{}" at ({})', name, pos) for i, marker in enumerate(MARKERS): if not marker.pos.in_bbox(bb_min, bb_max): continue if radius is not None and (marker.pos - pos).mag() > radius: continue if not match(marker.name): continue # Matched. if 'nameVar' in flag: inst.fixup[flag['namevar']] = marker.name if srctools.conv_bool( inst.fixup.substitute(flag['removeFound'], allow_invert=True)): LOGGER.debug('Removing found marker {}', marker) del MARKERS[i] for prop in flag.find_all('copyto'): src, dest = prop.value.split(' ', 1) marker.inst.fixup[dest] = inst.fixup[src] for prop in flag.find_all('copyfrom'): src, dest = prop.value.split(' ', 1) inst.fixup[dest] = marker.inst.fixup[src] return True return False
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None: """Analyse fizzler instances to assign fizzler types. Instance traits are required. The model instances and brushes will be removed from the map. Needs connections to be parsed. """ # Item ID and model skin -> fizzler type fizz_types = {} # type: Dict[Tuple[str, int], FizzlerType] for fizz_type in FIZZ_TYPES.values(): for item_id in fizz_type.item_ids: if ':' in item_id: item_id, barrier_type = item_id.split(':') if barrier_type == 'laserfield': barrier_skin = 2 elif barrier_type == 'fizzler': barrier_skin = 0 else: LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id) fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type continue fizz_types[item_id, barrier_skin] = fizz_type else: fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type fizz_bases = {} # type: Dict[str, Entity] fizz_models = defaultdict(list) # type: Dict[str, List[Entity]] # Position and normal -> name, for output relays. fizz_pos = {} # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str] # First use traits to gather up all the instances. for inst in vmf.by_class['func_instance']: traits = instance_traits.get(inst) if 'fizzler' not in traits: continue name = inst['targetname'] if 'fizzler_model' in traits: name = name.rsplit('_model', 1)[0] fizz_models[name].append(inst) inst.remove() elif 'fizzler_base' in traits: fizz_bases[name] = inst else: LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name) continue origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) fizz_pos[origin.as_tuple(), normal.as_tuple()] = name for name, base_inst in fizz_bases.items(): models = fizz_models[name] up_axis = Vec(y=1).rotate_by_str(base_inst['angles']) # If upside-down, make it face upright. if up_axis == (0, 0, -1): up_axis = Vec(z=1) base_inst.outputs.clear() # Now match the pairs of models to each other. # The length axis is the line between them. # We don't care about the instances after this, so don't keep track. length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis() emitters = [] # type: List[Tuple[Vec, Vec]] model_pairs = {} # type: Dict[Tuple[float, float], Vec] model_skin = models[0].fixup.int('$skin') try: item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[base_inst['file'].casefold()] fizz_type = fizz_types[item_id, model_skin] except KeyError: LOGGER.warning('Fizzler types: {}', fizz_types.keys()) raise ValueError('No fizzler type for "{}"!'.format( base_inst['file'], )) from None for attr_name in fizz_type.voice_attrs: voice_attrs[attr_name] = True for model in models: pos = Vec.from_str(model['origin']) try: other_pos = model_pairs.pop(pos.other_axes(length_axis)) except KeyError: # No other position yet, we need to find that. model_pairs[pos.other_axes(length_axis)] = pos continue min_pos, max_pos = Vec.bbox(pos, other_pos) # Move positions to the wall surface. min_pos[length_axis] -= 64 max_pos[length_axis] += 64 emitters.append((min_pos, max_pos)) FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters) # Delete all the old brushes associated with fizzlers for brush in ( vmf.by_class['trigger_portal_cleanser'] | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush'] ): name = brush['targetname'] if not name: continue name = name.rsplit('_brush')[0] if name in FIZZLERS: brush.remove() # Check for fizzler output relays. relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>', silent=True) if not relay_file: # No relay item - deactivated most likely. return for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in relay_file: continue inst.remove() relay_item = connections.ITEMS[inst['targetname']] try: fizz_name = fizz_pos[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple() ] fizz_item = connections.ITEMS[fizz_name] except KeyError: # Not placed on a fizzler, or a fizzler with no IO # - ignore, and destroy. for out in list(relay_item.outputs): out.remove() for out in list(relay_item.inputs): out.remove() del connections.ITEMS[relay_item.name] continue # Copy over fixup values fizz_item.inst.fixup.update(inst.fixup) # Copy over the timer delay set in the relay. fizz_item.timer = relay_item.timer # Transfer over antlines. fizz_item.antlines |= relay_item.antlines fizz_item.shape_signs += relay_item.shape_signs fizz_item.ind_panels |= relay_item.ind_panels # Remove the relay item so it doesn't get added to the map. del connections.ITEMS[relay_item.name] for conn in list(relay_item.outputs): conn.from_item = fizz_item
def add_glass_floorbeams(vmf: VMF, temp_name: str): """Add beams to separate large glass panels. The texture is assumed to match plasticwall004a's shape. """ template = template_brush.get_template(temp_name) temp_world, temp_detail, temp_over = template.visgrouped() try: [beam_template] = temp_world + temp_detail # type: Solid except ValueError: raise ValueError('Bad Glass Floorbeam template!') # Grab the 'end' side, which we move around. for side in beam_template.sides: if side.normal() == (-1, 0, 0): beam_end_face = side break else: raise ValueError('Not aligned to world...') separation = vbsp_options.get(int, 'glass_floorbeam_sep') + 1 separation *= 128 # First we want to find all the groups of contiguous glass sections. # This is a mapping from some glass piece to its group list. groups = {} for (origin, normal), barr_type in BARRIERS.items(): # Grating doesn't use it. if barr_type is not BarrierType.GLASS: continue normal = Vec(normal) if not normal.z: # Not walls. continue pos = Vec(origin) + normal * 62 groups[pos.as_tuple()] = [pos] # Loop over every pos and check in the +x/y directions for another glass # piece. If there, merge the two lists and set every pos in the group to # point to the new list. # Once done, every unique list = a group. for pos_tup in groups.keys(): pos = Vec(pos_tup) for off in ((128, 0, 0), (0, 128, 0)): neighbour = (pos + off).as_tuple() if neighbour in groups: our_group = groups[pos_tup] neigh_group = groups[neighbour] if our_group is neigh_group: continue # Now merge the two lists. We then need to update all dict locs # to point to the new list. if len(neigh_group) > len(our_group): small_group, large_group = our_group, neigh_group else: small_group, large_group = neigh_group, our_group large_group.extend(small_group) for pos in small_group: groups[pos.as_tuple()] = large_group # Remove duplicates objects by using the ID as key.. groups = list({ id(group): group for group in groups.values() }.values()) # Side -> u, v or None for group in groups: bbox_min, bbox_max = Vec.bbox(group) dimensions = bbox_max - bbox_min LOGGER.info('Size = {}', dimensions) # Our beams align to the smallest axis. if dimensions.y > dimensions.x: beam_ax = 'x' side_ax = 'y' rot = Vec(0, 0, 0) else: beam_ax = 'y' side_ax = 'x' rot = Vec(0, 90, 0) # Build min, max tuples for each axis in the other direction. # This tells us where the beams will be. beams = {} # type: Dict[int, Tuple[int, int]] # Add 128 so the first pos isn't a beam. offset = bbox_min[side_ax] + 128 for pos in group: side_off = pos[side_ax] beam_off = pos[beam_ax] # Skip over non-'sep' positions.. if (side_off - offset) % separation != 0: continue try: min_pos, max_pos = beams[side_off] except KeyError: beams[side_off] = beam_off, beam_off else: beams[side_off] = min(min_pos, beam_off), max(max_pos, beam_off) detail = vmf.create_ent('func_detail') for side_off, (min_off, max_off) in beams.items(): for min_pos, max_pos in beam_hole_split( beam_ax, Vec.with_axes(side_ax, side_off, beam_ax, min_off, 'z', bbox_min), Vec.with_axes(side_ax, side_off, beam_ax, max_off, 'z', bbox_min), ): if min_pos[beam_ax] >= max_pos[beam_ax]: raise ValueError(min_pos, max_pos, beam_ax) # Make the beam. # Grab the end face and snap to the length we want. beam_end_off = max_pos[beam_ax] - min_pos[beam_ax] assert beam_end_off > 0, beam_end_off for plane in beam_end_face.planes: plane.x = beam_end_off new_beam = beam_template.copy(vmf_file=vmf) new_beam.localise(min_pos, rot) detail.solids.append(new_beam)
def group_props_ent( prop_groups: Dict[Optional[tuple], List[StaticProp]], rejected: List[StaticProp], get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]], bbox_ents: List[Entity], min_cluster: int, ) -> Iterator[List[StaticProp]]: """Given the groups of props, merge props according to the provided ents.""" # Ents with group names. We have to split those by filter too. grouped_sets: Dict[Tuple[str, FrozenSet[str]], CombineVolume] = {} # Skinset filter -> volumes that match. sets_by_skin: Dict[FrozenSet[str], List[CombineVolume]] = defaultdict(list) empty_fs = frozenset('') for ent in bbox_ents: origin = Vec.from_str(ent['origin']) skinset = empty_fs mdl_name = ent['prop'] if mdl_name: qc, mdl = get_model(mdl_name) if mdl is not None: skinset = frozenset({ tex.casefold().replace('\\', '/') for tex in mdl.iter_textures([conv_int(ent['skin'])]) }) # Compute 6 planes to use for collision detection. mat = Matrix.from_angle(Angle.from_str(ent['angles'])) mins, maxes = Vec.bbox( Vec.from_str(ent['mins']), Vec.from_str(ent['maxs']), ) size = maxes - mins # Enlarge slightly to ensure it never has a zero area. # Otherwise the normal could potentially be invalid. mins -= 0.05 maxes += 0.05 # Group name group_name = ent['name'] if group_name: try: combine_set = grouped_sets[group_name, skinset] except KeyError: combine_set = grouped_sets[group_name, skinset] = CombineVolume( group_name, skinset, origin) sets_by_skin[skinset].append(combine_set) else: combine_set = CombineVolume(group_name, skinset, origin) sets_by_skin[skinset].append(combine_set) combine_set.volume += size.x * size.y * size.z # For each direction, compute a position on the plane and # the normal vector. combine_set.collision.append([( origin + Vec.with_axes(axis, offset) @ mat, Vec.with_axes(axis, norm) @ mat, ) for offset, norm in zip([mins, maxes], (-1, 1)) for axis in ('x', 'y', 'z')]) # We want to apply a ordering to groups, so smaller ones apply first, and # filtered ones override all others. for group_list in sets_by_skin.values(): group_list.sort(key=operator.attrgetter('volume')) # Groups with no filter have no skins in the group. unfiltered_group = sets_by_skin.get(frozenset(), []) # Each of these groups cannot be merged with other ones. for group_key, group in prop_groups.items(): if group_key is None: continue # No point merging single/empty groups. group_skinset = group_key[0] if len(group) < min_cluster: rejected.extend(group) group.clear() continue for combine_set in itertools.chain(sets_by_skin.get(group_skinset, ()), unfiltered_group): found = [] for prop in list(group): if combine_set.contains(prop.origin): found.append(prop) combine_set.used = True actual = set(found).intersection(group) if len(actual) >= min_cluster: yield list(actual) for prop in actual: group.remove(prop) # Finally, reject all the ones not in a bbox. for group in prop_groups.values(): rejected.extend(group) # And log unused groups for combine_set_list in sets_by_skin.values(): for combine_set in combine_set_list: if not combine_set.used: LOGGER.warning('Unused comp_propcombine_set {}', combine_set.desc)
def make_straight( vmf: VMF, origin: Vec, normal: Vec, dist: int, config: Config, is_start=False, ) -> None: """Make a straight line of instances from one point to another.""" angles = round(normal, 6).to_angle() orient = Matrix.from_angle(angles) # The starting brush needs to stick out a bit further, to cover the # point_push entity. start_off = -96 if is_start else -64 p1, p2 = Vec.bbox( origin + Vec(start_off, -config.trig_radius, -config.trig_radius) @ orient, origin + Vec(dist - 64, config.trig_radius, config.trig_radius) @ orient, ) solid = vmf.make_prism(p1, p2, mat='tools/toolstrigger').solid motion_trigger(vmf, solid.copy()) push_trigger(vmf, origin, normal, [solid]) off = 0 for seg_dist in utils.fit(dist, config.inst_straight_sizes): vmf.create_ent( classname='func_instance', origin=origin + off * orient.forward(), angles=angles, file=config.inst_straight[seg_dist], ) off += seg_dist # Supports. if config.inst_support: for off in range(0, int(dist), 128): position = origin + off * normal placed_support = False for supp_dir in [ orient.up(), orient.left(), -orient.left(), -orient.up() ]: try: tile = tiling.TILES[ (position - 128 * supp_dir).as_tuple(), supp_dir.norm().as_tuple() ] except KeyError: continue # Check all 4 center tiles are present. if all(tile[u, v].is_tile for u in (1, 2) for v in (1, 2)): vmf.create_ent( classname='func_instance', origin=position, angles=Matrix.from_basis(x=normal, z=supp_dir).to_angle(), file=config.inst_support, ) placed_support = True if placed_support and config.inst_support_ring: vmf.create_ent( classname='func_instance', origin=position, angles=angles, file=config.inst_support_ring, )
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name( res['coopActivate', 'OnChangeToAllTrue'] ), output_deact=Output.parse_name( res['coopDeactivate', 'OnChangeToAnyFalse'] ), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin']) ) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. * Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. * `up_axis` should be set to a normal vector pointing in the new 'upward' direction. * If none are connected, a regular fizzler will be synthesized. The following fixup vars will be set to allow the shape to match the fizzler: * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_orient = Matrix.from_angle(Angle.from_str(shape_inst['angles'])) up_axis: Vec = round(res.vec('up_axis') @ shape_orient, 6) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: continue # Detach this connection and remove traces of it. conn.remove() fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis fizz.base_inst['origin'] = shape_inst['origin'] fizz.base_inst['angles'] = shape_inst['angles'] break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = conditions.add_inst( vmf, targetname=shape_name, origin=shape_inst['origin'], angles=shape_inst['angles'], file=resolve_one('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], ant_floor_style=shape_item.ant_floor_style, ant_wall_style=shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Transfer the input/outputs from us to the fizzler. for inp in list(shape_item.inputs): inp.to_item = fizz_item for conn in list(shape_item.outputs): conn.from_item = fizz_item # If the fizzler has no outputs, then strip out antlines. Otherwise, # they need to be transferred across, so we can't tell safely. if fizz_item.output_act() is None and fizz_item.output_deact() is None: shape_item.delete_antlines() else: shape_item.transfer_antlines(fizz_item) fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) fizz.has_cust_position = True # Since the fizzler is moved elsewhere, it's the responsibility of # the new item to have holes. fizz.embedded = False # So tell it whether or not it needs to do so. shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1) @ shape_orient + origin, Vec.from_str(vec2) @ shape_orient + origin, ) fizz.emitters.append(seg_min_max)
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None: """Analyse fizzler instances to assign fizzler types. Instance traits are required. The model instances and brushes will be removed from the map. Needs connections to be parsed. """ # Item ID and model skin -> fizzler type fizz_types = {} # type: Dict[Tuple[str, int], FizzlerType] for fizz_type in FIZZ_TYPES.values(): for item_id in fizz_type.item_ids: if ':' in item_id: item_id, barrier_type = item_id.split(':') if barrier_type == 'laserfield': barrier_skin = 2 elif barrier_type == 'fizzler': barrier_skin = 0 else: LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id) fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type continue fizz_types[item_id, barrier_skin] = fizz_type else: fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type fizz_bases = {} # type: Dict[str, Entity] fizz_models = defaultdict(list) # type: Dict[str, List[Entity]] # Position and normal -> name, for output relays. fizz_pos = { } # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str] # First use traits to gather up all the instances. for inst in vmf.by_class['func_instance']: traits = instance_traits.get(inst) if 'fizzler' not in traits: continue name = inst['targetname'] if 'fizzler_model' in traits: name = name.rsplit('_model', 1)[0] fizz_models[name].append(inst) inst.remove() elif 'fizzler_base' in traits: fizz_bases[name] = inst else: LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name) continue origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) fizz_pos[origin.as_tuple(), normal.as_tuple()] = name for name, base_inst in fizz_bases.items(): models = fizz_models[name] up_axis = Vec(y=1).rotate_by_str(base_inst['angles']) # If upside-down, make it face upright. if up_axis == (0, 0, -1): up_axis = Vec(z=1) base_inst.outputs.clear() # Now match the pairs of models to each other. # The length axis is the line between them. # We don't care about the instances after this, so don't keep track. length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis() emitters = [] # type: List[Tuple[Vec, Vec]] model_pairs = {} # type: Dict[Tuple[float, float], Vec] model_skin = models[0].fixup.int('$skin') try: item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[ base_inst['file'].casefold()] fizz_type = fizz_types[item_id, model_skin] except KeyError: LOGGER.warning('Fizzler types: {}', fizz_types.keys()) raise ValueError('No fizzler type for "{}"!'.format( base_inst['file'], )) from None for attr_name in fizz_type.voice_attrs: voice_attrs[attr_name] = True for model in models: pos = Vec.from_str(model['origin']) try: other_pos = model_pairs.pop(pos.other_axes(length_axis)) except KeyError: # No other position yet, we need to find that. model_pairs[pos.other_axes(length_axis)] = pos continue min_pos, max_pos = Vec.bbox(pos, other_pos) # Move positions to the wall surface. min_pos[length_axis] -= 64 max_pos[length_axis] += 64 emitters.append((min_pos, max_pos)) FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters) # Delete all the old brushes associated with fizzlers for brush in (vmf.by_class['trigger_portal_cleanser'] | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush']): name = brush['targetname'] if not name: continue name = name.rsplit('_brush')[0] if name in FIZZLERS: brush.remove() # Check for fizzler output relays. relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>', silent=True) if not relay_file: # No relay item - deactivated most likely. return for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in relay_file: continue inst.remove() relay_item = connections.ITEMS[inst['targetname']] try: fizz_name = fizz_pos[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] fizz_item = connections.ITEMS[fizz_name] except KeyError: # Not placed on a fizzler, or a fizzler with no IO # - ignore, and destroy. for out in list(relay_item.outputs): out.remove() for out in list(relay_item.inputs): out.remove() del connections.ITEMS[relay_item.name] continue # Copy over fixup values fizz_item.inst.fixup.update(inst.fixup) # Copy over the timer delay set in the relay. fizz_item.timer = relay_item.timer # Transfer over antlines. fizz_item.antlines |= relay_item.antlines fizz_item.shape_signs += relay_item.shape_signs fizz_item.ind_panels |= relay_item.ind_panels # Remove the relay item so it doesn't get added to the map. del connections.ITEMS[relay_item.name] for conn in list(relay_item.outputs): conn.from_item = fizz_item
def res_resizeable_trigger(res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: 'markerInst': <ITEM_ID:1,2> value referencing the marker instances, or a filename. 'markerItem': The item's ID 'previewVar': A stylevar which enables/disables the preview overlay. 'previewinst': An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. 'previewMat': If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. 'previewScale': The scale for the func_brush materials. 'previewActivate', 'previewDeactivate': The 'instance:name;Input' value to turn the previewInst on and off. 'triggerActivate, triggerDeactivate': The outputs used when the trigger turns on or off. 'coopVar': The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. 'coopActivate, coopDeactivate': The outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. 'coopOnce': If true, kill the manager after it first activates. 'keys': A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. 'localkeys': The same as above, except values will be changed to use instance-local names. """ marker = resolve_inst(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate','OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection preview_var = res['previewVar', ''].casefold() # Display preview overlays if it's preview mode, and the style var is true # or does not exist if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name( res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst['origin']), Vec.from_str(other['origin']) ) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent( classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin'] ) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['origin'], Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vbsp.VMF.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out(Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out(Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out(Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED
def improve_item(item: Property) -> None: """Improve editoritems formats in various ways. This operates inplace. """ # OccupiedVoxels does not allow specifying 'volume' regions like # EmbeddedVoxel. Implement that. # First for 32^2 cube sections. for voxel_part in item.find_all("Exporting", "OccupiedVoxels", "SurfaceVolume"): if 'subpos1' not in voxel_part or 'subpos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs SubPos1 and SubPos2)!', item['type'], ) continue voxel_part.name = "Voxel" pos_1 = None voxel_subprops = list(voxel_part) voxel_part.clear() for prop in voxel_subprops: if prop.name not in ('subpos', 'subpos1', 'subpos2'): voxel_part.append(prop) continue pos_2 = Vec.from_str(prop.value) if pos_1 is None: pos_1 = pos_2 continue bbox_min, bbox_max = Vec.bbox(pos_1, pos_2) pos_1 = None for pos in Vec.iter_grid(bbox_min, bbox_max): voxel_part.append( Property("Surface", [ Property("Pos", str(pos)), ])) if pos_1 is not None: LOGGER.warning( 'Item {} has only half of SubPos bbox!', item['type'], ) # Full blocks for occu_voxels in item.find_all("Exporting", "OccupiedVoxels"): for voxel_part in list(occu_voxels.find_all("Volume")): del occu_voxels['Volume'] if 'pos1' not in voxel_part or 'pos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs Pos1 and Pos2)!', item['type']) continue voxel_part.name = "Voxel" bbox_min, bbox_max = Vec.bbox( voxel_part.vec('pos1'), voxel_part.vec('pos2'), ) del voxel_part['pos1'] del voxel_part['pos2'] for pos in Vec.iter_grid(bbox_min, bbox_max): new_part = voxel_part.copy() new_part['Pos'] = str(pos) occu_voxels.append(new_part)
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name(res['coopActivate', 'OnChangeToAllTrue']), output_deact=Output.parse_name(res['coopDeactivate', 'OnChangeToAnyFalse']), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin'])) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def group_props_ent( prop_groups: Dict[Optional[tuple], List[StaticProp]], rejected: List[StaticProp], get_model: Callable[[str], Tuple[Optional[QC], Optional[Model]]], bbox_ents: List[Entity], min_cluster: int, ) -> Iterator[List[StaticProp]]: """Given the groups of props, merge props according to the provided ents.""" # (name, skinset) -> list of boxes, constructed as 6 (pos, norm) tuples. combine_sets = defaultdict( list ) # type: Dict[Tuple[str, FrozenSet[str]], List[List[Tuple[Vec, Vec]]]] empty_fs = frozenset('') for ent in bbox_ents: # Either provided name, or unique value. name = ent['name'] or format(int(ent['hammerid']), 'X') origin = Vec.from_str(ent['origin']) skinset = empty_fs mdl_name = ent['prop'] if mdl_name: qc, mdl = get_model(mdl_name) if mdl is not None: skinset = frozenset({ tex.casefold().replace('\\', '/') for tex in mdl.iter_textures([conv_int(ent['skin'])]) }) # Compute 6 planes to use for collision detection. mat = Matrix.from_angle(Angle.from_str(ent['angles'])) mins, maxes = Vec.bbox( Vec.from_str(ent['mins']), Vec.from_str(ent['maxs']), ) # Enlarge slightly to ensure it never has a zero area. # Otherwise the normal could potentially be invalid. mins -= 0.05 maxes += 0.05 # For each direction, compute a position on the plane and # the normal vector. combine_sets[name, skinset].append([( origin + Vec.with_axes(axis, offset) @ mat, Vec.with_axes(axis, norm) @ mat, ) for offset, norm in zip([mins, maxes], (-1, 1)) for axis in ('x', 'y', 'z')]) # Each of these groups cannot be merged with other ones. for group_key, group in prop_groups.items(): if group_key is None: continue # No point merging single/empty groups. group_skinset = group_key[0] if len(group) < min_cluster: rejected.extend(group) group.clear() continue for (name, skinset), boxes in combine_sets.items(): if skinset and skinset != group_skinset: continue # No match found = defaultdict(list) # type: Dict[int, List[StaticProp]] for prop in list(group): for box in boxes: if bsp_collision(prop.origin, box): # Group by this box object's identity. # That's a cheap way to keep each propcombine set # grouped uniquely. found[id(boxes)].append(prop) break for subgroup in found.values(): actual = set(subgroup).intersection(group) if len(actual) >= min_cluster: yield list(actual) for prop in actual: group.remove(prop) # Finally, reject all the ones not in a bbox. for group in prop_groups.values(): rejected.extend(group)
def brush_at_loc( inst: Entity, props: Property, ) -> Tuple[tiling.TileType, bool, Set[tiling.TileType]]: """Common code for posIsSolid and ReadSurfType. This returns the average tiletype, if both colors were found, and a set of all types found. """ origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) # Allow using pos1 instead, to match pos2. pos = props.vec('pos1' if 'pos1' in props else 'pos') pos.z -= 64 # Subtract so origin is the floor-position pos.localise(origin, angles) norm = props.vec('dir', 0, 0, 1).rotate(*angles) if props.bool('gridpos') and norm is not None: for axis in 'xyz': # Don't realign things in the normal's axis - # those are already fine. if norm[axis] == 0: pos[axis] = pos[axis] // 128 * 128 + 64 result_var = props['setVar', ''] # RemoveBrush is the pre-tiling name. should_remove = props.bool('RemoveTile', props.bool('RemoveBrush', False)) tile_types: Set[tiling.TileType] = set() both_colors = False if 'pos2' in props: pos2 = props.vec('pos2') pos2.z -= 64 # Subtract so origin is the floor-position pos2.localise(origin, angles) bbox_min, bbox_max = Vec.bbox(pos, pos2) white_count = black_count = 0 for pos in Vec.iter_grid(bbox_min, bbox_max, 32): try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: continue tile_type = tiledef[u, v] tile_types.add(tile_type) if should_remove: tiledef[u, v] = tiling.TileType.VOID if tile_type.is_tile: if tile_type.color is tiling.Portalable.WHITE: white_count += 1 else: black_count += 1 both_colors = white_count > 0 and black_count > 0 if white_count == black_count == 0: tile_type = tiling.TileType.VOID tile_types.add(tiling.TileType.VOID) elif white_count > black_count: tile_type = tiling.TileType.WHITE else: tile_type = tiling.TileType.BLACK else: # Single tile. try: tiledef, u, v = tiling.find_tile(pos, norm) except KeyError: tile_type = tiling.TileType.VOID else: tile_type = tiledef[u, v] if should_remove: tiledef[u, v] = tiling.TileType.VOID tile_types.add(tile_type) if result_var: if tile_type.is_tile: # Don't distinguish between 4x4, goo sides inst.fixup[result_var] = tile_type.color.value elif tile_type is tiling.TileType.VOID: inst.fixup[result_var] = 'none' else: inst.fixup[result_var] = tile_type.name.casefold() return tile_type, both_colors, tile_types
def export(self, vmf: VMF, *, wall_conf: AntType, floor_conf: AntType) -> None: """Add the antlines into the map.""" # First, do some optimisation. If corners aren't defined, try and # optimise those antlines out by merging the straight segment # before/after it into the corners. collapse_line: list[Segment | None] if not wall_conf.tex_corner or not floor_conf.tex_corner: collapse_line = list(self.line) for i, seg in enumerate(collapse_line): if seg is None or seg.type is not SegType.STRAIGHT: continue if (floor_conf if seg.on_floor else wall_conf).tex_corner: continue for corner_ind in [i-1, i+1]: if i == -1: continue try: corner = collapse_line[corner_ind] except IndexError: # Each end of the list. continue if ( corner is not None and corner.type is SegType.CORNER and corner.normal == seg.normal ): corner_pos = corner.start if (seg.start - corner_pos).mag_sq() == 8 ** 2: # The line segment is at the border between them, # the corner is at the center. So move double the # distance towards the corner, so it reaches to the # other side of the corner and replaces it. seg.start += 2 * (corner_pos - seg.start) # Remove corner by setting to None, so we aren't # resizing the list constantly. collapse_line[corner_ind] = None # Now merge together the tiledefs. seg.tiles.update(corner.tiles) elif (seg.end - corner_pos).mag_sq() == 8 ** 2: seg.end += 2 * (corner_pos - seg.end) collapse_line[corner_ind] = None seg.tiles.update(corner.tiles) self.line[:] = [seg for seg in collapse_line if seg is not None] LOGGER.info('Collapsed {} antline corners', collapse_line.count(None)) for seg in self.line: conf = floor_conf if seg.on_floor else wall_conf # Check tiledefs in the voxels, and assign just in case. # antline corner items don't have them defined, and some embedfaces don't work # properly. But we keep any segments actually defined also. mins, maxs = Vec.bbox(seg.start, seg.end) norm_axis = seg.normal.axis() u_axis, v_axis = Vec.INV_AXIS[norm_axis] for pos in Vec.iter_line(mins, maxs, 128): pos[u_axis] = pos[u_axis] // 128 * 128 + 64 pos[v_axis] = pos[v_axis] // 128 * 128 + 64 pos -= 64 * seg.normal try: tile = tiling.TILES[pos.as_tuple(), seg.normal.as_tuple()] except KeyError: pass else: seg.tiles.add(tile) rng = rand.seed(b'antline', seg.start, seg.end) if seg.type is SegType.CORNER: mat: AntTex if rng.randrange(100) < conf.broken_chance: mat = rng.choice(conf.broken_corner or conf.broken_straight) else: mat = rng.choice(conf.tex_corner or conf.tex_straight) # Because we can, apply a random rotation to mix up the texture. orient = Matrix.from_angle(seg.normal.to_angle( rng.choice((0.0, 90.0, 180.0, 270.0)) )) self._make_overlay( vmf, seg, seg.start, 16.0 * orient.left(), 16.0 * orient.up(), mat, ) else: # Straight # TODO: Break up these segments. for a, b, is_broken in seg.broken_iter(conf.broken_chance): if is_broken: mat = rng.choice(conf.broken_straight) else: mat = rng.choice(conf.tex_straight) self._make_straight( vmf, seg, a, b, mat, )