def res_set_tile(inst: Entity, res: Property) -> None: """Set 4x4 parts of a tile to the given values. `Offset` defines the position of the upper-left tile in the grid. Each `Tile` section defines a row of the positions to edit like so: "Tile" "bbbb" "Tile" "b..b" "Tile" "b..b" "Tile" "bbbb" If `Force` is true, the specified tiles will override any existing ones and create the tile if necessary. Otherwise they will be merged in - white/black tiles will not replace tiles set to nodraw or void for example. `chance`, if specified allows producing irregular tiles by randomly not changing the tile. If you need less regular placement (other orientation, precise positions) use a bee2_template_tilesetter in a template. Allowed tile characters: - `W`: White tile. - `w`: White 4x4 only tile. - `B`: Black tile. - `b`: Black 4x4 only tile. - `g`: The side/bottom of goo pits. - `n`: Nodraw surface. - `i`: Invert the tile surface, if black/white. - `1`: Convert to a 1x1 only tile, if a black/white tile. - `4`: Convert to a 4x4 only tile, if a black/white tile. - `.`: Void (remove the tile in this position). - `_` or ` `: Placeholder (don't modify this space). - `x`: Cutout Tile (Broken) - `o`: Cutout Tile (Partial) """ origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) offset = (res.vec('offset', -48, 48) - (0, 0, 64)) @ orient + origin norm = round(orient.up(), 6) force_tile = res.bool('force') tiles: list[str] = [ row.value for row in res if row.name in ('tile', 'tiles') ] if not tiles: raise ValueError('No "tile" parameters in SetTile!') chance = srctools.conv_float(res['chance', '100'].rstrip('%'), 100.0) if chance < 100.0: rng = rand.seed(b'tile', inst, res['seed', '']) else: rng = None for y, row in enumerate(tiles): for x, val in enumerate(row): if val in '_ ': continue if rng is not None and rng.uniform(0, 100) > chance: continue pos = Vec(32 * x, -32 * y, 0) @ orient + offset if val == '4': size = tiling.TileSize.TILE_4x4 elif val == '1': size = tiling.TileSize.TILE_1x1 elif val == 'i': size = None else: try: new_tile = tiling.TILETYPE_FROM_CHAR[val] except KeyError: LOGGER.warning('Unknown tiletype "{}"!', val) else: tiling.edit_quarter_tile(pos, norm, new_tile, force_tile) continue # Edit the existing tile. try: tile, u, v = tiling.find_tile(pos, norm, force_tile) except KeyError: LOGGER.warning( 'Expected tile, but none found: {}, {}', pos, norm, ) continue if size is None: # Invert the tile. tile[u, v] = tile[u, v].inverted continue # Unless forcing is enabled don't alter the size of GOO_SIDE. if tile[u, v].is_tile and tile[u, v] is not tiling.TileType.GOO_SIDE: tile[u, v] = tiling.TileType.with_color_and_size( size, tile[u, v].color) elif force_tile: # If forcing, make it black. Otherwise no need to change. tile[u, v] = tiling.TileType.with_color_and_size( size, tiling.Portalable.BLACK)
def res_signage(vmf: VMF, inst: Entity, res: Property): """Implement the Signage item.""" sign: Optional[Sign] try: sign = (CONN_SIGNAGES if res.bool('connection') else SIGNAGES)[inst.fixup[consts.FixupVars.TIM_DELAY]] except KeyError: # Blank sign sign = None has_arrow = inst.fixup.bool(consts.FixupVars.ST_ENABLED) make_4x4 = res.bool('set4x4tile') sign_prim: Optional[Sign] sign_sec: Optional[Sign] if has_arrow: sign_prim = sign sign_sec = SIGNAGES['arrow'] elif sign is not None: sign_prim = sign.primary or sign sign_sec = sign.secondary or None else: # Neither sign or arrow, delete this. inst.remove() return origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal = -orient.up() forward = -orient.forward() prim_pos = Vec(0, -16, -64) @ orient + origin sec_pos = Vec(0, +16, -64) @ orient + origin template_id = res['template_id', ''] if inst.fixup.bool(consts.FixupVars.ST_REVERSED): # Flip around. forward = -forward prim_visgroup = 'secondary' sec_visgroup = 'primary' prim_pos, sec_pos = sec_pos, prim_pos else: prim_visgroup = 'primary' sec_visgroup = 'secondary' if sign_prim and sign_sec: inst['file'] = res['large_clip', ''] inst['origin'] = (prim_pos + sec_pos) / 2 else: inst['file'] = res['small_clip', ''] inst['origin'] = prim_pos if sign_prim else sec_pos brush_faces: List[Side] = [] tiledef: Optional[tiling.TileDef] = None if template_id: if sign_prim and sign_sec: visgroup = [prim_visgroup, sec_visgroup] elif sign_prim: visgroup = [prim_visgroup] else: visgroup = [sec_visgroup] template = template_brush.import_template( vmf, template_id, origin, orient, force_type=template_brush.TEMP_TYPES.detail, additional_visgroups=visgroup, ) for face in template.detail.sides(): if face.normal() == normal: brush_faces.append(face) else: # Direct on the surface. # Find the grid pos first. grid_pos = (origin // 128) * 128 + 64 try: tiledef = tiling.TILES[(grid_pos + 128 * normal).as_tuple(), (-normal).as_tuple()] except KeyError: LOGGER.warning( "Can't place signage at ({}) in ({}) direction!", origin, normal, exc_info=True, ) return if sign_prim is not None: over = place_sign( vmf, brush_faces, sign_prim, prim_pos, normal, forward, rotate=True, ) if tiledef is not None: tiledef.bind_overlay(over) if make_4x4: try: tile, u, v = tiling.find_tile(prim_pos, -normal) except KeyError: pass else: tile[u, v] = tile[u, v].as_4x4 if sign_sec is not None: if has_arrow and res.bool('arrowDown'): # Arrow texture points down, need to flip it. forward = -forward over = place_sign( vmf, brush_faces, sign_sec, sec_pos, normal, forward, rotate=not has_arrow, ) if tiledef is not None: tiledef.bind_overlay(over) if make_4x4: try: tile, u, v = tiling.find_tile(sec_pos, -normal) except KeyError: pass else: tile[u, v] = tile[u, v].as_4x4
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: set[tuple[int, int]] | None 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 = inst.fixup.substitute(props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( inst.fixup.substitute(props['nodraw'], allow_invert=True)) if 'seal' in props: panel.seal = srctools.conv_bool( inst.fixup.substitute(props['seal'], allow_invert=True)) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( inst.fixup.substitute(props['move_bullseye'], allow_invert=True)) 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[Entity | None] = {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']), Angle.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 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 = Angle.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: Vec = round(props.vec('dir', 0, 0, 1) @ angles, 6) 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(round(pos, 6), round(pos2, 6)) 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