def add_output(inst: Entity) -> None: """Add the output.""" if out_type in ('activate', 'deactivate'): try: item_type = connections.ITEM_TYPES[out_id.casefold()] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': if item_type.output_act is None: return inst_out, output = item_type.output_act else: if item_type.output_deact is None: return inst_out, output = item_type.output_deact else: output = out_id inst_out = conf_inst_out inst.add_out( Output( inst.fixup.substitute(output), conditions.local_name(inst, inst.fixup.substitute(targ)) or inst['targetname'], inst.fixup.substitute(input_name), inst.fixup.substitute(parm), srctools.conv_float(inst.fixup.substitute(delay)), times=times, inst_out=inst.fixup.substitute(inst_out) or None, inst_in=inst.fixup.substitute(inst_in) or None, ))
def add_output(inst: Entity, prop: Property, target: str) -> None: """Add a customisable output to an instance.""" inst.add_out(Output( prop['output', ''], target, prop['input', ''], inst_in=prop['targ_in', ''], inst_out=prop['targ_out', ''], ))
def add_output(inst: Entity, prop: Property, target: str) -> None: """Add a customisable output to an instance.""" inst.add_out(Output( prop['output', ''], target, prop['input', ''], inst_in=prop['targ_in', ''], inst_out=prop['targ_out', ''], ))
def res_add_output(inst: Entity, res: Property): """Add an output from an instance to a global or local name. Values: - `output`: The output name. Can be `<ITEM_ID:activate>` or `<ITEM_ID:deactivate>` to lookup that item type. - `target`: The name of the target entity - `input`: The input to give - `parm`: Parameters for the input - `delay`: Delay for the output - `only_once`: True to make the input last only once (overrides times) - `times`: The number of times to trigger the input """ ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value if out_type in ('activate', 'deactivate'): try: item_type = connections.ITEM_TYPES[out_id.casefold()] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': if item_type.output_act is None: return inst_out, output = item_type.output_act else: if item_type.output_deact is None: return inst_out, output = item_type.output_deact else: output = resolve_value(inst, out_id) inst_out = resolve_value(inst, inst_out) inst.add_out( Output( resolve_value(inst, output), local_name(inst, resolve_value(inst, targ)) or inst['targetname'], resolve_value(inst, input_name), resolve_value(inst, parm), srctools.conv_float(resolve_value(inst, delay)), times=times, inst_out=resolve_value(inst, inst_out) or None, inst_in=resolve_value(inst, inst_in) or None, ))
def res_add_output(inst: Entity, res: Property): """Add an output from an instance to a global or local name. Values: - output: The output name.Can be <ITEM_ID:activate> or <ITEM_ID:deactivate> to lookup that item type. - target: The name of the target entity - input: The input to give - parm: Parameters for the input - delay: Delay for the output - only_once: True to make the input last only once (overrides times) - times: The number of times to trigger the input """ ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value LOGGER.info('Conn: {}', res.value) if out_type in ('activate', 'deactivate'): try: connection = CONNECTIONS[out_id] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': inst_out, output = connection.out_act else: inst_out, output = connection.out_deact else: output = resolve_value(inst, out_id) inst_out = resolve_value(inst, inst_out) inst.add_out( Output( resolve_value(inst, output), local_name(inst, resolve_value(inst, targ)), resolve_value(inst, input_name), resolve_value(inst, parm), srctools.conv_float(resolve_value(inst, delay)), times=times, inst_out=resolve_value(inst, inst_out) or None, inst_in=resolve_value(inst, inst_in) or None, ))
def res_add_output(inst: Entity, res: Property): """Add an output from an instance to a global or local name. Values: - output: The output name.Can be <ITEM_ID:activate> or <ITEM_ID:deactivate> to lookup that item type. - target: The name of the target entity - input: The input to give - parm: Parameters for the input - delay: Delay for the output - only_once: True to make the input last only once (overrides times) - times: The number of times to trigger the input """ ( out_type, out_id, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value LOGGER.info('Conn: {}', res.value) if out_type in ('activate', 'deactivate'): try: connection = CONNECTIONS[out_id] except KeyError: LOGGER.warning('"{}" has no connections!', out_id) return if out_type[0] == 'a': inst_out, output = connection.out_act else: inst_out, output = connection.out_deact else: output = resolve_value(inst, out_id) inst_out = resolve_value(inst, inst_out) inst.add_out(Output( resolve_value(inst, output), local_name(inst, resolve_value(inst, targ)), resolve_value(inst, input_name), resolve_value(inst, parm), srctools.conv_float(resolve_value(inst, delay)), times=times, inst_out=resolve_value(inst, inst_out) or None, inst_in=resolve_value(inst, inst_in) or None, ))
def __init__(self, ent: Entity): """Convert the entity to have the right logic.""" self.scanner = None self.persist_tv = conv_bool(ent.keys.pop('persist_tv', False)) pos = Vec.from_str(ent['origin']) for prop in ent.map.by_class['prop_dynamic']: if (Vec.from_str(prop['origin']) - pos).mag_sq() > 64**2: continue model = prop['model'].casefold().replace('\\', '/') # Allow spelling this correctly, if you're not Valve. if 'vacum_scanner_tv' in model or 'vacuum_scanner_tv' in model: self.scanner = prop prop.make_unique('_vac_scanner') elif 'vacum_scanner_motion' in model or 'vacuum_scanner_motion' in model: prop.make_unique('_vac_scanner') ent.add_out(Output(self.pass_out_name, prop, "SetAnimation", "scan01")) super(Straight, self).__init__(ent)
def res_linked_cube_dropper(drp_inst: Entity, res: Property): """Link a cube and dropper together, to preplace the cube at a location.""" time = drp_inst.fixup.int('$timer_delay') # Portal 2 bug - when loading existing maps, timers are set to 3... if not (3 < time <= 30): # Infinite or 3-second - this behaviour is disabled.. return try: cube_inst, cube_type, resp_out_name, resp_out = LINKED_CUBES[time] except KeyError: raise Exception('Unknown cube "linkage" value ({}) in dropper!'.format( time, )) # Force the dropper to match the cube.. # = cube_type # Set auto-drop to False (so there isn't two cubes), # and auto-respawn to True (so it actually functions). drp_inst.fixup['$disable_autodrop'] = '1' drp_inst.fixup['$disable_autorespawn'] = '0' fizz_out_name, fizz_out = Output.parse_name(res['FizzleOut']) # Output to destroy the cube when the dropper is triggered externally. drp_inst.add_out(Output( inst_out=fizz_out_name, out=fizz_out, targ=local_name(cube_inst, 'cube'), inp='Dissolve', only_once=True, )) # Cube items don't have proxies, so we need to use AddOutput # after it's created (@relay_spawn_3's time). try: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] except KeyError: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] = cube_inst.map.create_ent( classname='logic_relay', targetname='@relay_spawn_3', origin=cube_inst['origin'], ) respawn_inp = list(res.find_all('RespawnIn')) # There's some voice-logic specific to companion cubes. respawn_inp.extend(res.find_all( 'RespawnCcube' if drp_inst.fixup['$cube_type'] == '1' else 'RespawnCube' )) for inp in respawn_inp: resp_in_name, resp_in = inp.value.split(':', 1) out = Output( out='OnFizzled', targ=drp_inst, inst_in=resp_in_name, inp=resp_in, only_once=True, ) relay_spawn_3.add_out(Output( out='OnTrigger', targ=local_name(cube_inst, 'cube'), inp='AddOutput', param=out.gen_addoutput(), only_once=True, delay=0.01, ))
def parse(cls, vmf: VMF, ent: Entity, radius: float) -> 'Dropper': """Scan the map applying dropper tweaks, then create the Dropper object.""" filter_name = ent['filtername'] template_name = ent['template'] for cube_filter in vmf.search(filter_name): break else: raise ValueError( f'No filter "{filter_name}" for dropper at {ent["origin"]}!') for template in vmf.search(template_name): break else: raise ValueError( f'No template "{template_name}" for dropper at {ent["origin"]}!' ) best_cube = None best_dist = math.inf radius **= 2 ref_pos = Vec.from_str(cube_filter['origin']) for cube in vmf.by_class['prop_weighted_cube'] | vmf.by_class[ 'prop_monster_box']: dist = (Vec.from_str(cube['origin']) - ref_pos).mag_sq() if dist > radius or dist > best_dist: continue best_dist = dist best_cube = cube if best_cube is None: LOGGER.warning( 'Cube dropper at {} has no cube. Generating standard one...', ref_pos) best_cube = vmf.create_ent( 'prop_weighted_cube', angles='0 0 0', newskins='1', skintype='0', cubetype='0', skin='0', paintpower='4', model=CUBE_MODEL, ) # Now adjust the cube for dropper use. best_cube.make_unique('dropper_cube') best_cube['origin'] = ent['origin'] # Only regular cubes can disable funnelling, but frankenturrets # require being in box form. if best_cube['classname'] == 'prop_monster_box': best_cube['startasbox'] = '1' else: best_cube['allowfunnel'] = '0' # Copy the cube name to filter and dropper. cube_filter['filtername'] = best_cube['targetname'] for i in range(1, 10): if not template[f'Template{i:02}']: template[f'Template{i:02}'] = best_cube['targetname'] break else: raise ValueError(f'No spare slots for template "{template_name}"!') # Add fizzle outputs if enabled. if srctools.conv_bool(ent['autorespawn']): best_cube.outputs += [ out for out in ent.outputs if out.output.casefold() == 'onfizzled' ] ent.add_out(Output(Dropper.pass_out_name, template, 'ForceSpawn')) return Dropper(ent, template, best_cube)
def res_cust_output(inst: Entity, res: Property): """Add an additional output to the instance with any values. Always points to the targeted item. If DecConCount is 1, connections """ ( outputs, dec_con_count, targ_conditions, force_sign_type, (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), ) = res.value over_name = '@' + inst['targetname'] + '_indicator' for toggle in vbsp.VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Build a mapping from names to targets. # This is also the set of all output items, plus indicators. targets = defaultdict(list) for out in inst.outputs: if out.target != toggle_name: targets[out.target].append(out) pan_files = instanceLocs.resolve('[indPan]') # These all require us to search through the instances. if force_sign_type or dec_con_count or targ_conditions: for con_inst in vbsp.VMF.by_class['func_instance']: # type: Entity if con_inst['targetname'] not in targets: # Not our instance continue # Is it an indicator panel, and should we be modding it? if force_sign_type is not None and con_inst['file'].casefold( ) in pan_files: # Remove the panel if force_sign_type == '': con_inst.remove() continue # Overwrite the signage instance, and then add the # appropriate outputs to control it. sign_id, sign_file_id = force_sign_type con_inst['file'] = instanceLocs.resolve_one(sign_file_id, error=True) # First delete the original outputs: for out in targets[con_inst['targetname']]: inst.outputs.remove(out) inputs = CONNECTIONS[sign_id] act_name, act_inp = inputs.in_act deact_name, deact_inp = inputs.in_deact LOGGER.info('outputs: a="{}" d="{}"\n' 'inputs: a="{}" d="{}"'.format( (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), inputs.in_act, inputs.in_deact)) if act_inp and sign_act_out: inst.add_out( Output( inst_out=sign_act_name, out=sign_act_out, inst_in=act_name, inp=act_inp, targ=con_inst['targetname'], )) if deact_inp and sign_deact_out: inst.add_out( Output( inst_out=sign_deact_name, out=sign_deact_out, inst_in=deact_name, inp=deact_inp, targ=con_inst['targetname'], )) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val - 1) except ValueError: # skip if it's invalid LOGGER.warning(con_inst['targetname'] + ' has invalid ConnectionCount!') if targ_conditions: for cond in targ_conditions: # type: Condition cond.test(con_inst) if outputs: for targ in targets: for out in outputs: conditions.add_output(inst, out, targ)
def generate_fizzlers(vmf: VMF): """Generates fizzler models and the brushes according to their set types. After this is done, fizzler-related conditions will not function correctly. However the model instances are now available for modification. """ from vbsp import MAP_RAND_SEED, TO_PACK, PACK_FILES for fizz in FIZZLERS.values(): if fizz.base_inst not in vmf.entities: continue # The fizzler was removed from the map. fizz_name = fizz.base_inst['targetname'] fizz_type = fizz.fizz_type # Static versions are only used for fizzlers which start on. # Permanently-off fizzlers are kinda useless, so we don't need # to bother optimising for it. is_static = bool( fizz.base_inst.fixup.int('$connectioncount', 0) == 0 and fizz.base_inst.fixup.bool('$start_enabled', 1)) if is_static: TO_PACK |= fizz.fizz_type.pack_lists_static TO_PACK |= fizz.fizz_type.pack_lists if fizz_type.inst[FizzInst.BASE, is_static]: random.seed('{}_fizz_base_{}'.format(MAP_RAND_SEED, fizz_name)) fizz.base_inst['file'] = random.choice( fizz_type.inst[FizzInst.BASE, is_static]) if not fizz.emitters: LOGGER.warning('No emitters for fizzler "{}"!', fizz_name) continue # Brush index -> entity for ones that need to merge. # template_brush is used for the templated one. single_brushes = {} # type: Dict[FizzlerBrush, Entity] if fizz_type.temp_max or fizz_type.temp_min: template_brush_ent = vmf.create_ent( classname='func_brush', origin=fizz.base_inst['origin'], ) conditions.set_ent_keys( template_brush_ent, fizz.base_inst, fizz_type.temp_brush_keys, ) else: template_brush_ent = None up_dir = fizz.up_axis forward = (fizz.emitters[0][1] - fizz.emitters[0][0]).norm() min_angles = FIZZ_ANGLES[forward.as_tuple(), up_dir.as_tuple()] max_angles = FIZZ_ANGLES[(-forward).as_tuple(), up_dir.as_tuple()] model_min = (fizz_type.inst[FizzInst.PAIR_MIN, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) model_max = (fizz_type.inst[FizzInst.PAIR_MAX, is_static] or fizz_type.inst[FizzInst.ALL, is_static]) if not model_min or not model_max: raise ValueError( 'No model specified for one side of "{}"' ' fizzlers'.format(fizz_type.id), ) # Define a function to do the model names. model_index = 0 if fizz_type.model_naming is ModelName.SAME: def get_model_name(ind): """Give every emitter the base's name.""" return fizz_name elif fizz_type.model_naming is ModelName.LOCAL: def get_model_name(ind): """Give every emitter a name local to the base.""" return fizz_name + '-' + fizz_type.model_name elif fizz_type.model_naming is ModelName.PAIRED: def get_model_name(ind): """Give each pair of emitters the same unique name.""" return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, ind, ) elif fizz_type.model_naming is ModelName.UNIQUE: def get_model_name(ind): """Give every model a unique name.""" nonlocal model_index model_index += 1 return '{}-{}{:02}'.format( fizz_name, fizz_type.model_name, model_index, ) else: raise ValueError('Bad ModelName?') # Generate env_beam pairs. for beam in fizz_type.beams: beam_template = Entity(vmf) conditions.set_ent_keys(beam_template, fizz.base_inst, beam.keys) beam_template['classname'] = 'env_beam' del beam_template[ 'LightningEnd'] # Don't allow users to set end pos. name = beam_template['targetname'] + '_' counter = 1 for seg_min, seg_max in fizz.emitters: for offset in beam.offset: # type: Vec min_off = offset.copy() max_off = offset.copy() min_off.localise(seg_min, min_angles) max_off.localise(seg_max, max_angles) beam_ent = beam_template.copy() vmf.add_ent(beam_ent) # Allow randomising speed and direction. if 0 < beam.speed_min < beam.speed_max: random.seed('{}{}{}'.format(MAP_RAND_SEED, min_off, max_off)) beam_ent['TextureScroll'] = random.randint( beam.speed_min, beam.speed_max) if random.choice((False, True)): # Flip to reverse direction. min_off, max_off = max_off, min_off beam_ent['origin'] = min_off beam_ent['LightningStart'] = beam_ent['targetname'] = ( name + str(counter)) counter += 1 beam_ent['targetpoint'] = max_off mat_mod_tex = {} # type: Dict[FizzlerBrush, Set[str]] for brush_type in fizz_type.brushes: if brush_type.mat_mod_var is not None: mat_mod_tex[brush_type] = set() trigger_hurt_name = '' for seg_ind, (seg_min, seg_max) in enumerate(fizz.emitters, start=1): length = (seg_max - seg_min).mag() random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_min)) if length == 128 and fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]: min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(fizz_type.inst[FizzInst.PAIR_SINGLE, is_static]), origin=(seg_min + seg_max) / 2, angles=min_angles, ) else: # Both side models. min_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_min), origin=seg_min, angles=min_angles, ) random.seed('{}_fizz_{}'.format(MAP_RAND_SEED, seg_max)) max_inst = vmf.create_ent( targetname=get_model_name(seg_ind), classname='func_instance', file=random.choice(model_max), origin=seg_max, angles=max_angles, ) max_inst.fixup.update(fizz.base_inst.fixup) min_inst.fixup.update(fizz.base_inst.fixup) if fizz_type.inst[FizzInst.GRID, is_static]: # Generate one instance for each position. # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for ind, dist in enumerate(range(64, round(length) - 63, 128)): mid_pos = seg_min + forward * dist random.seed('{}_fizz_mid_{}'.format( MAP_RAND_SEED, mid_pos)) mid_inst = vmf.create_ent( classname='func_instance', targetname=fizz_name, angles=min_angles, file=random.choice(fizz_type.inst[FizzInst.GRID, is_static]), origin=mid_pos, ) mid_inst.fixup.update(fizz.base_inst.fixup) if template_brush_ent is not None: if length == 128 and fizz_type.temp_single: temp = template_brush.import_template( fizz_type.temp_single, (seg_min + seg_max) / 2, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) else: if fizz_type.temp_min: temp = template_brush.import_template( fizz_type.temp_min, seg_min, min_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) if fizz_type.temp_max: temp = template_brush.import_template( fizz_type.temp_max, seg_max, max_angles, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) template_brush_ent.solids.extend(temp.world) # Generate the brushes. for brush_type in fizz_type.brushes: brush_ent = None # If singular, we reuse the same brush ent for all the segments. if brush_type.singular: brush_ent = single_brushes.get(brush_type, None) # Non-singular or not generated yet - make the entity. if brush_ent is None: brush_ent = Entity(vmf, keys=brush_type.keys) vmf.add_ent(brush_ent) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, key_value) brush_ent['targetname'] = conditions.local_name( fizz.base_inst, brush_type.name, ) # Set this to the center, to make sure it's not going to leak. brush_ent['origin'] = (seg_min + seg_max) / 2 # For fizzlers flat on the floor/ceiling, scanlines look # useless. Turn them off. if 'usescanline' in brush_ent and fizz.normal().z: brush_ent['UseScanline'] = 0 if brush_ent['classname'] == 'trigger_hurt': trigger_hurt_name = brush_ent['targetname'] if brush_type.set_axis_var: axis_script = 'BEE2/fizzler_axis_{}.nut'.format( fizz.normal().axis()) scripts = brush_ent['vscripts'].split() scripts.insert(0, axis_script) brush_ent['vscripts'] = ' '.join(scripts) PACK_FILES.add('scripts/vscripts/' + axis_script) for out in brush_type.outputs: new_out = out.copy() new_out.target = conditions.local_name( fizz.base_inst, new_out.target, ) brush_ent.add_out(new_out) if brush_type.singular: # Record for the next iteration. single_brushes[brush_type] = brush_ent # If we have a material_modify_control to generate, # we need to parent it to ourselves to restrict it to us # only. We also need one for each material, so provide a # function to the generator which adds to a set. if brush_type.mat_mod_var is not None: used_tex_func = mat_mod_tex[brush_type].add else: def used_tex_func(val): """If not, ignore those calls.""" return None # Generate the brushes and texture them. brush_ent.solids.extend( brush_type.generate( vmf, fizz, seg_min, seg_max, used_tex_func, )) if trigger_hurt_name: fizz.gen_flinch_trigs(vmf, trigger_hurt_name) # If we have the config, but no templates used anywhere... if template_brush_ent is not None and not template_brush_ent.solids: template_brush_ent.remove() for brush_type, used_tex in mat_mod_tex.items(): brush_name = conditions.local_name(fizz.base_inst, brush_type.name) mat_mod_name = conditions.local_name(fizz.base_inst, brush_type.mat_mod_name) for off, tex in zip(MATMOD_OFFSETS, sorted(used_tex)): pos = off.copy().rotate(*min_angles) pos += Vec.from_str(fizz.base_inst['origin']) vmf.create_ent( classname='material_modify_control', origin=pos, targetname=mat_mod_name, materialName='materials/' + tex + '.vmt', materialVar=brush_type.mat_mod_var, parentname=brush_name, )