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 add_io_command( self, output: Optional[Tuple[Optional[str], str]], target: Union[Entity, str], inp_cmd: str, params: str = '', delay: float = 0.0, times: int = -1, inst_in: Optional[str]=None, ) -> None: """Add an output to this item. For convenience, if the output is None this does nothing. """ if output is None: return out_name, out_cmd = output if not out_name: out_name = '' # Dump the None. out_name = conditions.resolve_value(self.inst, out_name) if isinstance(target, Entity): target = target['targetname'] try: kv_setter = self._kv_setters[out_name] except KeyError: if out_name: full_name = conditions.local_name(self.inst, out_name) else: full_name = self.name kv_setter = self._kv_setters[out_name] = self.inst.map.create_ent( 'comp_kv_setter', origin=self.inst['origin'], target=full_name, ) kv_setter.add_out(Output( conditions.resolve_value(self.inst, out_cmd), target, inp_cmd, params, delay=delay, times=times, inst_in=inst_in, ))
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 do_item_optimisation(vmf: VMF) -> None: """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.config is None or not item.config.input_type.is_logic: continue prim_inverted = conv_bool(conditions.resolve_value( item.inst, item.config.invert_var, )) sec_inverted = conv_bool(conditions.resolve_value( item.inst, item.config.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 ant in item.antlines: ant.name = '_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=options.get(Vec, 'global_ents_loc'), targetname='_static_ind_tog', target='_static_ind', )
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_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 add_timer_relay(item: Item, has_sounds: bool) -> None: """Make a relay to play timer sounds, or fire once the outputs are done.""" assert item.timer is not None rl_name = item.name + '_timer_rl' relay = item.inst.map.create_ent( 'logic_relay', targetname=rl_name, startDisabled=0, spawnflags=0, ) if item.config.timer_sound_pos: relay_loc = item.config.timer_sound_pos.copy() relay_loc.localise( Vec.from_str(item.inst['origin']), Angle.from_str(item.inst['angles']), ) relay['origin'] = relay_loc else: relay['origin'] = item.inst['origin'] for cmd in item.config.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.config.timer_sound_pos is not None and has_sounds: timer_sound = options.get(str, 'timer_sound') timer_cc = 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 # Write out the VScript code to precache the sound, and play it on # demand. relay['vscript_init_code'] = ( 'function Precache() {' f'self.PrecacheSoundScript(`{timer_sound}`)' '}') relay['vscript_init_code2'] = ('function snd() {' f'self.EmitSound(`{timer_sound}`)' '}') packing.pack_files(item.inst.map, timer_sound, file_type='sound') 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 output in outputs: item.add_io_command(output, rl_name, cmd)
def add_item_indicators( item: Item, inst_type: PanelSwitchingStyle, pan_item: Config, ) -> None: """Generate the commands for antlines and the overlays themselves.""" ant_name = '@{}_overlay'.format(item.name) has_sign = len(item.ind_panels) > 0 has_ant = len(item.antlines) > 0 for ant in item.antlines: ant.name = ant_name ant.export(item.inst.map, wall_conf=item.ant_wall_style, floor_conf=item.ant_floor_style) # 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.config.timer_start is not None or item.config.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[consts.FixupVars.TOGGLE_OVERLAY] = ant_name # Ensure only one gets the indicator name. elif first_inst and inst_type is PanelSwitchingStyle.INTERNAL: pan.fixup[ consts.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[consts.FixupVars.TOGGLE_OVERLAY] = '-' # Overwrite the timer delay value, in case a sign changed ownership. if item.timer is not None: pan.fixup[consts.FixupVars.TIM_DELAY] = item.timer pan.fixup[consts.FixupVars.TIM_ENABLED] = '1' else: pan.fixup[consts.FixupVars.TIM_DELAY] = '99999999999' pan.fixup[consts.FixupVars.TIM_ENABLED] = '0' for outputs, input_cmds in [ (item.timer_output_start(), pan_item.enable_cmd), (item.timer_output_stop(), pan_item.disable_cmd) ]: for output in outputs: for cmd in input_cmds: item.add_io_command( output, 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), delay=cmd.delay, 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. item.add_io_command( item.output_deact(), toggle, 'SetTextureIndex', '0', ) item.add_io_command( item.output_act(), toggle, 'SetTextureIndex', '1', )
def add_item_inputs( dummy_logic_ents: List[Entity], item: Item, logic_type: InputType, inputs: List[Connection], count_var: str, enable_cmd: Iterable[Output], disable_cmd: Iterable[Output], invert_var: str, spawn_fire: FeatureMode, inv_relay_name: str, ) -> None: """Handle either the primary or secondary inputs to an item.""" item.inst.fixup[count_var] = len(inputs) if len(inputs) == 0: # Special case - spawnfire items with no inputs need to fire # off the outputs. There's no way to control those, so we can just # fire it off. if spawn_fire is FeatureMode.ALWAYS: if item.is_logic: # Logic gates need to trigger their outputs. # Make this item a logic_auto temporarily, then we'll fix them # them up into an OnMapSpawn output properly at the end. item.inst.clear_keys() item.inst['classname'] = 'logic_auto' dummy_logic_ents.append(item.inst) else: is_inverted = conv_bool( conditions.resolve_value( item.inst, invert_var, )) logic_auto = item.inst.map.create_ent( 'logic_auto', origin=item.inst['origin'], spawnflags=1, ) for cmd in (enable_cmd if is_inverted else disable_cmd): logic_auto.add_out( Output( 'OnMapSpawn', 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, only_once=True, )) return # The rest of this function requires at least one input. if logic_type is InputType.DEFAULT: # 'Original' PeTI proxy style inputs. We're not actually using the # proxies though. 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)]: for cmd in input_cmds: inp_item.add_io_command( output, item.inst, conditions.resolve_value(item.inst, cmd.input), conditions.resolve_value(item.inst, cmd.params), 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. # Create the counter for that. # Note that we use the instance name itself for the counter. # This will break if we've got dual inputs, but we're only # using this for laser catchers... # We have to do this so that the name is something that can be # targeted by other items. # TODO: Do this by generating an AND gate proxy-instance... counter = item.inst.map.create_ent( 'math_counter', origin=item.inst['origin'], targetname=item.name, min=0, max=len(inputs) + 1, ) if (count_var is consts.FixupVars.BEE_CONN_COUNT_A or count_var is consts.FixupVars.BEE_CONN_COUNT_B): LOGGER.warning( '{}: Daisychain logic type is ' 'incompatible with dual inputs in item type {}! ' 'This will not work well...', item.name, item.config.id, ) # Use the item type's output, we've overridden the normal one. item.add_io_command( item.config.output_act, counter, 'Add', '1', ) item.add_io_command( item.config.output_deact, counter, 'Subtract', '1', ) for conn in inputs: inp_item = conn.from_item inp_item.add_io_command(inp_item.output_act(), counter, 'Add', '1') inp_item.add_io_command(inp_item.output_deact(), counter, 'Subtract', '1') return is_inverted = conv_bool(conditions.resolve_value( item.inst, invert_var, )) if is_inverted: enable_cmd, disable_cmd = disable_cmd, enable_cmd # Inverted logic items get a short amount of lag, so loops will propagate # over several frames so we don't lock up. if item.inputs and item.outputs: enable_cmd = [ Output( '', out.target, out.input, out.params, out.delay + 0.01, times=out.times, inst_in=out.inst_in, ) for out in enable_cmd ] disable_cmd = [ Output( '', out.target, out.input, out.params, out.delay + 0.01, times=out.times, inst_in=out.inst_in, ) for out in disable_cmd ] needs_counter = len(inputs) > 1 # If this option is enabled, generate additional logic to fire the disable # output after spawn (but only if it's not triggered normally.) # We just use a relay to do this. # User2 is the real enable input, User1 is the real disable input. # The relay allows cancelling the 'disable' output that fires shortly after # spawning. if spawn_fire is not FeatureMode.NEVER: if logic_type.is_logic: # We have to handle gates specially, and make us the instance # so future evaluation applies to this. origin = item.inst['origin'] name = item.name spawn_relay = item.inst spawn_relay.clear_keys() spawn_relay['origin'] = origin spawn_relay['targetname'] = name spawn_relay['classname'] = 'logic_relay' # This needs to be blank so it'll be substituted by the instance # name in enable/disable_cmd. relay_cmd_name = '' else: relay_cmd_name = f'@{item.name}{inv_relay_name}' spawn_relay = item.inst.map.create_ent( classname='logic_relay', targetname=relay_cmd_name, origin=item.inst['origin'], ) if is_inverted: enable_user = '******' disable_user = '******' else: enable_user = '******' disable_user = '******' spawn_relay['spawnflags'] = '0' spawn_relay['startdisabled'] = '0' spawn_relay.add_out( Output('OnTrigger', '!self', 'Fire' + disable_user, only_once=True), Output('OnSpawn', '!self', 'Trigger', delay=0.1), ) for output_name, input_cmds in [('On' + enable_user, enable_cmd), ('On' + disable_user, disable_cmd)]: for cmd in input_cmds: spawn_relay.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, )) # Now overwrite input commands to redirect to the relay. enable_cmd = [ Output('', relay_cmd_name, 'Fire' + enable_user), Output('', relay_cmd_name, 'Disable', only_once=True), ] disable_cmd = [ Output('', relay_cmd_name, 'Fire' + disable_user), Output('', relay_cmd_name, 'Disable', only_once=True), ] # For counters, swap out the input type. if logic_type is InputType.AND_LOGIC: logic_type = InputType.AND elif logic_type is InputType.OR_LOGIC: logic_type = InputType.OR if needs_counter: if logic_type.is_logic: # Logic items are just the counter. The instance is useless, so # remove that from the map. counter_name = item.name item.inst.remove() else: counter_name = item.name + COUNTER_NAME[count_var] counter = item.inst.map.create_ent( classname='math_counter', targetname=counter_name, origin=item.inst['origin'], ) counter['min'] = counter['startvalue'] = counter['StartDisabled'] = 0 counter['max'] = len(inputs) for conn in inputs: inp_item = conn.from_item inp_item.add_io_command(inp_item.output_act(), counter, 'Add', '1') inp_item.add_io_command(inp_item.output_deact(), counter, 'Subtract', '1') if logic_type is InputType.AND: count_on = consts.COUNTER_AND_ON count_off = consts.COUNTER_AND_OFF elif logic_type is InputType.OR: count_on = consts.COUNTER_OR_ON count_off = consts.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. 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)]: 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)]: for cmd in input_cmds: inp_item.add_io_command( output, 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, )
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: Dict[str, connections.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: Dict[connections.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: Dict[connections.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 place_template(inst: Entity) -> None: """Place a template.""" temp_id = inst.fixup.substitute(orig_temp_id) # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(flag, coll, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) force_colour = conf_force_colour if color_var == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[conf_force_colour] # else: False value, no invert. if ang_override is not None: orient = ang_override else: orient = rotation @ Angle.from_str(inst['angles', '0 0 0']) origin = conditions.resolve_offset(inst, offset) # If this var is set, it forces all to be included. if srctools.conv_bool( conditions.resolve_value(inst, visgroup_force_var)): visgroups.update(template.visgroups) elif visgroup_func is not None: visgroups.update( visgroup_func( rand.seed(b'temp', template.id, origin, orient), list(template.visgroups), )) LOGGER.debug('Placing template "{}" at {} with visgroups {}', template.id, origin, visgroups) temp_data = template_brush.import_template( vmf, template, origin, orient, targetname=inst['targetname'], force_type=force_type, add_to_map=True, coll=coll, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, align_bind=align_bind_overlay, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, orient) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ orient temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get(picker_name, None) if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
def res_import_template(vmf: VMF, inst: Entity, res: Property): """Import a template VMF file, retexturing it to match orientation. It will be placed overlapping the given instance. If no block is used, only ID can be specified. Options: - `ID`: The ID of the template to be inserted. Add visgroups to additionally add after a colon, comma-seperated (`temp_id:vis1,vis2`). Either section, or the whole value can be a `$fixup`. - `force`: a space-seperated list of overrides. If 'white' or 'black' is present, the colour of tiles will be overridden. If `invert` is added, white/black tiles will be swapped. If a tile size (`2x2`, `4x4`, `wall`, `special`) is included, all tiles will be switched to that size (if not a floor/ceiling). If 'world' or 'detail' is present, the brush will be forced to that type. - `replace`: A block of template material -> replacement textures. This is case insensitive - any texture here will not be altered otherwise. If the material starts with a `#`, it is instead a list of face IDs separated by spaces. If the result evaluates to "", no change occurs. Both can be $fixups (parsed first). - `bindOverlay`: Bind overlays in this template to the given surface, and bind overlays on a surface to surfaces in this template. The value specifies the offset to the surface, where 0 0 0 is the floor position. It can also be a block of multiple positions. - `keys`/`localkeys`: If set, a brush entity will instead be generated with these values. This overrides force world/detail. Specially-handled keys: - `"origin"`, offset automatically. - `"movedir"` on func_movelinear - set a normal surrounded by `<>`, this gets replaced with angles. - `colorVar`: If this fixup var is set to `white` or `black`, that colour will be forced. If the value is `<editor>`, the colour will be chosen based on the color of the surface for ItemButtonFloor, funnels or entry/exit frames. - `invertVar`: If this fixup value is true, tile colour will be swapped to the opposite of the current force option. This applies after colorVar. - `visgroup`: Sets how visgrouped parts are handled. Several values are possible: - A property block: Each name should match a visgroup, and the value should be a block of flags that if true enables that group. - 'none' (default): All extra groups are ignored. - 'choose': One group is chosen randomly. - a number: The percentage chance for each visgroup to be added. - `visgroup_force_var`: If set and True, visgroup is ignored and all groups are added. - `pickerVars`: If this is set, the results of colorpickers can be read out of the template. The key is the name of the picker, the value is the fixup name to write to. The output is either 'white', 'black' or ''. - `outputs`: Add outputs to the brush ent. Syntax is like VMFs, and all names are local to the instance. - `senseOffset`: If set, colorpickers and tilesetters will be treated as being offset by this amount. """ ( orig_temp_id, replace_tex, force_colour, force_grid, force_type, surf_cat, bind_tile_pos, invert_var, color_var, visgroup_func, visgroup_force_var, visgroup_instvars, key_block, picker_vars, outputs, sense_offset, ) = res.value temp_id = inst.fixup.substitute(orig_temp_id) if srctools.conv_bool(conditions.resolve_value(inst, visgroup_force_var)): def visgroup_func(group): """Use all the groups.""" yield from group # Special case - if blank, just do nothing silently. if not temp_id: return temp_name, visgroups = template_brush.parse_temp_name(temp_id) try: template = template_brush.get_template(temp_name) except template_brush.InvalidTemplateName: # If we did lookup, display both forms. if temp_id != orig_temp_id: LOGGER.warning('{} -> "{}" is not a valid template!', orig_temp_id, temp_name) else: LOGGER.warning('"{}" is not a valid template!', temp_name) # We don't want an error, just quit. return for vis_flag_block in visgroup_instvars: if all( conditions.check_flag(vmf, flag, inst) for flag in vis_flag_block): visgroups.add(vis_flag_block.real_name) if color_var.casefold() == '<editor>': # Check traits for the colour it should be. traits = instance_traits.get(inst) if 'white' in traits: force_colour = texturing.Portalable.white elif 'black' in traits: force_colour = texturing.Portalable.black else: LOGGER.warning( '"{}": Instance "{}" ' "isn't one with inherent color!", temp_id, inst['file'], ) elif color_var: color_val = conditions.resolve_value(inst, color_var).casefold() if color_val == 'white': force_colour = texturing.Portalable.white elif color_val == 'black': force_colour = texturing.Portalable.black # else: no color var if srctools.conv_bool(conditions.resolve_value(inst, invert_var)): force_colour = template_brush.TEMP_COLOUR_INVERT[force_colour] # else: False value, no invert. origin = Vec.from_str(inst['origin']) angles = Angle.from_str(inst['angles', '0 0 0']) temp_data = template_brush.import_template( vmf, template, origin, angles, targetname=inst['targetname', ''], force_type=force_type, visgroup_choose=visgroup_func, add_to_map=True, additional_visgroups=visgroups, bind_tile_pos=bind_tile_pos, ) if key_block is not None: conditions.set_ent_keys(temp_data.detail, inst, key_block) br_origin = Vec.from_str(key_block.find_key('keys')['origin']) br_origin.localise(origin, angles) temp_data.detail['origin'] = br_origin move_dir = temp_data.detail['movedir', ''] if move_dir.startswith('<') and move_dir.endswith('>'): move_dir = Vec.from_str(move_dir) @ angles temp_data.detail['movedir'] = move_dir.to_angle() for out in outputs: # type: Output out = out.copy() out.target = conditions.local_name(inst, out.target) temp_data.detail.add_out(out) template_brush.retexture_template( temp_data, origin, inst.fixup, replace_tex, force_colour, force_grid, surf_cat, sense_offset, ) for picker_name, picker_var in picker_vars: picker_val = temp_data.picker_results.get( picker_name, None, ) # type: Optional[texturing.Portalable] if picker_val is not None: inst.fixup[picker_var] = picker_val.value else: inst.fixup[picker_var] = ''
def gen_item_outputs(vmf: VMF) -> None: """Create outputs for all items with connections. This performs an optimization pass over items with outputs to remove redundancy, then applies all the outputs to the instances. Before this, connection count and inversion values are not valid. After this point, items may not have connections altered. """ LOGGER.info('Generating item IO...') pan_switching_check = options.get(PanelSwitchingStyle, 'ind_pan_check_switching') pan_switching_timer = options.get(PanelSwitchingStyle, 'ind_pan_timer_switching') pan_check_type = ITEM_TYPES['item_indicator_panel'] pan_timer_type = ITEM_TYPES['item_indicator_panel_timer'] auto_logic = [] # Apply input A/B types to connections. # After here, all connections are primary or secondary only. for item in ITEMS.values(): for conn in item.outputs: # If not a dual item, it's primary. if conn.to_item.config.input_type is not InputType.DUAL: conn.type = ConnType.PRIMARY continue # If already set, that is the priority. if conn.type is not ConnType.DEFAULT: continue # Our item set the type of outputs. if item.config.output_type is not ConnType.DEFAULT: conn.type = item.config.output_type else: # Use the affinity of the target. conn.type = conn.to_item.config.default_dual do_item_optimisation(vmf) # We go 'backwards', creating all the inputs for each item. # That way we can change behaviour based on item counts. for item in ITEMS.values(): if item.config is None: continue # Try to add the locking IO. add_locking(item) # Check we actually have timers, and that we want the relay. if item.timer is not None and ( item.config.timer_sound_pos is not None or item.config.timer_done_cmd ): has_sound = item.config.force_timer_sound or len(item.ind_panels) > 0 add_timer_relay(item, has_sound) # Add outputs for antlines. if item.antlines or item.ind_panels: if item.timer is None: add_item_indicators(item, pan_switching_check, pan_check_type) else: add_item_indicators(item, pan_switching_timer, pan_timer_type) # Special case - spawnfire items with no inputs need to fire # off the outputs. There's no way to control those, so we can just # fire it off. if not item.inputs and item.config.spawn_fire is FeatureMode.ALWAYS: if item.is_logic: # Logic gates need to trigger their outputs. # Make a logic_auto temporarily for this to collect the # outputs we need. item.inst.clear_keys() item.inst['classname'] = 'logic_auto' auto_logic.append(item.inst) else: is_inverted = conv_bool(conditions.resolve_value( item.inst, item.config.invert_var, )) logic_auto = vmf.create_ent( 'logic_auto', origin=item.inst['origin'], spawnflags=1, ) for cmd in (item.enable_cmd if is_inverted else item.disable_cmd): logic_auto.add_out( Output( 'OnMapSpawn', 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, only_once=True, ) ) if item.config.input_type is InputType.DUAL: prim_inputs = [ conn for conn in item.inputs if conn.type is ConnType.PRIMARY or conn.type is ConnType.BOTH ] sec_inputs = [ conn for conn in item.inputs if conn.type is ConnType.SECONDARY or conn.type is ConnType.BOTH ] add_item_inputs( item, InputType.AND, prim_inputs, consts.FixupVars.BEE_CONN_COUNT_A, item.enable_cmd, item.disable_cmd, item.config.invert_var, ) add_item_inputs( item, InputType.AND, sec_inputs, consts.FixupVars.BEE_CONN_COUNT_B, item.sec_enable_cmd, item.sec_disable_cmd, item.config.sec_invert_var, ) else: add_item_inputs( item, item.config.input_type, list(item.inputs), consts.FixupVars.CONN_COUNT, item.enable_cmd, item.disable_cmd, item.config.invert_var, ) # Check/cross instances sometimes don't match the kind of timer delay. # We also might want to swap them out. panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) for item in ITEMS.values(): desired_panel_inst = panel_check if item.timer is None else panel_timer for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup[consts.FixupVars.TIM_ENABLED] = item.timer is not None logic_auto = vmf.create_ent( 'logic_auto', origin=options.get(Vec, 'global_ents_loc') ) for ent in auto_logic: # Condense all these together now. # User2 is the one that enables the target. ent.remove() for out in ent.outputs: if out.output == 'OnUser2': out.output = 'OnMapSpawn' logic_auto.add_out(out) out.only_once = True LOGGER.info('Item IO generated.')
def res_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]: """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 the Piston Platform's handles. - `rotation`: Rotate the instance by this amount. - `angles`: If set, overrides `rotation` and the instance angles entirely. - `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)]) if 'angles' in res: angles = Angle.from_str(res['angles']) if 'rotation' in res: LOGGER.warning('"angles" option overrides "rotation"!') else: angles = Angle.from_str(res['rotation', '0 0 0']) angles @= Angle.from_str(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 None overlay_inst = vmf.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angles, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6) 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) @ orient + 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: set[tuple[int, int]] | None 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) @ orient + 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 = inst.fixup.substitute(props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( inst.fixup.substitute(props['nodraw'], allow_invert=True)) if 'seal' in props: panel.seal = srctools.conv_bool( inst.fixup.substitute(props['seal'], allow_invert=True)) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( inst.fixup.substitute(props['move_bullseye'], allow_invert=True)) 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[Entity | None] = {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']), Angle.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 modify_platform(inst: Entity) -> None: """Modify each platform.""" min_pos = inst.fixup.int(FixupVars.PIST_BTM) max_pos = inst.fixup.int(FixupVars.PIST_TOP) start_up = inst.fixup.bool(FixupVars.PIST_IS_UP) # Allow doing variable lookups here. visgroup_names = [ conditions.resolve_value(inst, fname) for fname in conf_visgroup_names ] if len(ITEMS[inst['targetname']].inputs) == 0: # No inputs. Check for the 'auto' var if applicable. if automatic_var and inst.fixup.bool(automatic_var): pass # The item is automatically moving, so we generate the dynamics. else: # It's static, we just make that and exit. position = max_pos if start_up else min_pos inst.fixup[FixupVars.PIST_BTM] = position inst.fixup[FixupVars.PIST_TOP] = position static_inst = inst.copy() vmf.add_ent(static_inst) static_inst['file'] = fname = inst_filenames['fullstatic_' + str(position)] conditions.ALL_INST.add(fname) return init_script = 'SPAWN_UP <- {}'.format('true' if start_up else 'false') if snd_start and snd_stop: packing.pack_files(vmf, snd_start, snd_stop, file_type='sound') init_script += '; START_SND <- `{}`; STOP_SND <- `{}`'.format( snd_start, snd_stop) elif snd_start: packing.pack_files(vmf, snd_start, file_type='sound') init_script += '; START_SND <- `{}`'.format(snd_start) elif snd_stop: packing.pack_files(vmf, snd_stop, file_type='sound') init_script += '; STOP_SND <- `{}`'.format(snd_stop) script_ent = vmf.create_ent( classname='info_target', targetname=conditions.local_name(inst, 'script'), vscripts='BEE2/piston/common.nut', vscript_init_code=init_script, origin=inst['origin'], ) if has_dn_fizz: script_ent['thinkfunction'] = 'FizzThink' if start_up: st_pos, end_pos = max_pos, min_pos else: st_pos, end_pos = min_pos, max_pos script_ent.add_out( Output('OnUser1', '!self', 'RunScriptCode', f'moveto({st_pos})'), Output('OnUser2', '!self', 'RunScriptCode', f'moveto({end_pos})'), ) origin = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) off = orient.up(128) move_ang = off.to_angle() # Index -> func_movelinear. pistons: dict[int, Entity] = {} static_ent = vmf.create_ent('func_brush', origin=origin) for pist_ind in [1, 2, 3, 4]: pist_ent = inst.copy() vmf.add_ent(pist_ent) if pist_ind <= min_pos: # It's below the lowest position, so it can be static. pist_ent['file'] = fname = inst_filenames['static_' + str(pist_ind)] pist_ent['origin'] = brush_pos = origin + pist_ind * off temp_targ = static_ent else: # It's a moving component. pist_ent['file'] = fname = inst_filenames['dynamic_' + str(pist_ind)] if pist_ind > max_pos: # It's 'after' the highest position, so it never extends. # So simplify by merging those all. # The max pos was evaluated earlier, so this must be set. temp_targ = pistons[max_pos] if start_up: pist_ent['origin'] = brush_pos = origin + max_pos * off else: pist_ent['origin'] = brush_pos = origin + min_pos * off pist_ent.fixup['$parent'] = 'pist' + str(max_pos) else: # It's actually a moving piston. if start_up: brush_pos = origin + pist_ind * off else: brush_pos = origin + min_pos * off pist_ent['origin'] = brush_pos pist_ent.fixup['$parent'] = 'pist' + str(pist_ind) pistons[pist_ind] = temp_targ = vmf.create_ent( 'func_movelinear', targetname=conditions.local_name( pist_ent, f'pist{pist_ind}'), origin=brush_pos - off, movedir=move_ang, startposition=start_up, movedistance=128, speed=150, ) if pist_ind - 1 in pistons: pistons[pist_ind][ 'parentname'] = conditions.local_name( pist_ent, f'pist{pist_ind - 1}', ) if fname: conditions.ALL_INST.add(fname.casefold()) else: # No actual instance, remove. pist_ent.remove() temp_result = template_brush.import_template( vmf, template, brush_pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, additional_visgroups={visgroup_names[pist_ind - 1]}, ) temp_targ.solids.extend(temp_result.world) template_brush.retexture_template( temp_result, origin, pist_ent.fixup, generator=GenCat.PANEL, ) # Associate any set panel with the same entity, if it's present. tile_pos = origin - orient.up(128) panel: Optional[Panel] = None try: tiledef = TILES[tile_pos.as_tuple(), off.norm().as_tuple()] except KeyError: pass else: for panel in tiledef.panels: if panel.same_item(inst): break else: # Checked all of them. panel = None if panel is not None: if panel.brush_ent in vmf.entities and not panel.brush_ent.solids: panel.brush_ent.remove() panel.brush_ent = pistons[max(pistons.keys())] panel.offset = st_pos * off if not static_ent.solids and (panel is None or panel.brush_ent is not static_ent): static_ent.remove() if snd_loop: script_ent['classname'] = 'ambient_generic' script_ent['message'] = snd_loop script_ent['health'] = 10 # Volume script_ent['pitch'] = 100 script_ent['spawnflags'] = 16 # Start silent, looped. script_ent['radius'] = 1024 if source_ent: # Parent is irrelevant for actual entity locations, but it # survives for the script to read. script_ent['SourceEntityName'] = script_ent[ 'parentname'] = conditions.local_name(inst, source_ent)
def res_water_splash(vmf: VMF, inst: Entity, res: Property) -> None: """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, Vec, str, str pos1 = pos1.copy() splash_pos = pos1.copy() if calc_type == 'track_platform': lin_off = srctools.conv_int(inst.fixup['$travel_distance']) travel_ang = Angle.from_str(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) @ travel_ang pos2 = Vec(x=lin_off) @ travel_ang + 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 = Angle.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)) if pos1.z == pos2.z: # Flat - this won't do anything... return for pos in check_pos: grid_pos = pos // 128 * 128 grid_pos += (64, 64, 64) block = BLOCK_POS['world':pos] if block.is_goo: break else: return # Not in goo at all water_pos = grid_pos + (0, 0, 32) # Check if both positions are above or below the water.. # that means it won't ever trigger. 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_antlaser(vmf: VMF, res: Property) -> object: """The condition to generate AntLasers and Antline Corners. This is executed once to modify all instances. """ conf_inst_corner = instanceLocs.resolve('<item_bee2_antline_corner>', silent=True) conf_inst_laser = 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', or_blank=True) glow_conf = res.find_key('GlowKeys', or_blank=True) cable_conf = res.find_key('CableKeys', or_blank=True) 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: dict[str, Node] = {} for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() name = inst['targetname'] if filename in conf_inst_laser: node_type = NodeType.LASER elif filename in conf_inst_corner: node_type = NodeType.CORNER else: continue try: # Remove the item - it's no longer going to exist after # we're done. item = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None pos = Vec.from_str(inst['origin']) orient = Matrix.from_angle(Angle.from_str(inst['angles'])) if node_type is NodeType.CORNER: timer_delay = item.inst.fixup.int('$timer_delay') # We treat inf, 1, 2 and 3 as the same, to get around the 1 and 2 not # being selectable issue. pos = CORNER_POS[max(0, timer_delay - 3) % 8] @ orient + pos nodes[name] = Node(node_type, inst, item, pos, orient) 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: list[Group] = [] 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, start.type) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node.is_grouped = True for conn in list(node.item.outputs): neighbour = conn.to_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.from_item = group.item has_output = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neigh_node})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.item.transfer_antlines(group.item) else: node.item.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.item.inputs): neighbour = conn.from_item neigh_node = nodes.get(neighbour.name, None) todo.discard(neigh_node) if neigh_node is None or neigh_node.type is not node.type: # Not a node or different item type, it must therefore # be a target of our logic. conn.to_item = group.item node.had_input = True continue elif not neigh_node.is_grouped: # Another node. group.nodes.append(neigh_node) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neigh_node, 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 item name to use for our group. base_name = group.nodes[0].item.name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] if group.type is NodeType.LASER: for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) if group.type is NodeType.LASER and 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) # Node -> index for targetnames. indexes: dict[Node, int] = {} # For antline corners, the antline segments. segments: list[antlines.Segment] = [] # frozenset[Node] unpacking isn't clear. node_a: Node node_b: Node if group.type is NodeType.CORNER: for node_a, node_b in group.links: # Place a straight antline between each connected node. # If on the same plane, we only need one. If not, we need to # do one for each plane it's in. offset = node_b.pos - node_a.pos up_a = node_a.orient.up() up_b = node_b.orient.up() plane_a = Vec.dot(node_a.pos, up_a) plane_b = Vec.dot(node_b.pos, up_b) if Vec.dot(up_a, up_b) > 0.9: if abs(plane_a - plane_b) > 1e-6: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are on different planes', node_a.item.name, node_b.item.name, ) continue u = node_a.orient.left() v = node_a.orient.forward() # Which are we aligned to? if abs(Vec.dot(offset, u)) < 1e-6 or abs(Vec.dot( offset, v)) < 1e-6: forward = offset.norm() group.add_ant_straight( up_a, node_a.pos + 8.0 * forward, node_b.pos - 8.0 * forward, ) else: LOGGER.warning( 'Antline corners "{}" - "{}" ' 'are not directly aligned', node_a.item.name, node_b.item.name, ) else: # We expect them be aligned to each other. side = Vec.cross(up_a, up_b) if abs(Vec.dot(side, offset)) < 1e-6: mid1 = node_a.pos + Vec.dot(offset, up_b) * up_b mid2 = node_b.pos - Vec.dot(offset, up_a) * up_a if mid1 != mid2: LOGGER.warning( 'Midpoint mismatch: {} != {} for "{}" - "{}"', mid1, mid2, node_a.item.name, node_b.item.name, ) group.add_ant_straight( up_a, node_a.pos + 8.0 * (mid1 - node_a.pos).norm(), mid1, ) group.add_ant_straight( up_b, node_b.pos + 8.0 * (mid2 - node_b.pos).norm(), mid2, ) # 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: dict[Node, Union[Entity, str]] = {} for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.item.name = base_name if group.type is NodeType.CORNER: node.inst.remove() # Figure out whether we want a corner at this point, or # just a regular dot. If a non-node input was provided it's # always a corner. Otherwise it's one if there's an L, T or X # junction. use_corner = True norm = node.orient.up().as_tuple() if not node.had_input: neighbors = [ mag * direction for direction in [ node.orient.forward(), node.orient.left(), ] for mag in [-8.0, 8.0] if ((node.pos + mag * direction).as_tuple(), norm) in group.ant_seg ] if len(neighbors) == 2: [off1, off2] = neighbors if Vec.dot(off1, off2) < -0.99: # ---o---, merge together. The endpoints we want # are the other ends of the two segments. group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + off1), group.rem_ant_straight(norm, node.pos + off2), ) use_corner = False elif len(neighbors) == 1: # o-----, merge. [offset] = neighbors group.add_ant_straight( node.orient.up(), group.rem_ant_straight(norm, node.pos + offset), node.pos - offset, ) use_corner = False if use_corner: segments.append( antlines.Segment( antlines.SegType.CORNER, round(node.orient.up(), 3), Vec(node.pos), Vec(node.pos), )) elif group.type is NodeType.LASER: sprite_pos = node.pos + conf_glow_height @ node.orient 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 = node.pos + conf_las_start @ node.orient 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 segments += set(group.ant_seg.values()) if group.type is NodeType.CORNER and segments: group.item.antlines.add( antlines.Antline(group.item.name + '_antline', segments)) if group.type is NodeType.LASER and 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.pos 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 if group.type is NodeType.LASER and cable_conf: build_cables( vmf, group, cable_points, base_name, beam_conf, conf_rope_off, ) return conditions.RES_EXHAUSTED