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 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 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 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 load_embeddedvoxel(item: Item, ent: Entity) -> None: """Parse embed definitions contained in the VMF.""" bbox_min, bbox_max = ent.get_bbox() bbox_min = round(bbox_min, 0) bbox_max = round(bbox_max, 0) if bbox_min % 128 != (64.0, 64.0, 64.0) or bbox_max % 128 != (64.0, 64.0, 64.0): LOGGER.warning( 'Embedded voxel definition ({}) - ({}) is not aligned to grid!', bbox_min, bbox_max, ) return item.embed_voxels.update( map( Coord.from_vec, Vec.iter_grid( (bbox_min + (64, 64, 64)) / 128, (bbox_max - (64, 64, 64)) / 128, )))
# Position -> entity # We merge ones within 3 blocks of our item. CHECKPOINT_TRIG = {} # type: Dict[Tuple[float, float, float], Entity] # Approximately a 3-distance from # the center. # x # xxx # xx xx # xxx # x CHECKPOINT_NEIGHBOURS = list( Vec.iter_grid( Vec(-128, -128, 0), Vec(128, 128, 0), stride=128, )) CHECKPOINT_NEIGHBOURS.extend([ Vec(-256, 0, 0), Vec(256, 0, 0), Vec(0, -256, 0), Vec(0, 256, 0), ]) # Don't include ourself.. CHECKPOINT_NEIGHBOURS.remove(Vec(0, 0, 0)) @make_result('CheckpointTrigger') def res_checkpoint_trigger(inst: Entity, res: Property): """Generate a trigger underneath coop checkpoint items
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 load_occupiedvoxel(item: Item, ent: Entity) -> None: """Parse voxel collisions contained in the VMF.""" bbox_min, bbox_max = ent.get_bbox() bbox_min = round(bbox_min, 0) bbox_max = round(bbox_max, 0) coll_type = parse_colltype(ent['coll_type']) if ent['coll_against']: coll_against = parse_colltype(ent['coll_against']) else: coll_against = None if bbox_min % 128 == (64.0, 64.0, 64.0) and bbox_max % 128 == (64.0, 64.0, 64.0): # Full voxels. for voxel in Vec.iter_grid( (bbox_min + (64, 64, 64)) / 128, (bbox_max - (64, 64, 64)) / 128, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec(voxel), )) return elif bbox_min % 32 == (0.0, 0.0, 0.0) and bbox_max % 32 == (0.0, 0.0, 0.0): # Subvoxel sections. for subvoxel in Vec.iter_grid( bbox_min / 32, (bbox_max - (32.0, 32.0, 32.0)) / 32, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec((subvoxel + (2, 2, 2)) // 4), Coord.from_vec((subvoxel - (2, 2, 2)) % 4), )) return # else, is this a surface definition? size = round(bbox_max - bbox_min, 0) for axis in ['x', 'y', 'z']: if size[axis] < 8: u, v = Vec.INV_AXIS[axis] # Figure out if we're aligned to the min or max side of the voxel. # Compute the normal, then flatten to zero thick. if bbox_min[axis] % 32 == 0: norm = +1 plane_dist = bbox_max[axis] = bbox_min[axis] elif bbox_max[axis] % 32 == 0: norm = -1 plane_dist = bbox_min[axis] = bbox_max[axis] else: # Both faces aren't aligned to the grid, skip to error. break if bbox_min[u] % 128 == bbox_min[v] % 128 == bbox_max[ v] % 128 == bbox_max[v] % 128 == 64.0: # Full voxel surface definitions. for voxel in Vec.iter_grid( Vec.with_axes(u, bbox_min[u] + 64, v, bbox_min[v] + 64, axis, plane_dist + 64 * norm) / 128, Vec.with_axes(u, bbox_max[u] - 64, v, bbox_max[v] - 64, axis, plane_dist + 64 * norm) / 128, ): item.occupy_voxels.add( OccupiedVoxel( coll_type, coll_against, Coord.from_vec(voxel), normal=Coord.from_vec(Vec.with_axes(axis, norm)), )) return elif bbox_min[u] % 32 == bbox_min[v] % 32 == bbox_max[ v] % 32 == bbox_max[v] % 32 == 0.0: # Subvoxel surface definitions. return else: # Not aligned to grid, skip to error. break LOGGER.warning( 'Unknown occupied voxel definition: ({}) - ({}), type="{}", against="{}"', bbox_min, bbox_max, ent['coll_type'], ent['coll_against'], )
) # Position -> entity # We merge ones within 3 blocks of our item. CHECKPOINT_TRIG = {} # type: Dict[Tuple[float, float, float], Entity] # Approximately a 3-distance from # the center. # x # xxx # xx xx # xxx # x CHECKPOINT_NEIGHBOURS = list(Vec.iter_grid( Vec(-128, -128, 0), Vec(128, 128, 0), stride=128, )) CHECKPOINT_NEIGHBOURS.extend([ Vec(-256, 0, 0), Vec(256, 0, 0), Vec(0, -256, 0), Vec(0, 256, 0), ]) # Don't include ourself.. CHECKPOINT_NEIGHBOURS.remove(Vec(0, 0, 0)) @make_result('CheckpointTrigger') def res_checkpoint_trigger(inst: Entity, res: Property): """Generate a trigger underneath coop checkpoint items
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 setup(self, global_seed: str, tiles: List['TileDef']): """Build the list of clump locations.""" assert self.portal is not None assert self.orient is not None # Convert the generator key to a generator-specific seed. # That ensures different surfaces don't end up reusing the same # texture indexes. self.gen_seed = int.from_bytes( self.category.name.encode() + self.portal.name.encode() + self.orient.name.encode(), 'big', ) LOGGER.info('Generating texture clumps...') clump_length: int = self.options['clump_length'] clump_width: int = self.options['clump_width'] # The tiles currently present in the map. orient_z = self.orient.z remaining_tiles: Set[Tuple[float, float, float]] = { (tile.pos + 64 * tile.normal // 128 * 128).as_tuple() for tile in tiles if tile.normal.z == orient_z } # A global RNG for picking clump positions. clump_rand = random.Random(global_seed + '_clumping') pos_min = Vec() pos_max = Vec() # For debugging, generate skip brushes with the shape of the clumps. debug_visgroup: Optional[VisGroup] if self.options['clump_debug']: import vbsp debug_visgroup = vbsp.VMF.create_visgroup( f'{self.category.name}_{self.orient.name}_{self.portal.name}') else: debug_visgroup = None while remaining_tiles: # Pick from a random tile. tile_pos = next( itertools.islice( remaining_tiles, clump_rand.randrange(0, len(remaining_tiles)), len(remaining_tiles), )) remaining_tiles.remove(tile_pos) pos = Vec(tile_pos) # Clumps are long strips mainly extended in one direction # In the other directions extend by 'width'. It can point any axis. direction = clump_rand.choice('xyz') for axis in 'xyz': if axis == direction: dist = clump_length else: dist = clump_width pos_min[axis] = pos[axis] - clump_rand.randint(0, dist) * 128 pos_max[axis] = pos[axis] + clump_rand.randint(0, dist) * 128 remaining_tiles.difference_update( map(Vec.as_tuple, Vec.iter_grid(pos_min, pos_max, 128))) self._clump_locs.append( Clump( pos_min.x, pos_min.y, pos_min.z, pos_max.x, pos_max.y, pos_max.z, # We use this to reseed an RNG, giving us the same textures # each time for the same clump. clump_rand.getrandbits(32), )) if debug_visgroup is not None: # noinspection PyUnboundLocalVariable debug_brush: Solid = vbsp.VMF.make_prism( pos_min - 64, pos_max + 64, 'tools/toolsskip', ).solid debug_brush.visgroup_ids.add(debug_visgroup.id) debug_brush.vis_shown = False vbsp.VMF.add_brush(debug_brush) LOGGER.info( '{}.{}.{}: {} Clumps for {} tiles', self.category.name, self.orient.name, self.portal.name, len(self._clump_locs), len(tiles), )
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