def add_locking(item: Item) -> None: """Create IO to control buttons from the target item. This allows items to customise how buttons behave. """ if item.config.output_lock is None and item.config.output_unlock is None: return if item.config.input_type is InputType.DUAL: LOGGER.warning( 'Item type ({}) with locking IO, but dual inputs. ' 'Locking functionality is ignored!', item.config.id ) return # If more than one, it's not logical to lock the button. try: [lock_conn] = item.inputs except ValueError: return lock_button = lock_conn.from_item if item.config.inf_lock_only and lock_button.timer is not None: return # Check the button doesn't also activate other things - # we need exclusive control. # Also the button actually needs to be lockable. if len(lock_button.outputs) != 1 or not lock_button.config.lock_cmd: return instance_traits.get(item.inst).add('locking_targ') instance_traits.get(lock_button.inst).add('locking_btn') # Force the item to not have a timer. for pan in item.ind_panels: pan.remove() item.ind_panels.clear() for output, input_cmds in [ (item.config.output_lock, lock_button.config.lock_cmd), (item.config.output_unlock, lock_button.config.unlock_cmd) ]: if not output: continue for cmd in input_cmds: if cmd.target: target = conditions.local_name(lock_button.inst, cmd.target) else: target = lock_button.inst item.add_io_command( output, target, cmd.input, cmd.params, delay=cmd.delay, times=cmd.times, )
def flag_has_trait(inst: Entity, flag: Property) -> bool: """Check if the instance has a specific 'trait', which is set by code. Current traits: * `white`, `black`: If editoritems indicates the colour of the item. * `arrival_departure_transition`: `arrival_departure_transition_ents`. * `barrier`: Glass/grating instances: * `barrier_128`: Segment instance. * `barrier_frame`: Any frame part. * `frame_convex_corner`: Convex corner (unused). * `frame_short`: Shortened frame to fit a corner. * `frame_straight`: Straight frame section. * `frame_corner`: Frame corner section. * `frame_left`: Left half of the frame. * `frame_right`: Right half of the frame. * `floor_button`: ItemButtonFloor type item: * `btn_ball`: Button Type = Sphere. * `btn_cube`: Button Type = Cube * `weighted`: Button Type = Weighted * `dropperless`: A dropperless Cube: * `cube_standard`: Normal Cube. * `cube_companion`: Companion Cube. * `cube_ball`: Edgeless Safety Cube. * `cube_reflect`: Discouragment Redirection Cube. * `cube_franken`: FrankenTurret. * `preplaced`: The various pre-existing instances: * `coop_corridor`: A Coop exit Corridor. * `sp_corridor`: SP entry or exit corridor. * `corridor_frame`: White/black door frame. * `corridor_1`-`7`: The specified entry/exit corridor. * `elevator`: An elevator instance. * `entry_elevator`: Entry Elevator. * `exit_elevator`: Exit Elevator. * `entry_corridor`: Entry SP Corridor. * `exit_corridor`: Exit SP/Coop Corridor. * `fizzler`: A fizzler item: * `fizzler_base`: Logic instance. * `fizzler_model`: Model instance. * `cust_shape`: Set if the fizzler has been moved to a custom position by ReshapeFizzler. * `locking_targ`: Target of a locking pedestal button. * `locking_btn`: Locking pedestal button. * `paint_dropper`: Gel Dropper: * `paint_dropper_bomb`: Bomb-type dropper. * `paint_dropper_sprayer`: Sprayer-type dropper. * `panel_angled`: Angled Panel-type item. * `track_platform`: Track Platform-style item: * `plat_bottom`: Bottom frame. * `plat_bottom_grate`: Grating. * `plat_middle`: Middle frame. * `plat_single`: One-long frame. * `plat_top`: Top frame. * `plat_non_osc`: Non-oscillating platform. * `plat_osc`: Oscillating platform. * `tbeam_emitter`: Funnel emitter. * `tbeam_frame`: Funnel frame. """ return flag.value.casefold() in instance_traits.get(inst)
def set_random_seed(inst: Entity, seed: str) -> None: """Compute and set a random seed for a specific entity.""" from precomp import instance_traits name = inst['targetname'] # The global instances like elevators always get the same name, or # none at all so we cannot use those for the seed. Instead use the global # seed. if name == '' or 'preplaced' in instance_traits.get(inst): import vbsp random.seed('{}{}{}{}'.format( vbsp.MAP_RAND_SEED, seed, inst['origin'], inst['angles'], )) else: # We still need to use angles and origin, since things like # fizzlers might not get unique names. random.seed('{}{}{}{}'.format( inst['targetname'], seed, inst['origin'], inst['angles'] ))
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 calc_connections( vmf: VMF, antlines: Dict[str, List[Antline]], shape_frame_tex: List[str], enable_shape_frame: bool, *, # Don't mix up antlines! antline_wall: AntType, antline_floor: AntType, ) -> None: """Compute item connections from the map file. This also fixes cases where items have incorrect checkmark/timer signs. Instance Traits must have been calculated. It also applies frames to shape signage to distinguish repeats. """ # First we want to match targetnames to item types. toggles = {} # type: Dict[str, Entity] # Accumulate all the signs into groups, so the list should be 2-long: # sign_shapes[name, material][0/1] sign_shape_overlays = defaultdict( list) # type: Dict[Tuple[str, str], List[Entity]] # Indicator panels panels = {} # type: Dict[str, Entity] # We only need to pay attention for TBeams, other items we can # just detect any output. tbeam_polarity = {OutNames.IN_SEC_ACT, OutNames.IN_SEC_DEACT} # Also applies to other items, but not needed for this analysis. tbeam_io = {OutNames.IN_ACT, OutNames.IN_DEACT} for inst in vmf.by_class['func_instance']: inst_name = inst['targetname'] # No connections, so nothing to worry about. if not inst_name: continue traits = instance_traits.get(inst) if 'indicator_toggle' in traits: toggles[inst_name] = inst # We do not use toggle instances. inst.remove() elif 'indicator_panel' in traits: panels[inst_name] = inst elif 'fizzler_model' in traits: # Ignore fizzler models - they shouldn't have the connections. # Just the base itself. pass else: # Normal item. item_id = instance_traits.get_item_id(inst) if item_id is None: LOGGER.warning('No item ID for "{}"!', inst) continue try: item_type = ITEM_TYPES[item_id.casefold()] except KeyError: LOGGER.warning('No item type for "{}"!', item_id) continue if item_type is None: # It exists, but has no I/O. continue # Pass in the defaults for antline styles. ITEMS[inst_name] = Item( inst, item_type, ant_floor_style=antline_floor, ant_wall_style=antline_wall, ) # Strip off the original connection count variables, these are # invalid. if item_type.input_type is InputType.DUAL: del inst.fixup[consts.FixupVars.CONN_COUNT] del inst.fixup[consts.FixupVars.CONN_COUNT_TBEAM] for over in vmf.by_class['info_overlay']: name = over['targetname'] mat = over['material'] if mat in SIGN_ORDER_LOOKUP: sign_shape_overlays[name, mat.casefold()].append(over) # Name -> signs pairs sign_shapes = defaultdict(list) # type: Dict[str, List[ShapeSignage]] # By material index, for group frames. sign_shape_by_index = defaultdict( list) # type: Dict[int, List[ShapeSignage]] for (name, mat), sign_pair in sign_shape_overlays.items(): # It's possible - but rare - for more than 2 to be in a pair. # We have to just treat them as all in their 'pair'. # Shouldn't be an issue, it'll be both from one item... shape = ShapeSignage(sign_pair) sign_shapes[name].append(shape) sign_shape_by_index[shape.index].append(shape) # Now build the connections and items. for item in ITEMS.values(): input_items: List[Item] = [] # Instances we trigger inputs: Dict[str, List[Output]] = defaultdict(list) if item.inst.outputs and item.config is None: raise ValueError('No connections for item "{}", ' 'but outputs in the map!'.format( instance_traits.get_item_id(item.inst))) for out in item.inst.outputs: inputs[out.target].append(out) # Remove the original outputs, we've consumed those already. item.inst.outputs.clear() # Pre-set the timer value, for items without antlines but with an output. if consts.FixupVars.TIM_DELAY in item.inst.fixup: if item.config.output_act or item.config.output_deact: item.timer = tim = item.inst.fixup.int( consts.FixupVars.TIM_DELAY) if not (1 <= tim <= 30): # These would be infinite. item.timer = None for out_name in inputs: # Fizzler base -> model/brush outputs, ignore these (discard). # fizzler.py will regenerate as needed. if out_name.rstrip('0123456789').endswith( ('_modelStart', '_modelEnd', '_brush')): continue if out_name in toggles: inst_toggle = toggles[out_name] try: item.antlines.update( antlines[inst_toggle.fixup['indicator_name']]) except KeyError: pass elif out_name in panels: pan = panels[out_name] item.ind_panels.add(pan) if pan.fixup.bool(consts.FixupVars.TIM_ENABLED): item.timer = tim = pan.fixup.int( consts.FixupVars.TIM_DELAY) if not (1 <= tim <= 30): # These would be infinite. item.timer = None else: item.timer = None else: try: inp_item = ITEMS[out_name] except KeyError: raise ValueError( '"{}" is not a known instance!'.format(out_name)) else: input_items.append(inp_item) if inp_item.config is None: raise ValueError('No connections for item "{}", ' 'but inputs in the map!'.format( instance_traits.get_item_id( inp_item.inst))) for inp_item in input_items: # Default A/B type. conn_type = ConnType.DEFAULT in_outputs = inputs[inp_item.name] if inp_item.config.id == 'ITEM_TBEAM': # It's a funnel - we need to figure out if this is polarity, # or normal on/off. for out in in_outputs: if out.input in tbeam_polarity: conn_type = ConnType.TBEAM_DIR break elif out.input in tbeam_io: conn_type = ConnType.TBEAM_IO break else: raise ValueError('Excursion Funnel "{}" has inputs, ' 'but no valid types!'.format( inp_item.name)) conn = Connection( inp_item, item, conn_type, in_outputs, ) conn.add() # Make signage frames shape_frame_tex = [mat for mat in shape_frame_tex if mat] if shape_frame_tex and enable_shape_frame: for shape_mat in sign_shape_by_index.values(): # Sort so which gets what frame is consistent. shape_mat.sort() for index, shape in enumerate(shape_mat): shape.repeat_group = index if index == 0: continue # First, no frames.. frame_mat = shape_frame_tex[(index - 1) % len(shape_frame_tex)] for overlay in shape: frame = overlay.copy() shape.overlay_frames.append(frame) vmf.add_ent(frame) frame['material'] = frame_mat frame['renderorder'] = 1 # On top
def traits(self) -> Set[str]: """Return the set of instance traits for the item.""" return instance_traits.get(self.inst)
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] = ''