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_transfer_bullseye(inst: Entity, props: Property): """Transfer catapult targets and placement helpers from one tile to another.""" start_pos = conditions.resolve_offset(inst, props['start_pos', '']) end_pos = conditions.resolve_offset(inst, props['end_pos', '']) start_norm = props.vec('start_norm', 0, 0, 1).rotate_by_str(inst['angles']) end_norm = props.vec('end_norm', 0, 0, 1).rotate_by_str(inst['angles']) try: start_tile = tiling.TILES[ (start_pos - 64 * start_norm).as_tuple(), start_norm.as_tuple() ] except KeyError: LOGGER.warning('"{}": Cannot find tile to transfer from at {}, {}!'.format( inst['targetname'], start_pos, start_norm )) return end_tile = tiling.TileDef.ensure( end_pos - 64 * end_norm, end_norm, ) # Now transfer the stuff. if start_tile.has_oriented_portal_helper: # We need to rotate this. orient = start_tile.portal_helper_orient.copy() # If it's directly opposite, just mirror - we have no clue what the # intent is. if Vec.dot(start_norm, end_norm) != -1.0: # Use the dict to compute the rotation to apply. orient.rotate(*NORM_ROTATIONS[ start_norm.as_tuple(), end_norm.as_tuple() ]) end_tile.add_portal_helper(orient) elif start_tile.has_portal_helper: # Non-oriented, don't orient. end_tile.add_portal_helper() start_tile.remove_portal_helper(all=True) if start_tile.bullseye_count: end_tile.bullseye_count = start_tile.bullseye_count start_tile.bullseye_count = 0 # Then transfer the targets across. for plate in faithplate.PLATES.values(): if getattr(plate, 'target', None) is start_tile: plate.target = end_tile
def res_add_placement_helper(inst: Entity, res: Property): """Add a placement helper to a specific tile. `Offset` and `normal` specify the position and direction out of the surface the helper should be added to. If `upDir` is specified, this is the direction of the top of the portal. """ angles = Vec.from_str(inst['angles']) pos = conditions.resolve_offset(inst, res['offset', '0 0 0'], zoff=-64) normal = res.vec('normal', 0, 0, 1).rotate(*angles) up_dir: Optional[Vec] try: up_dir = Vec.from_str(res['upDir']).rotate(*angles) except LookupError: up_dir = None try: tile = tiling.TILES[(pos - 64 * normal).as_tuple(), normal.as_tuple()] except KeyError: LOGGER.warning('No tile at {} @ {}', pos, normal) return tile.add_portal_helper(up_dir)
def res_translate_inst(inst: Entity, res: Property): """Translate the instance locally by the given amount. The special values `<piston>`, `<piston_bottom>` and `<piston_top>` can be used to offset it based on the starting position, bottom or top position of a piston platform. """ inst['origin'] = resolve_offset(inst, res.value)
def flag_goo_at_loc(inst: Entity, flag: Property): """Check to see if a given location is submerged in goo. `0 0 0` is the origin of the instance, values are in `128` increments. """ offset = resolve_offset(inst, flag.value, scale=128) block = brushLoc.POS['world':offset] return block.is_goo
def flag_goo_at_loc(inst: Entity, flag: Property): """Check to see if a given location is submerged in goo. `0 0 0` is the origin of the instance, values are in `128` increments. """ offset = resolve_offset(inst, flag.value, scale=128) block = brushLoc.POS['world': offset] return block.is_goo
def res_set_block(inst: Entity, res: Property): """Set a block to the given value. This should be used only if you know what is in the position. The offset is in block increments, with `0 0 0` equal to the mounting surface. """ 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: raise ValueError("Can't use compound block types ({})!".format(res['type'])) pos = resolve_offset(inst, res['offset', '0 0 0'], scale=128, zoff=-128) brushLoc.POS['world': pos] = new_val
def res_set_block(inst: Entity, res: Property): """Set a block to the given value. This should be used only if you know what is in the position. The offset is in block increments, with `0 0 0` equal to the mounting surface. """ 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: raise ValueError("Can't use compound block types ({})!".format( res['type'])) pos = resolve_offset(inst, res['offset', '0 0 0'], scale=128, zoff=-128) brushLoc.POS['world':pos] = new_val
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. If a single value, this sets only the filename. Values: `file`: The filename. `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. `copy_fixup`: If true, all the $replace values from the original instance will be copied over. `move_outputs`: If true, outputs will be moved to this instance. `offset`: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. `angles`: If set, overrides the base instance angles. This does not affect the offset property. `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [Property('File', res.value)]) angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" normal = props.vec('normal', 0, 0, 1).rotate_by_str(inst['angles']) 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).rotate_by_str(inst['angles']) + 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).rotate_by_str(inst['angles']) + 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_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. If a single value, this sets only the filename. Values: `file`: The filename. `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. `copy_fixup`: If true, all the $replace values from the original instance will be copied over. `move_outputs`: If true, outputs will be moved to this instance. `offset`: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. `angles`: If set, overrides the base instance angles. This does not affect the offset property. `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [ Property('File', res.value) ]) angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst