def _make_straight( self, vmf: VMF, segment: Segment, start: Vec, end: Vec, mat: AntTex, ) -> None: """Construct a straight antline between two points. The two points will be the end of the antlines. """ offset = start - end forward = offset.norm() side = Vec.cross(segment.normal, forward).norm() length = offset.mag() self._make_overlay( vmf, segment, (start + end) / 2, length * forward, 16 * side, mat, )
def place_sign( vmf: VMF, faces: Iterable[Side], sign: Sign, pos: Vec, normal: Vec, forward: Vec, rotate: bool = True, ) -> None: """Place the sign into the map.""" if rotate and normal.z == 0: # On the wall, point upward. forward = Vec(0, 0, 1) texture = sign.overlay if texture.startswith('<') and texture.endswith('>'): texture = vbsp.get_tex(texture[1:-1]) width, height = SIZES[sign.type] over = make_overlay( vmf, -normal, pos, uax=-width * Vec.cross(normal, forward).norm(), vax=-height * forward, material=texture, surfaces=faces, ) over['startu'] = '1' over['endu'] = '0' vbsp.IGNORED_OVERLAYS.add(over)
def place_sign( vmf: VMF, faces: Iterable[Side], sign: Sign, pos: Vec, normal: Vec, forward: Vec, rotate: bool = True, ) -> Entity: """Place the sign into the map.""" if rotate and abs(normal.z) < 0.1: # On the wall, point upward. forward = Vec(0, 0, 1) texture = sign.overlay if texture.startswith('<') and texture.endswith('>'): gen, tex_name = texturing.parse_name(texture[1:-1]) texture = gen.get(pos, tex_name) width, height = SIZES[sign.type] over = make_overlay( vmf, -normal, pos, uax=-width * Vec.cross(normal, forward).norm(), vax=-height * forward, material=texture, surfaces=faces, ) vbsp.IGNORED_OVERLAYS.add(over) over['startu'] = '1' over['endu'] = '0' return over
def compute_orients(nodes: Iterable[Node]) -> None: """Compute the appropriate orientation for each node.""" # This is based on the info at: # https://janakiev.com/blog/framing-parametric-curves/ tangents: Dict[Node, Vec] = {} all_nodes: Set[Node] = set() for node in nodes: if node.prev is node.next is None: continue node_prev = node.prev if node.prev is not None else node node_next = node.next if node.next is not None else node tangents[node] = (node_next.pos - node_prev.pos).norm() all_nodes.add(node) while all_nodes: node1 = all_nodes.pop() node1 = node1.find_start() tanj1 = tangents[node1] # Start with an arbitrary roll for the first orientation. node1.orient = Matrix.from_angle(tanj1.to_angle()) while node1.next is not None: node2 = node1.next all_nodes.discard(node2) tanj1 = tangents[node1] tanj2 = tangents[node2] b = Vec.cross(tanj1, tanj2) if b.mag_sq() < 0.001: node2.orient = node1.orient.copy() else: b = b.norm() phi = math.acos(Vec.dot(tanj1, tanj2)) up = node1.orient.up() @ Matrix.axis_angle( b, math.degrees(phi)) node2.orient = Matrix.from_basis(x=tanj2, z=up) node1 = node2
def join_markers(vmf: VMF, mark_a: Marker, mark_b: Marker, is_start: bool = False) -> None: """Join two marker ents together with corners.""" origin_a = Vec.from_str(mark_a.ent['origin']) origin_b = Vec.from_str(mark_b.ent['origin']) norm_a = Vec(-1, 0, 0).rotate_by_str(mark_a.ent['angles']) norm_b = Vec(-1, 0, 0).rotate_by_str(mark_b.ent['angles']) config = mark_a.conf if norm_a == norm_b: # Either straight-line, or s-bend. dist = (origin_a - origin_b).mag() if origin_a + (norm_a * dist) == origin_b: make_straight( vmf, origin_a, norm_a, dist, config, is_start, ) # else: S-bend, we don't do the geometry for this.. return if norm_a == -norm_b: # U-shape bend.. make_ubend( vmf, origin_a, origin_b, norm_a, config, max_size=mark_a.size, ) return # Lastly try a regular curve. Check they are on the same plane. side_dir = Vec.cross(norm_a, norm_b) side_off_a = side_dir.dot(origin_a) side_off_b = side_dir.dot(origin_b) if side_off_a == side_off_b: make_bend( vmf, origin_a, origin_b, norm_a, norm_b, config, max_size=mark_a.size, )
def test_gen_check(py_c_vec) -> None: """Do an exhaustive check on all rotation math using data from the engine.""" Vec, Angle, Matrix, parse_vec_str = py_c_vec X = Vec(x=1) Y = Vec(y=1) Z = Vec(z=1) with open('rotation.txt') as f: for line_num, line in enumerate(f, start=1): if not line.startswith('|'): # Skip other junk in the log. continue (pit, yaw, roll, for_x, for_y, for_z, right_x, right_y, right_z, up_x, up_y, up_z) = map(float, line[1:].split()) # The engine actually gave us a right vector, so we need to flip that. left_x, left_y, left_z = -right_x, -right_y, -right_z mat = Matrix.from_angle(Angle(pit, yaw, roll)) # Then check rotating vectors works correctly. assert_vec(X @ mat, for_x, for_y, for_z) assert_vec(Y @ mat, left_x, left_y, left_z) assert_vec(Z @ mat, up_x, up_y, up_z) # Check the direct matrix values. assert_vec(mat.forward(), for_x, for_y, for_z) assert_vec(mat.left(), left_x, left_y, left_z) assert_vec(mat.up(), up_x, up_y, up_z) assert math.isclose(for_x, mat[0, 0], abs_tol=EPSILON) assert math.isclose(for_y, mat[0, 1], abs_tol=EPSILON) assert math.isclose(for_z, mat[0, 2], abs_tol=EPSILON) assert math.isclose(left_x, mat[1, 0], abs_tol=EPSILON) assert math.isclose(left_y, mat[1, 1], abs_tol=EPSILON) assert math.isclose(left_z, mat[1, 2], abs_tol=EPSILON) assert math.isclose(up_x, mat[2, 0], abs_tol=EPSILON) assert math.isclose(up_y, mat[2, 1], abs_tol=EPSILON) assert math.isclose(up_z, mat[2, 2], abs_tol=EPSILON) # Also test Matrix.from_basis(). x = Vec(for_x, for_y, for_z) y = Vec(left_x, left_y, left_z) z = Vec(up_x, up_y, up_z) assert_rot(Matrix.from_basis(x=x, y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, y=y), mat) assert_rot(Matrix.from_basis(y=y, z=z), mat) assert_rot(Matrix.from_basis(x=x, z=z), mat) # Angle.from_basis() == Matrix.from_basis().to_angle(). assert_ang(Angle.from_basis(x=x, y=y, z=z), *Matrix.from_basis(x=x, y=y, z=z).to_angle()) assert_ang(Angle.from_basis(x=x, y=y), *Matrix.from_basis(x=x, y=y).to_angle()) assert_ang(Angle.from_basis(y=y, z=z), *Matrix.from_basis(y=y, z=z).to_angle()) assert_ang(Angle.from_basis(x=x, z=z), *Matrix.from_basis(x=x, z=z).to_angle()) # And Vec.cross(). assert_vec(Vec.cross(x, y), up_x, up_y, up_z, tol=1e-5) assert_vec(Vec.cross(y, z), for_x, for_y, for_z, tol=1e-5) assert_vec(Vec.cross(x, z), -left_x, -left_y, -left_z, tol=1e-5) assert_vec(Vec.cross(y, x), -up_x, -up_y, -up_z, tol=1e-5) assert_vec(Vec.cross(z, y), -for_x, -for_y, -for_z, tol=1e-5) assert_vec(Vec.cross(z, x), left_x, left_y, left_z, tol=1e-5) assert_vec(Vec.cross(x, x), 0, 0, 0) assert_vec(Vec.cross(y, y), 0, 0, 0) assert_vec(Vec.cross(z, z), 0, 0, 0)
def join_markers(vmf: VMF, mark_a: Marker, mark_b: Marker, is_start: bool=False) -> None: """Join two marker ents together with corners.""" origin_a = Vec.from_str(mark_a.ent['origin']) origin_b = Vec.from_str(mark_b.ent['origin']) norm_a = mark_a.orient.forward() norm_b = mark_b.orient.forward() config = mark_a.conf LOGGER.debug( 'Connect markers: {} @ {} -> {} @ {}, dot={}\n{}', origin_a, norm_a, origin_b, norm_b, Vec.dot(norm_a, norm_b), config, ) if norm_a == norm_b: # Either straight-line, or s-bend. dist = round((origin_a - origin_b).mag()) if origin_a + (norm_a * dist) == origin_b: make_straight( vmf, origin_a, norm_a, dist, config, is_start, ) # else: S-bend, we don't do the geometry for this.. return if norm_a == -norm_b: # U-shape bend.. make_ubend( vmf, origin_a, origin_b, norm_a, config, max_size=mark_a.size, ) return # Lastly try a regular curve. Check they are on the same plane. side_dir = Vec.cross(norm_a, norm_b) side_off_a = side_dir.dot(origin_a) side_off_b = side_dir.dot(origin_b) if abs(side_off_a - side_off_b) < 1e-6: make_bend( vmf, origin_a, origin_b, norm_a, norm_b, config, max_size=mark_a.size, ) else: LOGGER.warning( 'Cannot connect markers: {} @ {} -> {} @ {}\n ' 'Sides: {:.12f} {:.12f}\n{}', origin_a, norm_a, origin_b, norm_b, side_off_a, side_off_b, config, )
def res_antlaser(vmf: VMF, res: Property) -> object: """The condition to generate AntLasers and Antline Corners. This is executed once to modify all instances. """ conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>', silent=True) conf_inst_laser = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', or_blank=True) glow_conf = res.find_key('GlowKeys', or_blank=True) cable_conf = res.find_key('CableKeys', or_blank=True) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes: dict[str, Node] = {} for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() name = inst['targetname'] if filename in conf_inst_laser: node_type = NodeType.LASER elif filename in conf_inst_corner: node_type = NodeType.CORNER else: continue try: # Remove the item - it's no longer going to exist after # we're done. item = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None pos = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) if node_type is NodeType.CORNER: timer_delay = item.inst.fixup.int('$timer_delay') # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not # being selectable issue. pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos nodes[name] = Node(node_type, inst, item, pos, orient) if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups: list[Group] = [] while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start, start.type) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node.is_grouped = True for conn in list(node.item.outputs): neighbour = conn.to_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.from_item = group.item has_output = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neigh_node})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.item.transfer_antlines(group.item) else: node.item.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.item.inputs): neighbour = conn.from_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.to_item = group.item node.had_input = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neigh_node, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random item name to use for our group. base_name = group.nodes[0].item.name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] if group.type is NodeType.LASER: for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) if group.type is NodeType.LASER and conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) # Node -> index for targetnames. indexes: dict[Node, int] = {} # For antline corners, the antline segments. segments: list[antlines.Segment] = [] # frozenset[Node] unpacking isn't clear. node_a: Node node_b: Node if group.type is NodeType.CORNER: for node_a, node_b in group.links: # Place a straight antline between each connected node. # If on the same plane, we only need one. If not, we need to # do one for each plane it's in. offset = node_b.pos - node_a.pos up_a = node_a.orient.up() up_b = node_b.orient.up() plane_a = Vec.dot(node_a.pos, up_a) plane_b = Vec.dot(node_b.pos, up_b) if Vec.dot(up_a, up_b) > 0.9: if abs(plane_a - plane_b) > 1e-6: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are on different planes', node_a.item.name, node_b.item.name, ) continue u = node_a.orient.left() v = node_a.orient.forward() # Which are we aligned to? if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot( offset, v)) < 1e-6: forward = offset.norm() group.add_ant_straight( up_a, node_a.pos + 8.0 * forward, node_b.pos - 8.0 * forward, ) else: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are not directly aligned', node_a.item.name, node_b.item.name, ) else: # We expect them be aligned to each other. side = Vec.cross(up_a, up_b) if abs(Vec.dot(side, offset)) < 1e-6: mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a if mid1 != mid2: LOGGER.warning( 'Midpoint mismatch: {} != {} for "{}" - "{}"', mid1, mid2, node_a.item.name, node_b.item.name, ) group.add_ant_straight( up_a, node_a.pos + 8.0 * (mid1 - node_a.pos).norm(), mid1, ) group.add_ant_straight( up_b, node_b.pos + 8.0 * (mid2 - node_b.pos).norm(), mid2, ) # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points: dict[Node, Union[Entity, str]] = {} for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.item.name = base_name if group.type is NodeType.CORNER: node.inst.remove() # Figure out whether we want a corner at this point, or # just a regular dot. If a non-node input was provided it's # always a corner. Otherwise it's one if there's an L, T or X # junction. use_corner = True norm = node.orient.up().as_tuple() if not node.had_input: neighbors = [ mag * direction for direction in [ node.orient.forward(), node.orient.left(), ] for mag in [-8.0, 8.0] if ((node.pos + mag * direction).as_tuple(), norm) in group.ant_seg ] if len(neighbors) == 2: [off1, off2] = neighbors if Vec.dot(off1, off2) < -0.99: # ---o---, merge together. The endpoints we want # are the other ends of the two segments. group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + off1), group.rem_ant_straight(norm, node.pos + off2), ) use_corner = False elif len(neighbors) == 1: # o-----, merge. [offset] = neighbors group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + offset), node.pos - offset, ) use_corner = False if use_corner: segments.append( antlines.Segment( antlines.SegType.CORNER, round(node.orient.up(), 3), Vec(node.pos), Vec(node.pos), )) elif group.type is NodeType.LASER: sprite_pos = node.pos + conf_glow_height @ node.orient if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value( node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = node.pos + conf_las_start @ node.orient beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value( node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start segments += set(group.ant_seg.values()) if group.type is NodeType.CORNER and segments: group.item.antlines.add( antlines.Antline(group.item.name + '_antline', segments)) if group.type is NodeType.LASER and beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.pos beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags if group.type is NodeType.LASER and cable_conf: build_cables( vmf, group, cable_points, base_name, beam_conf, conf_rope_off, ) return conditions.RES_EXHAUSTED