def get_temp(key): try: temp_id = block['temp_' + key] except LookupError: return None try: return template_brush.get_template(temp_id) except template_brush.InvalidTemplateName: LOGGER.warning('Invalid template "{}" for vactube group {}!', temp_id, group) return None
def get_temp(key: str) -> tuple[template_brush.Template | None, Iterable[str]]: """Read the template, handling errors.""" try: temp_name = block['temp_' + key] except LookupError: return None, () temp_id, visgroups = template_brush.parse_temp_name(temp_name) try: return template_brush.get_template(temp_id), visgroups except template_brush.InvalidTemplateName: LOGGER.warning('Invalid template "{}" for vactube group {}!', temp_name, group) return None, ()
def res_set_faith_setup(res: Property) -> tuple: temp_name = res['template', ''] if temp_name: template = template_brush.get_template(temp_name) else: template = None return ( template, res.vec('offset'), )
def res_piston_plat_setup(res: Property): # Allow reading instances direct from the ID. # But use direct ones first. item_id = res['itemid', None] inst = {} for name in INST_NAMES: if name in res: lookup = res[name] if lookup == '': # Special case, allow blank for no instance. inst[name] = '' continue elif item_id is not None: lookup = '<{}:bee2_pist_{}>'.format(item_id, name) else: raise ValueError('No "{}" specified!'.format(name)) inst[name] = resolve_single(lookup, error=True) template = template_brush.get_template(res['template']) visgroup_names = [ res['visgroup_1', 'pist_1'], res['visgroup_2', 'pist_2'], res['visgroup_3', 'pist_3'], res['visgroup_top', 'pist_4'], ] return ( template, visgroup_names, inst, res.bool('has_dn_fizz'), res['auto_var', ''], res['color_var', ''], res['source_ent', ''], res['snd_start', ''], res['snd_loop', ''], res['snd_stop', ''], )
def place_template(inst: Entity) -> None: """Place a template.""" temp_id = inst.fixup.substitute(orig_temp_id) # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(flag, coll, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) force_colour = conf_force_colour if color_var == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour] # else: False value, no invert. if ang_override is not None: orient = ang_override else: orient = rotation @ Angle.from_str(inst['angles', '0 0 0']) origin = conditions.resolve_offset(inst, offset) # If this var is set, it forces all to be included. if srctools.conv_bool( conditions.resolve_value(inst, visgroup_force_var)): visgroups.update(template.visgroups) elif visgroup_func is not None: visgroups.update( visgroup_func( rand.seed(b'temp', template.id, origin, orient), list(template.visgroups), )) LOGGER.debug('Placing template "{}" at {} with visgroups {}', template.id, origin, visgroups) temp_data = template_brush.import_template( vmf, template, origin, orient, targetname=inst['targetname'], force_type=force_type, add_to_map=True, coll=coll, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, align_bind=align_bind_overlay, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, orient) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ orient temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get(picker_name, None) if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
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_barriers(vmf: VMF): """Make barrier entities. get_tex is vbsp.get_tex.""" glass_temp = template_brush.get_scaling_template( options.get(str, "glass_template")) grate_temp = template_brush.get_scaling_template( options.get(str, "grating_template")) hole_temp_small: List[Solid] hole_temp_lrg_diag: List[Solid] hole_temp_lrg_cutout: List[Solid] hole_temp_lrg_square: List[Solid] # Avoid error without this package. if HOLES: # Grab the template solids we need. hole_combined_temp = template_brush.get_template( options.get(str, 'glass_hole_temp')) hole_world, hole_detail, _ = hole_combined_temp.visgrouped({'small'}) hole_temp_small = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_diagonal'}) hole_temp_lrg_diag = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_cutout'}) hole_temp_lrg_cutout = hole_world + hole_detail hole_world, hole_detail, _ = hole_combined_temp.visgrouped( {'large_square'}) hole_temp_lrg_square = hole_world + hole_detail else: hole_temp_small = hole_temp_lrg_diag = hole_temp_lrg_cutout = hole_temp_lrg_square = [] floorbeam_temp = options.get(str, 'glass_floorbeam_temp') if options.get_itemconf('BEE_PELLET:PelletGrating', False): # Merge together these existing filters in global_pti_ents vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_multi', filtertype=0, negated=0, filter01='@not_pellet', filter02='@not_paint_bomb', ) else: # Just skip paint bombs. vmf.create_ent( origin=options.get(Vec, 'global_pti_ents_loc'), targetname='@grating_filter', classname='filter_activator_class', negated=1, filterclass='prop_paint_bomb', ) # Group the positions by planes in each orientation. # This makes them 2D grids which we can optimise. # (normal_dist, positive_axis, type) -> [(x, y)] slices: Dict[Tuple[Tuple[float, float, float], bool, BarrierType], Dict[Tuple[int, int], False]] = defaultdict(dict) # We have this on the 32-grid so we can cut squares for holes. for (origin_tup, normal_tup), barr_type in BARRIERS.items(): origin = Vec(origin_tup) normal = Vec(normal_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[ norm_pos.as_tuple(), # distance from origin to this plane. normal[norm_axis] > 0, barr_type, ] for u_off in [-48, -16, 16, 48]: for v_off in [-48, -16, 16, 48]: slice_plane[int((u + u_off) // 32), int((v + v_off) // 32), ] = True # Remove pane sections where the holes are. We then generate those with # templates for slanted parts. for (origin_tup, norm_tup), hole_type in HOLES.items(): barr_type = BARRIERS[origin_tup, norm_tup] origin = Vec(origin_tup) normal = Vec(norm_tup) norm_axis = normal.axis() u, v = origin.other_axes(norm_axis) norm_pos = Vec.with_axes(norm_axis, origin) slice_plane = slices[norm_pos.as_tuple(), normal[norm_axis] > 0, barr_type, ] if hole_type is HoleType.LARGE: offsets = (-80, -48, -16, 16, 48, 80) else: offsets = (-16, 16) for u_off in offsets: for v_off in offsets: # Remove these squares, but keep them in the dict # so we can check if there was glass there. uv = ( int((u + u_off) // 32), int((v + v_off) // 32), ) if uv in slice_plane: slice_plane[uv] = False # These have to be present, except for the corners # on the large hole. elif abs(u_off) != 80 or abs(v_off) != 80: u_ax, v_ax = Vec.INV_AXIS[norm_axis] LOGGER.warning( 'Hole tried to remove missing tile at ({})?', Vec.with_axes(norm_axis, norm_pos, u_ax, u + u_off, v_ax, v + v_off), ) # Now generate the curved brushwork. if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError angles = normal.to_angle() hole_temp: List[Tuple[List[Solid], Matrix]] = [] # This is a tricky bit. Two large templates would collide # diagonally, and we allow the corner glass to not be present since # the hole doesn't actually use that 32x32 segment. # So we need to determine which of 3 templates to use. corn_angles = angles.copy() if hole_type is HoleType.LARGE: for corn_angles.roll in (0, 90, 180, 270): corn_mat = Matrix.from_angle(corn_angles) corn_dir = Vec(y=1, z=1) @ corn_angles hole_off = origin + 128 * corn_dir diag_type = HOLES.get( (hole_off.as_tuple(), normal.as_tuple()), None, ) corner_pos = origin + 80 * corn_dir corn_u, corn_v = corner_pos.other_axes(norm_axis) corn_u = int(corn_u // 32) corn_v = int(corn_v // 32) if diag_type is HoleType.LARGE: # There's another large template to this direction. # Just have 1 generate both combined, so the brushes can # be more optimal. To pick, arbitrarily make the upper one # be in charge. if corn_v > v // 32: hole_temp.append((hole_temp_lrg_diag, corn_mat)) continue if (corn_u, corn_v) in slice_plane: hole_temp.append((hole_temp_lrg_square, corn_mat)) else: hole_temp.append((hole_temp_lrg_cutout, corn_mat)) else: hole_temp.append((hole_temp_small, Matrix.from_angle(angles))) def solid_pane_func(off1: float, off2: float, mat: str) -> List[Solid]: """Given the two thicknesses, produce the curved hole from the template.""" off_min = 64 - max(off1, off2) off_max = 64 - min(off1, off2) new_brushes = [] for brushes, matrix in hole_temp: for orig_brush in brushes: brush = orig_brush.copy(vmf_file=vmf) new_brushes.append(brush) for face in brush.sides: face.mat = mat for point in face.planes: if point.x > 64: point.x = off_max else: point.x = off_min face.localise(origin, matrix) # Increase precision, these are small detail brushes. face.lightmap = 8 return new_brushes make_glass_grating( vmf, origin, normal, barr_type, front_temp, solid_pane_func, ) for (plane_pos, is_pos, barr_type), pos_slice in slices.items(): plane_pos = Vec(plane_pos) norm_axis = plane_pos.axis() normal = Vec.with_axes(norm_axis, 1 if is_pos else -1) if barr_type is BarrierType.GLASS: front_temp = glass_temp elif barr_type is BarrierType.GRATING: front_temp = grate_temp else: raise NotImplementedError u_axis, v_axis = Vec.INV_AXIS[norm_axis] for min_u, min_v, max_u, max_v in grid_optimise(pos_slice): # These are two points in the origin plane, at the borders. pos_min = Vec.with_axes( norm_axis, plane_pos, u_axis, min_u * 32, v_axis, min_v * 32, ) pos_max = Vec.with_axes( norm_axis, plane_pos, u_axis, max_u * 32 + 32, v_axis, max_v * 32 + 32, ) def solid_pane_func(pos1: float, pos2: float, mat: str) -> List[Solid]: """Make the solid brush.""" return [ vmf.make_prism( pos_min + normal * (64.0 - pos1), pos_max + normal * (64.0 - pos2), mat=mat, ).solid ] make_glass_grating( vmf, (pos_min + pos_max) / 2 + 63 * normal, normal, barr_type, front_temp, solid_pane_func, ) # Generate hint brushes, to ensure sorting is done correctly. [hint] = solid_pane_func(0, 4.0, consts.Tools.SKIP) for side in hint: if abs(Vec.dot(side.normal(), normal)) > 0.99: side.mat = consts.Tools.HINT vmf.add_brush(hint) if floorbeam_temp: LOGGER.info('Adding Glass floor beams...') add_glass_floorbeams(vmf, floorbeam_temp) LOGGER.info('Done!')
def res_piston_plat(vmf: VMF, res: Property) -> conditions.ResultCallable: """Generates piston platforms with optimized logic.""" # Allow reading instances direct from the ID. # But use direct ones first. item_id = res['itemid', None] inst_filenames = {} for name in INST_NAMES: if name in res: lookup = res[name] if lookup == '': # Special case, allow blank for no instance. inst_filenames[name] = '' continue elif item_id is not None: lookup = f'<{item_id}:bee2_pist_{name}>' else: raise ValueError(f'No "{name}" specified!') inst_filenames[name] = resolve_single(lookup, error=True) template = template_brush.get_template(res['template']) conf_visgroup_names = [ res['visgroup_1', 'pist_1'], res['visgroup_2', 'pist_2'], res['visgroup_3', 'pist_3'], res['visgroup_top', 'pist_4'], ] has_dn_fizz = res.bool('has_dn_fizz') automatic_var = res['auto_var', ''] source_ent = res['source_ent', ''] snd_start = res['snd_start', ''] snd_loop = res['snd_loop', ''] snd_stop = res['snd_stop', ''] def modify_platform(inst: Entity) -> None: """Modify each platform.""" min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, fname) for fname in conf_visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = fname = inst_filenames['fullstatic_' + str(position)] conditions.ALL_INST.add(fname) return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=conditions.local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if has_dn_fizz: script_ent['thinkfunction'] = 'FizzThink' if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', f'moveto({st_pos})'), Output('OnUser2', '!self', 'RunScriptCode', f'moveto({end_pos})'), ) origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) off = orient.up(128) move_ang = off.to_angle() # Index -> func_movelinear. pistons: dict[int, Entity] = {} static_ent = vmf.create_ent('func_brush', origin=origin) for pist_ind in [1, 2, 3, 4]: pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = fname = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = fname = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # The max pos was evaluated earlier, so this must be set. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=conditions.local_name( pist_ent, f'pist{pist_ind}'), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind][ 'parentname'] = conditions.local_name( pist_ent, f'pist{pist_ind - 1}', ) if fname: conditions.ALL_INST.add(fname.casefold()) else: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( vmf, template, brush_pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = origin - orient.up(128) panel: Optional[Panel] = None try: tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()] except KeyError: pass else: for panel in tiledef.panels: if panel.same_item(inst): break else: # Checked all of them. panel = None if panel is not None: if panel.brush_ent in vmf.entities and not panel.brush_ent.solids: panel.brush_ent.remove() panel.brush_ent = pistons[max(pistons.keys())] panel.offset = st_pos * off if not static_ent.solids and (panel is None or panel.brush_ent is not static_ent): static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = 100 script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = conditions.local_name(inst, source_ent) return modify_platform
def res_import_template(vmf: VMF, inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. If no block is used, only ID can be specified. Options: - `ID`: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (`temp_id:vis1,vis2`). Either section, or the whole value can be a `$fixup`. - `force`: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - `replace`: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a `#`, it is instead a list of face IDs separated by spaces. If the result evaluates to "", no change occurs. Both can be $fixups (parsed first). - `bindOverlay`: Bind overlays in this template to the given surface, and bind overlays on a surface to surfaces in this template. The value specifies the offset to the surface, where 0 0 0 is the floor position. It can also be a block of multiple positions. - `keys`/`localkeys`: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - `"origin"`, offset automatically. - `"movedir"` on func_movelinear - set a normal surrounded by `<>`, this gets replaced with angles. - `colorVar`: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - `invertVar`: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - `visgroup`: Sets how visgrouped parts are handled. Several values are possible: - A property block: Each name should match a visgroup, and the value should be a block of flags that if true enables that group. - 'none' (default): All extra groups are ignored. - 'choose': One group is chosen randomly. - a number: The percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `pickerVars`: If this is set, the results of colorpickers can be read out of the template. The key is the name of the picker, the value is the fixup name to write to. The output is either 'white', 'black' or ''. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. - `senseOffset`: If set, colorpickers and tilesetters will be treated as being offset by this amount. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, surf_cat, bind_tile_pos, invert_var, color_var, visgroup_func, visgroup_force_var, visgroup_instvars, key_block, picker_vars, outputs, sense_offset, ) = res.value temp_id = inst.fixup.substitute(orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(vmf, flag, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( vmf, template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ angles temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get( picker_name, None, ) # type: Optional[texturing.Portalable] if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''