def res_global_input(vmf: VMF, inst: Entity, res: Property): """Trigger an input either on map spawn, or when a relay is triggered. Arguments: - `Input`: the input to use, either a name or an `instance:` command. - `Target`: If set, a local name to send commands to. Otherwise, the instance itself. - `Delay`: Number of seconds to delay the input. - `Name`: If set the name of the `logic_relay` which must be triggered. If not set the output will fire `OnMapSpawn`. - `Output`: The name of the output, defaulting to `OnTrigger`. Ignored if Name is not set. - `Param`: The parameter for the output. Alternatively pass a string VMF-style output, which only provides OnMapSpawn functionality. """ relay_name, out = res.value output = out.copy() # type: Output if output.target: output.target = conditions.local_name( inst, conditions.resolve_value(inst, output.target)) else: output.target = inst['targetname'] relay_name = conditions.resolve_value(inst, relay_name) output.params = conditions.resolve_value(inst, output.params) global_input(vmf, inst['origin'], output, relay_name)
def res_global_input(vmf: VMF, inst: Entity, res: Property): """Trigger an input either on map spawn, or when a relay is triggered. Arguments: - `Input`: the input to use, either a name or an `instance:` command. - `Target`: If set, a local name to send commands to. Otherwise, the instance itself. - `Delay`: Number of seconds to delay the input. - `Name`: If set the name of the `logic_relay` which must be triggered. If not set the output will fire `OnMapSpawn`. - `Output`: The name of the output, defaulting to `OnTrigger`. Ignored if Name is not set. - `Param`: The parameter for the output. Alternatively pass a string VMF-style output, which only provides OnMapSpawn functionality. """ relay_name, out = res.value output = out.copy() # type: Output if output.target: output.target = conditions.local_name( inst, conditions.resolve_value(inst, output.target) ) else: output.target = inst['targetname'] relay_name = conditions.resolve_value(inst, relay_name) output.params = conditions.resolve_value(inst, output.params) global_input(vmf, inst['origin'], output, relay_name)
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 do_item_optimisation(vmf: VMF): """Optimise redundant logic items.""" needs_global_toggle = False for item in list(ITEMS.values()): # We can't remove items that have functionality, or don't have IO. if item.item_type is None or not item.item_type.input_type.is_logic: continue prim_inverted = conv_bool( conditions.resolve_value( item.inst, item.item_type.invert_var, )) sec_inverted = conv_bool( conditions.resolve_value( item.inst, item.item_type.sec_invert_var, )) # Don't optimise if inverted. if prim_inverted or sec_inverted: continue inp_count = len(item.inputs) if inp_count == 0: # Totally useless, remove. # We just leave the panel entities, and tie all the antlines # to the same toggle. needs_global_toggle = True for ent in item.antlines: ent['targetname'] = '_static_ind' del ITEMS[item.name] item.inst.remove() elif inp_count == 1: # Only one input, so AND or OR are useless. # Transfer input item to point to the output(s). collapse_item(item) # The antlines need a toggle entity, otherwise they'll copy random other # overlays. if needs_global_toggle: vmf.create_ent( classname='env_texturetoggle', origin=vbsp_options.get(Vec, 'global_ents_loc'), targetname='_static_ind_tog', target='_static_ind', )
def flag_instvar(inst: Entity, flag: Property): """Checks if the $replace value matches the given value. The flag value follows the form `$start_enabled == 1`, with or without the `$`. The operator can be any of `=`, `==`, `<`, `>`, `<=`, `>=`, `!=`. If omitted, the operation is assumed to be `==`. If only the variable name is present, it is tested as a boolean flag. """ values = flag.value.split(' ', 3) if len(values) == 3: variable, op, comp_val = values value = inst.fixup[variable] comp_val = conditions.resolve_value(inst, comp_val) try: # Convert to floats if possible, otherwise handle both as strings. # That ensures we normalise different number formats (1 vs 1.0) comp_val, value = float(comp_val), float(value) except ValueError: pass return INSTVAR_COMP.get(op, operator.eq)(value, comp_val) elif len(values) == 2: variable, value = values return inst.fixup[variable] == value else: # For just a name. return inst.fixup.bool(flag.value)
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_set_inst_var(inst: Entity, res: Property): """Set an instance variable to the given value. Values follow the format `$start_enabled 1`, with or without the `$`. `$out $in` will copy the value of `$in` into `$out`. """ var_name, val = res.value.split(' ', 1) inst.fixup[var_name] = conditions.resolve_value(inst, val)
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: VLib.Entity, res: Property): """Add an output from an instance to a global name. Values: - 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 """ ( output, targ, input_name, parm, delay, times, inst_in, inst_out, ) = res.value inst.add_out(VLib.Output( resolve_value(inst, output), resolve_value(inst, targ), resolve_value(inst, input_name), resolve_value(inst, parm), resolve_value(inst, delay), times=times, inst_out=resolve_value(inst, inst_out), inst_in=resolve_value(inst, inst_in), ))
def res_global_input(inst: Entity, res: Property): """Trigger an input either on map spawn, or when a relay is triggered. Arguments: - "Input": the input to use, either a name or an instance: command. - "Target": If set, a local name to send commands to. - "Delay": Number of seconds to delay the input. - "Name": If set the name of the logic_relay which must be triggered. If not set the output will fire OnMapSpawn. - "Output": The name of the output, defaulting to OnTrigger. Ignored if Name is not set. - "Param": The parameter for the output. """ name, inp_name, inp_command, output, delay, param, target = res.value if name is not None: name = conditions.resolve_value(inst, name) if target is not None: target = conditions.resolve_value(inst, target) try: glob_ent = GLOBAL_INPUT_ENTS[name] except KeyError: if name is None: glob_ent = GLOBAL_INPUT_ENTS[None] = inst.map.create_ent( classname='logic_auto', origin=inst['origin'], ) else: glob_ent = GLOBAL_INPUT_ENTS[name] = inst.map.create_ent( classname='logic_relay', targetname=name, origin=inst['origin'], ) out = Output( out=('OnMapSpawn' if name is None else output), targ=(conditions.local_name(inst, target) if target else inst['targetname']), inp=inp_command, inst_in=inp_name, delay=delay, param=conditions.resolve_value(inst, param), ) glob_ent.add_out(out)
def global_input( inst: Entity, command: str, proxy_name: str = None, relay_name: str = None, relay_out: str = 'OnTrigger', target: str = None, param='', delay=0.0, ): """Create a global input.""" if relay_name is not None: relay_name = conditions.resolve_value(inst, relay_name) if target is not None: target = conditions.resolve_value(inst, target) try: glob_ent = GLOBAL_INPUT_ENTS[relay_name] except KeyError: if relay_name is None: glob_ent = GLOBAL_INPUT_ENTS[None] = inst.map.create_ent( classname='logic_auto', origin=inst['origin'], ) else: glob_ent = GLOBAL_INPUT_ENTS[relay_name] = inst.map.create_ent( classname='logic_relay', targetname=relay_name, origin=inst['origin'], ) out = Output( out=('OnMapSpawn' if relay_name is None else relay_out), targ=(conditions.local_name(inst, target) if target else inst['targetname']), inp=command, inst_in=proxy_name, delay=delay, param=conditions.resolve_value(inst, param), ) glob_ent.add_out(out)
def res_script_var(inst: Entity, res: Property): """Set a variable on a script, via a logic_auto. Name is the local name for the script entity. Var is the variable name. Value is the value to set. """ global_input( inst, command='RunScriptCode', target=conditions.local_name(inst, res['name']), param='{} <- {}'.format( res['var'], conditions.resolve_value(inst, res['value']), ), )
def res_script_var(vmf: VMF, inst: Entity, res: Property): """Set a variable on a script, via a logic_auto. Name is the local name for the script entity. Var is the variable name. Value is the value to set. """ global_input( vmf, inst['origin'], Output( 'OnMapSpawn', conditions.local_name(inst, res['name']), 'RunScriptCode', param='{} <- {}'.format( res['var'], conditions.resolve_value(inst, res['value']), ), ), )
def flag_offset_distance(inst: Entity, flag: Property) -> bool: """Check if the given instance is in an offset position. This computes the distance between the instance location and the center of the voxel. The value can be the distance for an exact check, '< x', '> $var', etc. """ origin = Vec.from_str(inst['origin']) grid_pos = origin // 128 * 128 + 64 offset = (origin - grid_pos).mag() try: op, comp_val = flag.value.split() except ValueError: # A single value. op = '=' comp_val = flag.value try: value = float(conditions.resolve_value(inst, comp_val)) except ValueError: return False return INSTVAR_COMP.get(op, operator.eq)(offset, value)
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 res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value(node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value(node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED) ): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value(node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value(node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. 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. """ angle = res["angles", inst["angles", "0 0 0"]] overlay_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=inst["targetname", ""], file=resolve_inst(res["file", ""])[0], 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: folded_off = res["offset"].casefold() # Offset the overlay by the given distance # Some special placeholder values: if folded_off == "<piston_start>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_top>" else: folded_off = "<piston_bottom>" elif folded_off == "<piston_end>": if srctools.conv_bool(inst.fixup["$start_up", ""]): folded_off = "<piston_bottom>" else: folded_off = "<piston_top>" if folded_off == "<piston_bottom>": offset = Vec(z=srctools.conv_int(inst.fixup["$bottom_level"]) * 128) elif folded_off == "<piston_top>": offset = Vec(z=srctools.conv_int(inst.fixup["$top_level"], 1) * 128) else: # Regular vector offset = Vec.from_str(conditions.resolve_value(inst, res["offset"])) offset.rotate_by_str(inst["angles", "0 0 0"]) overlay_inst["origin"] = (offset + Vec.from_str(inst["origin"])).join(" ") return overlay_inst
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 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)) pack_list = (fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists) for pack in pack_list: packing.pack_list(vmf, pack) 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() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' 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 = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value( fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( 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'] trigger_hurt_start_disabled = brush_ent[ 'startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format(fizz.normal().axis(), )) 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, trigger_hurt_start_disabled, ) # 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, )
def res_water_splash(vmf: VMF, inst: Entity, res: Property): """Creates splashes when something goes in and out of water. Arguments: - parent: The name of the parent entity. - name: The name given to the env_splash. - scale: The size of the effect (8 by default). - position: The offset position to place the entity. - position2: The offset to which the entity will move. - type: Use certain fixup values to calculate pos2 instead: 'piston_1/2/3/4': Use $bottom_level and $top_level as offsets. 'track_platform': Use $travel_direction, $travel_distance, etc. - fast_check: Check faster for movement. Needed for items which move quickly. """ ( name, parent, scale, pos1, pos2, calc_type, fast_check, ) = res.value # type: str, str, float, Vec, str, str pos1 = pos1.copy() # type: Vec splash_pos = pos1.copy() # type: Vec if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = inst.fixup['$travel_direction'] start_pos = srctools.conv_float(inst.fixup['$starting_position']) if start_pos: start_pos = round(start_pos * lin_off) pos1 += Vec(x=-start_pos).rotate_by_str(travel_ang) pos2 = Vec(x=lin_off).rotate_by_str(travel_ang) pos2 += pos1 elif calc_type.startswith('piston'): # Use piston-platform offsetting. # The number is the highest offset to move to. max_pist = srctools.conv_int(calc_type.split('_', 2)[1], 4) bottom_pos = srctools.conv_int(inst.fixup['$bottom_level']) top_pos = min(srctools.conv_int(inst.fixup['$top_level']), max_pist) pos2 = pos1.copy() pos1 += Vec(z=128 * bottom_pos) pos2 += Vec(z=128 * top_pos) LOGGER.info('Bottom: {}, top: {}', bottom_pos, top_pos) else: # Directly from the given value. pos2 = Vec.from_str(conditions.resolve_value(inst, pos2)) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) splash_pos.localise(origin, angles) pos1.localise(origin, angles) pos2.localise(origin, angles) # Since it's a straight line and you can't go through walls, # if pos1 and pos2 aren't in goo we aren't ever in goo. check_pos = [pos1, pos2] if pos1.z < origin.z: # If embedding in the floor, the positions can both be below the # actual surface. In that case check the origin too. check_pos.append(Vec(pos1.x, pos1.y, origin.z)) for pos in check_pos: grid_pos = pos // 128 * 128 # type: Vec grid_pos += (64, 64, 64) try: surf = conditions.GOO_LOCS[grid_pos.as_tuple()] except KeyError: continue break else: return # Not in goo at all if pos1.z == pos2.z: # Flat - this won't do anything... return water_pos = surf.get_origin() # Check if both positions are above or below the water.. # that means it won't ever trigger. LOGGER.info('pos1: {}, pos2: {}, water_pos: {}', pos1.z, pos2.z, water_pos.z) if max(pos1.z, pos2.z) < water_pos.z - 8: return if min(pos1.z, pos2.z) > water_pos.z + 8: return # Pass along the water_pos encoded into the targetname. # Restrict the number of characters to allow direct slicing # in the script. enc_data = '_{:09.3f}{}'.format( water_pos.z + 12, 'f' if fast_check else 's', ) vmf.create_ent( classname='env_splash', targetname=conditions.local_name(inst, name) + enc_data, parentname=conditions.local_name(inst, parent), origin=splash_pos + (0, 0, 16), scale=scale, vscripts='BEE2/water_splash.nut', thinkfunction='Think', spawnflags='1', # Trace downward to water surface. )
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 res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - 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 face ID. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - 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. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. - outputs: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, 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 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 if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.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 = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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).rotate(*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) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def add_timer_relay(item: Item, has_sounds: bool): """Make a relay to play timer sounds, or fire once the outputs are done.""" rl_name = item.name + '_timer_rl' relay = item.inst.map.create_ent( 'logic_relay', targetname=rl_name, startDisabled=0, spawnflags=0, ) if has_sounds: relay['vscripts'] = 'BEE2/timer_sound.nut' if item.item_type.timer_sound_pos: relay_loc = item.item_type.timer_sound_pos.copy() relay_loc.localise( Vec.from_str(item.inst['origin']), Vec.from_str(item.inst['angles']), ) relay['origin'] = relay_loc else: relay['origin'] = item.inst['origin'] for cmd in item.item_type.timer_done_cmd: if cmd: relay.add_out( Output( 'OnTrigger', conditions.local_name(item.inst, cmd.target) or item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), inst_in=cmd.inst_in, delay=item.timer + cmd.delay, times=cmd.times, )) if item.item_type.timer_sound_pos is not None and has_sounds: timer_sound = vbsp_options.get(str, 'timer_sound') timer_cc = vbsp_options.get(str, 'timer_sound_cc') # The default sound has 'ticking' closed captions. # So reuse that if the style doesn't specify a different noise. # If explicitly set to '', we don't use this at all! if timer_cc is None and timer_sound != 'Portal.room1_TickTock': timer_cc = 'Portal.room1_TickTock' if timer_cc: timer_cc = 'cc_emit ' + timer_cc for delay in range(item.timer): relay.add_out( Output( 'OnTrigger', '!self', 'CallScriptFunction', 'snd', delay=delay, )) if timer_cc: relay.add_out( Output( 'OnTrigger', '@command', 'Command', timer_cc, delay=delay, )) for outputs, cmd in [(item.timer_output_start(), 'Trigger'), (item.timer_output_stop(), 'CancelPending')]: for out_name, out_cmd in outputs: item.inst.add_out(Output(out_cmd, rl_name, cmd, inst_out=out_name))
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 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) ) pack_list = ( fizz.fizz_type.pack_lists_static if is_static else fizz.fizz_type.pack_lists ) for pack in pack_list: packing.pack_list(vmf, pack) 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 # Prepare to copy over instance traits for the emitters. fizz_traits = instance_traits.get(fizz.base_inst).copy() # Special case, mark emitters that have a custom position for Clean # models. if fizz.has_cust_position: fizz_traits.add('cust_shape') 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() # Record the data for trigger hurts so flinch triggers can match them. trigger_hurt_name = '' trigger_hurt_start_disabled = '0' 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) instance_traits.get(max_inst).update(fizz_traits) min_inst.fixup.update(fizz.base_inst.fixup) instance_traits.get(min_inst).update(fizz_traits) 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) instance_traits.get(mid_inst).update(fizz_traits) 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 = vmf.create_ent(classname='func_brush') for key_name, key_value in brush_type.keys.items(): brush_ent[key_name] = conditions.resolve_value(fizz.base_inst, key_value) for key_name, key_value in brush_type.local_keys.items(): brush_ent[key_name] = conditions.local_name( fizz.base_inst, conditions.resolve_value( 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'] trigger_hurt_start_disabled = brush_ent['startdisabled'] if brush_type.set_axis_var: brush_ent['vscript_init_code'] = ( 'axis <- `{}`;'.format( fizz.normal().axis(), ) ) 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, trigger_hurt_start_disabled, ) # 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, )
def res_piston_plat(vmf: VMF, inst: Entity, res: Property) -> None: """Generates piston platforms with optimized logic.""" template: template_brush.Template visgroup_names: List[str] inst_filenames: Dict[str, str] has_dn_fizz: bool automatic_var: str color_var: str source_ent: str snd_start: str snd_loop: str snd_stop: str ( template, visgroup_names, inst_filenames, has_dn_fizz, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value 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, name) for name in 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'] = inst_filenames['fullstatic_' + str(position)] 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=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', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: 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'] = 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'] = 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. # That's before this so it'll have to exist. 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=local_name(pist_ent, 'pist' + str(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'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, 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 = Vec(z=-128) tile_pos.localise(origin, angles) 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'] = local_name(inst, source_ent)
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. 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 face ID. - `replaceBrush`: The position of a brush to replace (`0 0 0`=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - `Pos`: The position to replace. - `additionalIDs`: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - `removeBrush`: If true, the original brush will not be removed. - `transferOverlay`: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be a `$fixup`). - `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. If `none` (default), they are ignored. If `choose`, one is chosen. If a number, that is the percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, outputs, ) = res.value if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, 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 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 if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.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 = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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).rotate(*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) # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, # Don't allow clumping if using custom keyvalues - then it won't be edited. no_clumping=key_block is not None, )
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value( node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value( node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED)): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value( node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value( node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE( base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. 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. """ angle = res['angles', inst['angles', '0 0 0']] overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=resolve_inst(res['file', ''])[0], 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: folded_off = res['offset'].casefold() # Offset the overlay by the given distance # Some special placeholder values: if folded_off == '<piston_start>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_top>' else: folded_off = '<piston_bottom>' elif folded_off == '<piston_end>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_bottom>' else: folded_off = '<piston_top>' if folded_off == '<piston_bottom>': offset = Vec( z=srctools.conv_int(inst.fixup['$bottom_level']) * 128, ) elif folded_off == '<piston_top>': offset = Vec( z=srctools.conv_int(inst.fixup['$top_level'], 1) * 128, ) else: # Regular vector offset = Vec.from_str(conditions.resolve_value(inst, res['offset'])) offset.rotate_by_str( inst['angles', '0 0 0'] ) overlay_inst['origin'] = ( offset + Vec.from_str(inst['origin']) ).join(' ') return overlay_inst
def res_import_template(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 if ':' in orig_temp_id: # Split, resolve each part, then recombine. temp_id, visgroup = orig_temp_id.split(':', 1) temp_id = ( conditions.resolve_value(inst, temp_id) + ':' + conditions.resolve_value(inst, visgroup) ) else: temp_id = conditions.resolve_value(inst, 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(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 = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( 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).rotate(*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] = ''
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 add_item_indicators( item: Item, inst_type: PanelSwitchingStyle, pan_item: ItemType, ): """Generate the commands for antlines, and restyle them.""" ant_name = '@{}_overlay'.format(item.name) has_sign = len(item.ind_panels) > 0 for ind in item.antlines: ind['targetname'] = ant_name antlines.style_antline(ind, item.ant_wall_style, item.ant_floor_style) # If the antline material doesn't toggle, the name is removed by # style_antline(). So check if the overlay actually exists still, to # see if we need to add the toggle. has_ant = len(item.inst.map.by_target[ant_name]) > 0 # Special case - the item wants full control over its antlines. if has_ant and item.ant_toggle_var: item.inst.fixup[item.ant_toggle_var] = ant_name # We don't have antlines to control. has_ant = False if inst_type is PanelSwitchingStyle.CUSTOM: needs_toggle = has_ant elif inst_type is PanelSwitchingStyle.EXTERNAL: needs_toggle = has_ant or has_sign elif inst_type is PanelSwitchingStyle.INTERNAL: if (item.item_type.timer_start is not None or item.item_type.timer_stop is not None): # The item is doing custom control over the timer, so # don't tie antline control to the timer. needs_toggle = has_ant inst_type = PanelSwitchingStyle.CUSTOM else: needs_toggle = has_ant and not has_sign else: raise ValueError('Bad switch style ' + repr(inst_type)) first_inst = True for pan in item.ind_panels: if inst_type is PanelSwitchingStyle.EXTERNAL: pan.fixup[const.FixupVars.TOGGLE_OVERLAY] = ant_name # Ensure only one gets the indicator name. elif first_inst and inst_type is PanelSwitchingStyle.INTERNAL: pan.fixup[ const.FixupVars.TOGGLE_OVERLAY] = ant_name if has_ant else ' ' first_inst = False else: # VBSP and/or Hammer seems to get confused with totally empty # instance var, so give it a blank name. pan.fixup[const.FixupVars.TOGGLE_OVERLAY] = '-' for outputs, input_cmds in [ (item.timer_output_start(), pan_item.enable_cmd), (item.timer_output_stop(), pan_item.disable_cmd) ]: if not input_cmds: continue for out_name, out in outputs: for cmd in input_cmds: item.inst.add_out( Output( out, conditions.local_name( pan, conditions.resolve_value( item.inst, cmd.target), ) or pan, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), inst_out=out_name, inst_in=cmd.inst_in, times=cmd.times, )) if needs_toggle: toggle = item.inst.map.create_ent( classname='env_texturetoggle', origin=Vec.from_str(item.inst['origin']) + (0, 0, 16), targetname='toggle_' + item.name, target=ant_name, ) # Don't use the configurable inputs - if they want that, use custAntline. for output, skin in [(item.output_act(), '1'), (item.output_deact(), '0')]: if not output: continue out_name, out = output item.inst.add_out( Output( out, toggle, 'SetTextureIndex', skin, inst_out=out_name, ))
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str 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, name) for name in 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'] = inst_filenames['fullstatic_' + str(position)] 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=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) 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', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): 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'] = 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'] = 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. # That's before this so it'll have to exist. 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=local_name(pist_ent, 'pist' + str(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'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, 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, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: 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'] = local_name(inst, source_ent)
def add_item_inputs( item: Item, logic_type: InputType, inputs: List[Connection], count_var: str, enable_cmd: Iterable[Output], disable_cmd: Iterable[Output], invert_var: str, ): """Handle either the primary or secondary inputs to an item.""" item.inst.fixup[count_var] = len(inputs) if logic_type is InputType.DEFAULT: # 'Original' PeTI proxies. for conn in inputs: inp_item = conn.from_item for output, input_cmds in [(inp_item.output_act(), enable_cmd), (inp_item.output_deact(), disable_cmd)]: if not output or not input_cmds: continue out_name, out_cmd = output for cmd in input_cmds: inp_item.inst.add_out( Output( out_cmd, item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), inst_out=out_name, inst_in=cmd.inst_in, delay=cmd.delay, )) return elif logic_type is InputType.DAISYCHAIN: # Another special case, these items AND themselves with their inputs. # We aren't called if we have no inputs, so we don't need to handle that. # We transform the instance into a counter, but first duplicate the # instance as a new entity. This way references to the instance add # outputs to the counter instead. orig_inst = item.inst.copy() orig_inst.map.add_ent(orig_inst) orig_inst.outputs.clear() counter = item.inst counter.clear_keys() counter['origin'] = orig_inst['origin'] counter[ 'targetname'] = orig_inst['targetname'] + COUNTER_NAME[count_var] counter['classname'] = 'math_counter' counter['min'] = 0 counter['max'] = len(inputs) + 1 for output, input_name in [(item.item_type.output_act, 'Add'), (item.item_type.output_deact, 'Subtract')]: if not output: continue out_name, out_cmd = output orig_inst.add_out( Output( out_cmd, counter, input_name, '1', inst_out=out_name, )) for conn in inputs: inp_item = conn.from_item for output, input_name in [(inp_item.output_act(), 'Add'), (inp_item.output_deact(), 'Subtract')]: if not output: continue out_name, out_cmd = output inp_item.inst.add_out( Output( out_cmd, counter, input_name, '1', inst_out=out_name, )) return is_inverted = conv_bool(conditions.resolve_value( item.inst, invert_var, )) if is_inverted: enable_cmd, disable_cmd = disable_cmd, enable_cmd needs_counter = len(inputs) > 1 if logic_type.is_logic: origin = item.inst['origin'] name = item.name counter = item.inst counter.clear_keys() counter['origin'] = origin counter['targetname'] = name counter['classname'] = 'math_counter' if not needs_counter: LOGGER.warning('Item "{}" was not optimised out!', name) # Force counter so it still works. needs_counter = True elif needs_counter: counter = item.inst.map.create_ent( classname='math_counter', targetname=item.name + COUNTER_NAME[count_var], origin=item.inst['origin'], ) else: counter = None if needs_counter: counter['min'] = counter['startvalue'] = counter['StartDisabled'] = 0 counter['max'] = len(inputs) for conn in inputs: inp_item = conn.from_item for output, input_name in [(inp_item.output_act(), 'Add'), (inp_item.output_deact(), 'Subtract')]: if not output: continue out_name, out_cmd = output inp_item.inst.add_out( Output( out_cmd, counter, input_name, '1', inst_out=out_name, )) if logic_type is InputType.AND: count_on = COUNTER_AND_ON count_off = COUNTER_AND_OFF elif logic_type is InputType.OR: count_on = COUNTER_OR_ON count_off = COUNTER_OR_OFF elif logic_type.is_logic: # We don't add outputs here, the outputted items do that. # counter is item.inst, so those are added to that. LOGGER.info('LOGIC counter: {}', counter['targetname']) return else: # Should never happen, not other types. raise ValueError('Unknown counter logic type: ' + repr(logic_type)) for output_name, input_cmds in [(count_on, enable_cmd), (count_off, disable_cmd)]: if not input_cmds: continue for cmd in input_cmds: counter.add_out( Output( output_name, conditions.local_name( item.inst, conditions.resolve_value(item.inst, cmd.target), ) or item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), delay=cmd.delay, times=cmd.times, )) else: # No counter - fire directly. for conn in inputs: inp_item = conn.from_item for output, input_cmds in [(inp_item.output_act(), enable_cmd), (inp_item.output_deact(), disable_cmd)]: if not output or not input_cmds: continue out_name, out_cmd = output for cmd in input_cmds: inp_item.inst.add_out( Output( out_cmd, conditions.local_name( item.inst, conditions.resolve_value( item.inst, cmd.target), ) or item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), inst_out=out_name, delay=cmd.delay, times=cmd.times, ))
def res_import_template(inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. Options: - ID: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (temp_id:vis1,vis2) - 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. - replaceBrush: The position of a brush to replace (0 0 0=the surface). This brush will be removed, and overlays will be fixed to use all faces with the same normal. Can alternately be a block: - Pos: The position to replace. - additionalIDs: Space-separated list of face IDs in the template to also fix for overlays. The surface should have close to a vertical normal, to prevent rescaling the overlay. - removeBrush: If true, the original brush will not be removed. - transferOverlay: Allow disabling transferring overlays to this template. The IDs will be removed instead. (This can be an instvar). - 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. If 'none' (default), they are ignored. If 'choose', one is chosen. If a number, that is the percentage chance for each visgroup to be added. - visgroup_force_var: If set and True, visgroup is ignored and all groups are added. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_replace_ids, invert_var, color_var, visgroup_func, visgroup_force_var, key_block, ) = res.value temp_id = conditions.resolve_value(inst, 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 temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # The template map is read in after setup is performed, so # it must be checked here! # We don't want an error, just quit 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) return if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = template_brush.MAT_TYPES.white elif 'black' in traits: force_colour = template_brush.MAT_TYPES.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 = template_brush.MAT_TYPES.white elif color_val == 'black': force_colour = template_brush.MAT_TYPES.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 = Vec.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, ) 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).rotate(*angles) temp_data.detail['movedir'] = move_dir.to_angle() # Add it to the list of ignored brushes, so vbsp.change_brush() doesn't # modify it. vbsp.IGNORED_BRUSH_ENTS.add(temp_data.detail) try: # This is the original brush the template is replacing. We fix overlay # face IDs, so this brush is replaced by the faces in the template # pointing # the same way. if replace_brush_pos is None: raise KeyError # Not set, raise to jump out of the try block pos = Vec(replace_brush_pos).rotate(angles.x, angles.y, angles.z) pos += origin brush_group = SOLIDS[pos.as_tuple()] except KeyError: # Not set or solid group doesn't exist, skip.. pass else: LOGGER.info('IDS: {}', additional_replace_ids | template.overlay_faces) conditions.steal_from_brush( temp_data, brush_group, rem_replace_brush, map(int, additional_replace_ids | template.overlay_faces), conv_bool(conditions.resolve_value(inst, transfer_overlays), True), ) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, )
def res_piston_plat(vmf: VMF, inst: Entity, res: Property): """Generates piston platforms with optimized logic.""" ( template, visgroup_names, inst_filenames, automatic_var, color_var, source_ent, snd_start, snd_loop, snd_stop, ) = res.value # type: template_brush.Template, List[str], Dict[str, str], str, str, str, str, str, str 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, name) for name in 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'] = inst_filenames['fullstatic_' + str(position)] 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=local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) 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', 'moveto({})'.format(st_pos)), Output('OnUser2', '!self', 'RunScriptCode', 'moveto({})'.format(end_pos)), ) origin = Vec.from_str(inst['origin']) angles = Vec.from_str(inst['angles']) off = Vec(z=128).rotate(*angles) move_ang = off.to_angle() # Index -> func_movelinear. pistons = {} # type: Dict[int, Entity] static_ent = vmf.create_ent('func_brush', origin=origin) color_var = conditions.resolve_value(inst, color_var).casefold() if color_var == 'white': top_color = template_brush.MAT_TYPES.white elif color_var == 'black': top_color = template_brush.MAT_TYPES.black else: top_color = None for pist_ind in range(1, 5): 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'] = 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'] = 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. # That's before this so it'll have to exist. 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=local_name(pist_ent, 'pist' + str(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'] = local_name( pist_ent, 'pist' + str(pist_ind - 1), ) if not pist_ent['file']: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( template, brush_pos, angles, 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, force_colour=top_color, force_grid='special', no_clumping=True, ) if not static_ent.solids: 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'] = local_name(inst, source_ent)