def global_input( vmf: VMF, pos: Union[Vec, str], output: Output, relay_name: str=None, ): """Create a global input, either from a relay or logic_auto. The position is used to place the relay if this is the first time. """ try: glob_ent = GLOBAL_INPUT_ENTS[relay_name] except KeyError: if relay_name == '': glob_ent = GLOBAL_INPUT_ENTS[''] = vmf.create_ent( classname='logic_auto', spawnflags='1', # Remove on fire origin=pos, ) else: glob_ent = GLOBAL_INPUT_ENTS[relay_name] = vmf.create_ent( classname='logic_relay', targetname=relay_name, origin=pos, ) if not relay_name: output.output = 'OnMapSpawn' output.only_once = True output.comma_sep = False glob_ent.add_out(output)
def res_linked_cube(inst: Entity, res: Property): """Marks a cube to link it to a dropper. This assumes some things about the item. """ time = inst.fixup.int('$timer_delay') # Portal 2 bug - when loading existing maps, timers are set to 3... if not (3 < time <= 30): # Infinite or 3-second - this behaviour is disabled.. return if time in LINKED_CUBES: raise Exception( 'Two cubes have the same ' '"linkage" value set ({})!'.format( time, ) ) resp_out_name, resp_out = Output.parse_name(res.value) LINKED_CUBES[time] = ( inst, inst.fixup['$cube_type'], resp_out_name, resp_out, )
def res_global_input_setup(res: Property): if res.has_children(): name = res['name', ''] inp_name, inp_command = Output.parse_name(res['input']) return name, Output( out=res['output', 'OnTrigger'], targ=res['target', ''], inp=inp_command, inst_in=inp_name, delay=srctools.conv_float(res['delay', '']), param=res['param', ''], ) else: out = Output.parse(res) out.output = '' # Don't need to store GlobalInput... return '', out
def res_locking_input(inst: Entity, res: Property): """Executed on the input item, and evaluates to True if successful. The parameter is an `instance:name;Input` value, which resets the item. This must be executed after the `MarkLocking` results have run. """ from vbsp import IND_ITEM_NAMES, IND_PANEL_NAMES, VMF in_name, in_inp = Output.parse_name(res.value) targets = { out.target for out in inst.outputs # Skip toggle or indicator panel items. if out.target not in IND_ITEM_NAMES } # No outputs, or 2+ - we can't convert in that case if len(targets) != 1: return False target, = targets try: targ_inst, targ_out_name, targ_out, out_relay = LOCKABLE_ITEMS[target] except KeyError: # Some other item... return False # Remove the indicator panel instances. ind_panels = { out.target for out in inst.outputs # Skip toggle or indicator panel items. if out.target in IND_PANEL_NAMES } for pan_inst in VMF.by_class['func_instance']: if pan_inst['targetname'] in ind_panels: pan_inst.remove() # Add an output pointing in the opposite direction. if out_relay is None: targ_inst.add_out(Output( out=targ_out, inst_out=targ_out_name, targ=inst['targetname'], inp=in_inp, inst_in=in_name, )) else: from conditions.instances import add_global_input add_global_input( inst, in_name, in_inp, rl_name=out_relay, output=targ_out, ) return True
def res_locking_output(inst: Entity, res: Property): """Marks an output item for locked connections. The parameter is an `instance:name;Output` value, which is fired when the item resets. This must be executed before `LockingIO`. This only applies if `$connectioncount` is 1. """ # Items with more than one connection have AND logic in the mix - it makes # it unsafe to lock the input item. if inst.fixup['$connectioncount'] != '1': return if res.has_children(): name, output = Output.parse_name(res['output']) relay_name = res['rl_name', None] else: name, output = Output.parse_name(res.value) relay_name = None LOCKABLE_ITEMS[inst['targetname']] = inst, name, output, relay_name
def parse(cls, conf: Property) -> 'FizzlerBrush': """Parse from a config file.""" if 'side_color' in conf: side_color = conf.vec('side_color') else: side_color = None outputs = [ Output.parse(prop) for prop in conf.find_children('Outputs') ] textures = {} for group in TexGroup: textures[group] = conf['tex_' + group.value, None] keys = { prop.name: prop.value for prop in conf.find_children('keys') } local_keys = { prop.name: prop.value for prop in conf.find_children('localkeys') } if 'classname' not in keys: raise ValueError( 'Fizzler Brush "{}" does not have a classname!'.format( conf['name'], ) ) return FizzlerBrush( name=conf['name'], textures=textures, keys=keys, local_keys=local_keys, outputs=outputs, thickness=conf.float('thickness', 2.0), stretch_center=conf.bool('stretch_center', True), side_color=side_color, singular=conf.bool('singular'), mat_mod_name=conf['mat_mod_name', None], mat_mod_var=conf['mat_mod_var', None], set_axis_var=conf.bool('set_axis_var'), )
def res_timed_relay_setup(res: Property): var = res['variable', consts.FixupVars.TIM_DELAY] name = res['targetname'] disabled = res['disabled', '0'] flags = res['spawnflags', '0'] final_outs = [ Output.parse(subprop) for prop in res.find_all('FinalOutputs') for subprop in prop ] rep_outs = [ Output.parse(subprop) for prop in res.find_all('RepOutputs') for subprop in prop ] # Never use the comma seperator in the final output for consistency. for out in itertools.chain(rep_outs, final_outs): out.comma_sep = False return var, name, disabled, flags, final_outs, rep_outs
def res_checkpoint_trigger(inst: Entity, res: Property): """Generate a trigger underneath coop checkpoint items """ if vbsp.GAME_MODE == 'SP': # We can't have a respawn dropper in singleplayer. # Not generating the trigger means it's not going to # do anything. return pos = brushLoc.POS.raycast_world( Vec.from_str(inst['origin']), direction=(0, 0, -1), ) bbox_min = pos - (192, 192, 64) bbox_max = pos + (192, 192, 64) # Find triggers already placed next to ours, and # merge with them if that's the case for offset in CHECKPOINT_NEIGHBOURS: near_pos = pos + offset try: trig = CHECKPOINT_TRIG[near_pos.as_tuple()] break except KeyError: pass else: # None found, make one. trig = inst.map.create_ent( classname='trigger_playerteam', origin=pos, ) trig.solids = [] CHECKPOINT_TRIG[pos.as_tuple()] = trig trig.solids.append( inst.map.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid) for prop in res: out = Output.parse(prop) out.target = conditions.local_name(inst, out.target) trig.add_out(out)
def res_checkpoint_trigger(inst: Entity, res: Property): """Generate a trigger underneath coop checkpoint items """ if vbsp.GAME_MODE == 'SP': # We can't have a respawn dropper in singleplayer. # Not generating the trigger means it's not going to # do anything. return pos = brushLoc.POS.raycast_world( Vec.from_str(inst['origin']), direction=(0, 0, -1), ) bbox_min = pos - (192, 192, 64) bbox_max = pos + (192, 192, 64) # Find triggers already placed next to ours, and # merge with them if that's the case for offset in CHECKPOINT_NEIGHBOURS: near_pos = pos + offset try: trig = CHECKPOINT_TRIG[near_pos.as_tuple()] break except KeyError: pass else: # None found, make one. trig = inst.map.create_ent( classname='trigger_playerteam', origin=pos, ) trig.solids = [] CHECKPOINT_TRIG[pos.as_tuple()] = trig trig.solids.append(inst.map.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid) for prop in res: out = Output.parse(prop) out.target = conditions.local_name(inst, out.target) trig.add_out(out)
def comp_trigger_coop(ctx: Context): """Creates a trigger which only activates with both players.""" for trig in ctx.vmf.by_class['comp_trigger_coop']: trig['classname'] = 'trigger_playerteam' trig['target_team'] = 0 only_once = conv_bool(trig['trigger_once']) trig['trigger_once'] = 0 trig_name = trig['targetname'] if not trig_name: # Give it something unique trig['targetname'] = trig_name = '_comp_trigger_coop_' + str( trig['hammer_id']) man_name = trig_name + '_man' manager = ctx.vmf.create_ent( classname='logic_coop_manager', origin=trig['origin'], targetname=man_name, # Should make it die if the trigger does. parentname=trig_name, ) for out in list(trig.outputs): folded_out = out.output.casefold() if folded_out == 'onstarttouchboth': out.output = 'OnChangeToAllTrue' elif folded_out == 'onendtouchboth': out.output = 'OnChangeToAnyFalse' else: continue trig.outputs.remove(out) manager.add_out(out) trig.add_out( Output('OnStartTouchBluePlayer', man_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', man_name, 'SetStateBrue'), Output('OnEndTouchBluePlayer', man_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', man_name, 'SetStateBFalse'), ) if only_once: manager.add_out( Output('OnChangeToAllTrue', man_name, 'Kill'), Output('OnChangeToAllTrue', trig_name, 'Kill'), ) # Only keep OnChangeToAllTrue outputs, and remove # them once they've fired. for out in list(manager): if out.output.casefold() == 'onchangetoalltrue': out.only_once = True else: manager.outputs.remove(out)
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 __init__(self, ent: Entity): """Convert the entity to have the right logic.""" self.scanner = None self.persist_tv = conv_bool(ent.keys.pop('persist_tv', False)) pos = Vec.from_str(ent['origin']) for prop in ent.map.by_class['prop_dynamic']: if (Vec.from_str(prop['origin']) - pos).mag_sq() > 64**2: continue model = prop['model'].casefold().replace('\\', '/') # Allow spelling this correctly, if you're not Valve. if 'vacum_scanner_tv' in model or 'vacuum_scanner_tv' in model: self.scanner = prop prop.make_unique('_vac_scanner') elif 'vacum_scanner_motion' in model or 'vacuum_scanner_motion' in model: prop.make_unique('_vac_scanner') ent.add_out(Output(self.pass_out_name, prop, "SetAnimation", "scan01")) super(Straight, self).__init__(ent)
def res_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, inst.fixup.substitute(res['name'])), 'RunScriptCode', param='{} <- {}'.format( res['var'], inst.fixup.substitute(res['value']), ), ), )
def parse(cls, conf: Property) -> 'FizzlerBrush': """Parse from a config file.""" if 'side_color' in conf: side_color = conf.vec('side_color') else: side_color = None outputs = [ Output.parse(prop) for prop in conf.find_children('Outputs') ] textures = {} for group in TexGroup: textures[group] = conf['tex_' + group.value, None] keys = {prop.name: prop.value for prop in conf.find_children('keys')} local_keys = { prop.name: prop.value for prop in conf.find_children('localkeys') } if 'classname' not in keys: raise ValueError( 'Fizzler Brush "{}" does not have a classname!'.format( conf['name'], )) return FizzlerBrush( name=conf['name'], textures=textures, keys=keys, local_keys=local_keys, outputs=outputs, thickness=conf.float('thickness', 2.0), stretch_center=conf.bool('stretch_center', True), side_color=side_color, singular=conf.bool('singular'), mat_mod_name=conf['mat_mod_name', None], mat_mod_var=conf['mat_mod_var', None], set_axis_var=conf.bool('set_axis_var'), )
def res_append_io_type(res: Property) -> Callable[[Entity], None]: """Append additional outputs to an item's connections, which are fired when inputs change. Must be done before priority level -250. This has the same format of the editoritems BEE2 block, but only accepts any number of the following: - `enable_cmd` - `disable_cmd` - `sec_enable_cmd` - `sec_disable_cmd` """ prop_lists: dict[str, list[Output]] = { name: [] for name in ['enable_cmd', 'disable_cmd', 'sec_enable_cmd', 'sec_disable_cmd'] } for prop in res: try: lst = prop_lists[prop.name] except KeyError: raise ValueError( f'Unknown input command type "{prop.real_name}"!') from None prop.name = '' # Discard this from the output. lst.append(Output.parse(prop)) # Collect into tuples for appending later, discard any blanks. prop_tups = [(name, tuple(out_list)) for name, out_list in prop_lists.items() if out_list] LOGGER.info('Append inputs: {}', prop_tups) def append_to(inst: Entity) -> None: """Append inputs to the item.""" try: item = connections.ITEMS[inst['targetname']] except KeyError: raise ValueError('No item with name "{}"!'.format( inst['targetname'])) # Assign item.enable_cmd += out_tup, for all of them. for name, out_tup in prop_tups: setattr(item, name, getattr(item, name) + out_tup) return append_to
def res_linked_cube(inst: Entity, res: Property): """Marks a cube to link it to a dropper. This assumes some things about the item. """ time = inst.fixup.int('$timer_delay') # Portal 2 bug - when loading existing maps, timers are set to 3... if not (3 < time <= 30): # Infinite or 3-second - this behaviour is disabled.. return if time in LINKED_CUBES: raise Exception('Two cubes have the same ' '"linkage" value set ({})!'.format(time, )) resp_out_name, resp_out = Output.parse_name(res.value) LINKED_CUBES[time] = ( inst, resp_out_name, resp_out, )
def add( vmf: VMF, loc: Vec, conf: Property, voice_attr: Dict[str, str], is_sp: bool, ) -> None: """Add music to the map.""" LOGGER.info("Adding Music...") # These values are exported by the BEE2 app, indicating the # options on the music item. inst = options.get(str, 'music_instance') snd_length = options.get(int, 'music_looplen') # Don't add our logic if an instance was provided. # If this settings is set, we have a music config. if conf and not inst: music = vmf.create_ent( classname='ambient_generic', spawnflags='17', # Looping, Infinite Range, Starts Silent targetname='@music', origin=loc, message='music.BEE2', health='10', # Volume ) music_start = vmf.create_ent( classname='logic_relay', spawnflags='0', targetname='@music_start', origin=loc + (-16, 0, -16), ) music_stop = vmf.create_ent( classname='logic_relay', spawnflags='0', targetname='@music_stop', origin=loc + (16, 0, -16), ) music_stop.add_out( Output('OnTrigger', music, 'StopSound'), Output('OnTrigger', music, 'Volume', '0'), ) # In SinglePlayer, music gets killed during reload, # so we need to restart it. # If snd_length is set, we have a non-loopable MP3 # and want to re-trigger it after the time elapses, to simulate # looping. # In either case, we need @music_restart to do that safely. if is_sp or snd_length > 0: music_restart = vmf.create_ent( classname='logic_relay', spawnflags='2', # Allow fast retrigger. targetname='@music_restart', StartDisabled='1', origin=loc + (0, 0, -16), ) music_start.add_out( Output('OnTrigger', music_restart, 'Enable'), Output('OnTrigger', music_restart, 'Trigger', delay=0.01), ) music_stop.add_out( Output('OnTrigger', music_restart, 'Disable'), Output('OnTrigger', music_restart, 'CancelPending'), ) music_restart.add_out( Output('OnTrigger', music, 'StopSound'), Output('OnTrigger', music, 'Volume', '0'), Output('OnTrigger', music, 'Volume', '10', delay=0.1), Output('OnTrigger', music, 'PlaySound', delay=0.1), ) if is_sp == 'SP': # Trigger on level loads. vmf.create_ent( classname='logic_auto', origin=loc + (0, 0, 16), spawnflags='0', # Don't remove after fire globalstate='', ).add_out( Output('OnLoadGame', music_restart, 'CancelPending'), Output('OnLoadGame', music_restart, 'Trigger', delay=0.01), ) if snd_length > 0: # Re-trigger after the music duration. music_restart.add_out( Output('OnTrigger', '!self', 'Trigger', delay=snd_length) ) # Set to non-looping, so re-playing will restart it correctly. music['spawnflags'] = '49' else: # The music track never needs to have repeating managed, # just directly trigger. music_start.add_out( Output('OnTrigger', music, 'PlaySound'), Output('OnTrigger', music, 'Volume', '10'), ) # Add the ents for the config itself. # If the items aren't in the map, we can skip adding them. # Speed-gel sounds also play when flinging, so keep it always. funnel = conf.find_key('tbeam', or_blank=True) bounce = conf.find_key('bouncegel', or_blank=True) make_channel_conf( vmf, loc, Channel.BASE, conf.find_key('base', or_blank=True).as_array(), ) make_channel_conf( vmf, loc, Channel.SPEED, conf.find_key('speedgel', or_blank=True).as_array(), ) if 'funnel' in voice_attr or 'excursionfunnel' in voice_attr: make_channel_conf( vmf, loc, Channel.TBEAM, funnel.as_array(), conf.bool('sync_funnel'), ) if 'bouncegel' in voice_attr or 'bluegel' in voice_attr: make_channel_conf( vmf, loc, Channel.BOUNCE, bounce.as_array(), ) packfiles = conf.find_key('pack', or_blank=True).as_array() if packfiles: packer = vmf.create_ent('comp_pack', origin=loc) for i, fname in enumerate(packfiles, 1): packer[f'generic{i:02}'] = fname if inst: # We assume the instance is setup correct. vmf.create_ent( classname='func_instance', targetname='music', angles='0 0 0', origin=loc, file=inst, fixup_style='0', )
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_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_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_linked_cube_dropper(drp_inst: Entity, res: Property): """Link a cube and dropper together, to preplace the cube at a location.""" time = drp_inst.fixup.int('$timer_delay') # Portal 2 bug - when loading existing maps, timers are set to 3... if not (3 < time <= 30): # Infinite or 3-second - this behaviour is disabled.. return try: cube_inst, cube_type, resp_out_name, resp_out = LINKED_CUBES[time] except KeyError: raise Exception('Unknown cube "linkage" value ({}) in dropper!'.format( time, )) # Force the dropper to match the cube.. # = cube_type # Set auto-drop to False (so there isn't two cubes), # and auto-respawn to True (so it actually functions). drp_inst.fixup['$disable_autodrop'] = '1' drp_inst.fixup['$disable_autorespawn'] = '0' fizz_out_name, fizz_out = Output.parse_name(res['FizzleOut']) # Output to destroy the cube when the dropper is triggered externally. drp_inst.add_out(Output( inst_out=fizz_out_name, out=fizz_out, targ=local_name(cube_inst, 'cube'), inp='Dissolve', only_once=True, )) # Cube items don't have proxies, so we need to use AddOutput # after it's created (@relay_spawn_3's time). try: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] except KeyError: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] = cube_inst.map.create_ent( classname='logic_relay', targetname='@relay_spawn_3', origin=cube_inst['origin'], ) respawn_inp = list(res.find_all('RespawnIn')) # There's some voice-logic specific to companion cubes. respawn_inp.extend(res.find_all( 'RespawnCcube' if drp_inst.fixup['$cube_type'] == '1' else 'RespawnCube' )) for inp in respawn_inp: resp_in_name, resp_in = inp.value.split(':', 1) out = Output( out='OnFizzled', targ=drp_inst, inst_in=resp_in_name, inp=resp_in, only_once=True, ) relay_spawn_3.add_out(Output( out='OnTrigger', targ=local_name(cube_inst, 'cube'), inp='AddOutput', param=out.gen_addoutput(), only_once=True, delay=0.01, ))
def res_resizeable_trigger(res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: 'markerInst': <ITEM_ID:1,2> value referencing the marker instances, or a filename. 'markerItem': The item's ID 'previewVar': A stylevar which enables/disables the preview overlay. 'previewinst': An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. 'previewMat': If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. 'previewScale': The scale for the func_brush materials. 'previewActivate', 'previewDeactivate': The 'instance:name;Input' value to turn the previewInst on and off. 'triggerActivate, triggerDeactivate': The outputs used when the trigger turns on or off. 'coopVar': The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. 'coopActivate, coopDeactivate': The outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. 'coopOnce': If true, kill the manager after it first activates. 'keys': A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. 'localkeys': The same as above, except values will be changed to use instance-local names. """ marker = resolve_inst(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate','OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection preview_var = res['previewVar', ''].casefold() # Display preview overlays if it's preview mode, and the style var is true # or does not exist if vbsp.IS_PREVIEW and (not preview_var or vbsp.settings['style_vars'][preview_var]): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name( res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst['origin']), Vec.from_str(other['origin']) ) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent( classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin'] ) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['origin'], Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vbsp.VMF.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out(Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out(Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out(Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED
def parse(item_id: str, conf: Property): """Read the item type info from the given config.""" def get_outputs(prop_name): """Parse all the outputs with this name.""" return [ Output.parse(prop) for prop in conf.find_all(prop_name) # Allow blank to indicate no output. if prop.value != '' ] enable_cmd = get_outputs('enable_cmd') disable_cmd = get_outputs('disable_cmd') lock_cmd = get_outputs('lock_cmd') unlock_cmd = get_outputs('unlock_cmd') inf_lock_only = conf.bool('inf_lock_only') timer_done_cmd = get_outputs('timer_done_cmd') if 'timer_sound_pos' in conf: timer_sound_pos = conf.vec('timer_sound_pos') force_timer_sound = conf.bool('force_timer_sound') else: timer_sound_pos = None force_timer_sound = False try: input_type = InputType( conf['Type', 'default'].casefold() ) except ValueError: raise ValueError('Invalid input type "{}": {}'.format( item_id, conf['type'], )) from None invert_var = conf['invertVar', '0'] try: spawn_fire = FeatureMode(conf['spawnfire', 'never'].casefold()) except ValueError: # Older config option - it was a bool for always/never. spawn_fire_bool = conf.bool('spawnfire', None) if spawn_fire_bool is None: raise # Nope, not a bool. spawn_fire = FeatureMode.ALWAYS if spawn_fire_bool else FeatureMode.NEVER try: sec_spawn_fire = FeatureMode(conf['sec_spawnfire', 'never'].casefold()) except ValueError: # Default to primary value. sec_spawn_fire = FeatureMode.NEVER if input_type is InputType.DUAL: sec_enable_cmd = get_outputs('sec_enable_cmd') sec_disable_cmd = get_outputs('sec_disable_cmd') try: default_dual = CONN_TYPE_NAMES[ conf['Default_Dual', 'primary'].casefold() ] except KeyError: raise ValueError('Invalid default type for "{}": {}'.format( item_id, conf['Default_Dual'], )) from None # We need an affinity to use when nothing else specifies it. if default_dual is ConnType.DEFAULT: raise ValueError('Must specify a default type for "{}"!'.format( item_id, )) from None sec_invert_var = conf['sec_invertVar', '0'] else: # No dual type, set to dummy values. sec_enable_cmd = [] sec_disable_cmd = [] default_dual = ConnType.DEFAULT sec_invert_var = '' try: output_type = CONN_TYPE_NAMES[ conf['DualType', 'default'].casefold() ] except KeyError: raise ValueError('Invalid output affinity for "{}": {}'.format( item_id, conf['DualType'], )) from None def get_input(prop_name: str): """Parse an input command.""" try: return Output.parse_name(conf[prop_name]) except IndexError: return None out_act = get_input('out_activate') out_deact = get_input('out_deactivate') out_lock = get_input('out_lock') out_unlock = get_input('out_unlock') timer_start = timer_stop = None if 'out_timer_start' in conf: timer_start = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_start') if prop.value ] if 'out_timer_stop' in conf: timer_stop = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_stop') if prop.value ] return Config( item_id, default_dual, input_type, spawn_fire, invert_var, enable_cmd, disable_cmd, sec_spawn_fire, sec_invert_var, sec_enable_cmd, sec_disable_cmd, output_type, out_act, out_deact, lock_cmd, unlock_cmd, out_lock, out_unlock, inf_lock_only, timer_sound_pos, timer_done_cmd, force_timer_sound, timer_start, timer_stop, )
def res_cust_fizzler(base_inst: Entity, res: Property): """Customises the various components of a custom fizzler item. This should be executed on the base instance. Brush and MakeLaserField are not permitted on laserfield barriers. When executed, the $is_laser variable will be set on the base. Options: * ModelName: sets the targetname given to the model instances. * UniqueModel: If true, each model instance will get a suffix to allow unique targetnames. * Brush: A brush entity that will be generated (the original is deleted.) This cannot be used on laserfields. * Name is the instance name for the brush * Left/Right/Center/Short/Nodraw are the textures used * Keys are a block of keyvalues to be set. Targetname and Origin are auto-set. * Thickness will change the thickness of the fizzler if set. By default it is 2 units thick. * Outputs is a block of outputs (laid out like in VMFs). The targetnames will be localised to the instance. * MergeBrushes, if true will merge this brush set into one entity for each fizzler. This is useful for non-fizzlers to reduce the entity count. * SimplifyBrush, if true will merge the three parts into one brush. All sides will receive the "nodraw" texture at 0.25 scale. * MaterialModify generates material_modify_controls to control the brush. One is generated for each texture used in the brush. This has subkeys 'name' and 'var' - the entity name and shader variable to be modified. MergeBrushes must be enabled if this is present. * MakeLaserField generates a brush stretched across the whole area. * Name, keys and thickness are the same as the regular Brush. * Texture/Nodraw are the textures. * Width is the pixel width of the laser texture, used to scale it correctly. """ model_name = res['modelname', None] make_unique = res.bool('UniqueModel') fizz_name = base_inst['targetname', ''] # search for the model instances model_targetnames = ( fizz_name + '_modelStart', fizz_name + '_modelEnd', ) is_laser = False for inst in vbsp.VMF.by_class['func_instance']: if inst['targetname'] in model_targetnames: if inst.fixup['skin', '0'] == '2': is_laser = True if model_name is not None: if model_name == '': inst['targetname'] = base_inst['targetname'] else: inst['targetname'] = ( base_inst['targetname'] + '-' + model_name ) if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value base_inst.fixup['$is_laser'] = is_laser new_brush_config = list(res.find_all('brush')) if len(new_brush_config) == 0: return # No brush modifications if is_laser: # This is a laserfield! We can't edit those brushes! LOGGER.warning('CustFizzler executed on LaserField!') return # Record which materialmodify controls are used, so we can add if needed. # Conf id -> (brush_name, conf, [textures]) modify_controls = {} for orig_brush in ( vbsp.VMF.by_class['trigger_portal_cleanser'] & vbsp.VMF.by_target[fizz_name + '_brush']): orig_brush.remove() for config in new_brush_config: new_brush = orig_brush.copy() # Unique to the particular config property & fizzler name conf_key = (id(config), fizz_name) if config.bool('SimplifyBrush'): # Replace the brush with a simple one of the same size. bbox_min, bbox_max = new_brush.get_bbox() new_brush.solids = [vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.NODRAW, ).solid] should_merge = config.bool('MergeBrushes') if should_merge and conf_key in FIZZ_BRUSH_ENTS: # These are shared by both ents, but new_brush won't be added to # the map. (We need it though for the widening code to work). FIZZ_BRUSH_ENTS[conf_key].solids.extend(new_brush.solids) else: vbsp.VMF.add_ent(new_brush) # Don't allow restyling it vbsp.IGNORED_BRUSH_ENTS.add(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush['origin'] = orig_brush['origin'] new_brush['targetname'] = conditions.local_name( base_inst, config['name'], ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' conditions.set_ent_keys( new_brush, base_inst, config, ) for out_prop in config.find_children('Outputs'): out = Output.parse(out_prop) out.comma_sep = False out.target = conditions.local_name( base_inst, out.target ) new_brush.add_out(out) if should_merge: # The first brush... FIZZ_BRUSH_ENTS[conf_key] = new_brush mat_mod_conf = config.find_key('MaterialModify', []) if mat_mod_conf: try: used_materials = modify_controls[id(mat_mod_conf)][2] except KeyError: used_materials = set() modify_controls[id(mat_mod_conf)] = ( new_brush['targetname'], mat_mod_conf, used_materials ) # It can only parent to one brush, so it can't attach # to them all properly. if not should_merge: raise Exception( "MaterialModify won't work without MergeBrushes!" ) else: used_materials = None laserfield_conf = config.find_key('MakeLaserField', None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf['texture', const.Special.LASERFIELD] nodraw_tex = laserfield_conf['nodraw', const.Tools.NODRAW] tex_width = laserfield_conf.int('texwidth', 512) is_short = False for side in new_brush.sides(): if side == const.Fizzler.SHORT: is_short = True break if is_short: for side in new_brush.sides(): if side == const.Fizzler.SHORT: side.mat = laser_tex side.uaxis.offset = 0 side.scale = 0.25 else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield( new_brush, laser_tex, nodraw_tex, tex_width, ) if used_materials is not None: used_materials.add(laser_tex.casefold()) else: # Just change the textures for side in new_brush.sides(): try: tex_cat = TEX_FIZZLER[side.mat.casefold()] side.mat = config[tex_cat] except (KeyError, IndexError): # If we fail, just use the original textures pass else: if used_materials is not None and tex_cat != 'nodraw': used_materials.add(side.mat.casefold()) widen_amount = config.float('thickness', 2.0) if widen_amount != 2: for brush in new_brush.solids: conditions.widen_fizz_brush( brush, thickness=widen_amount, ) for brush_name, config, textures in modify_controls.values(): skip_if_static = config.bool('dynamicOnly', True) if skip_if_static and base_inst.fixup['$connectioncount'] == '0': continue mat_mod_name = config['name', 'modify'] var = config['var', '$outputintensity'] if not var.startswith('$'): var = '$' + var for tex in textures: vbsp.VMF.create_ent( classname='material_modify_control', origin=base_inst['origin'], targetname=conditions.local_name(base_inst, mat_mod_name), materialName='materials/' + tex + '.vmt', materialVar=var, parentname=brush_name, )
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 res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) TrackTeleport: Set the track points so they teleport trains to the start. Speed: The fixup or number for the train speed. MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions weighted cubes. The value is the name of the relevant filter. EndOutput: Adds an output to the last track. The value is the same as outputs in VMFs. RotateSegments: If true (default), force segments to face in the direction of movement RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError( "Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir) ) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam'] ) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append(vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append(vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = 'tools/toolstrigger'
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property): """Properly setup rotating brush entities to match the instance. This uses the orientation of the instance to determine the correct spawnflags to make it rotate in the correct direction. This can either modify an existing entity (which may be in an instance), or generate a new one. The generated brush will be 2x2x2 units large, and always set to be non-solid. For both modes: - `Axis`: specifies the rotation axis local to the instance. - `Reversed`: If set, flips the direction around. - `Classname`: Specifies which entity, since the spawnflags required varies. For application to an existing entity: - `ModifyTarget`: The local name of the entity to modify. For brush generation mode: - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating) * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot) * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button) * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating) * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button) """ des_axis = res['axis', 'z'].casefold() reverse = res.bool('reversed') door_type = res['classname', 'func_door_rotating'] orient = Matrix.from_angle(Angle.from_str(ent['angles'])) axis = round(Vec.with_axes(des_axis, 1) @ orient, 6) if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse axis = abs(axis) try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] door_ent: Entity | None if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] door_ent = None spawnflags = 0 else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) setter_loc = str(pos) door_ent = vmf.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) # Extra stuff to apply to the flags (USE, toggle, etc) spawnflags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') # Make the door always non-solid! )) | flag_values.get('solid_flags', 0) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse], ): if flag not in flag_values: continue if door_ent is not None: if value: spawnflags |= flag_values[flag] else: spawnflags &= ~flag_values[flag] else: # Place a KV setter to set this. vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_global=value, ) if door_ent is not None: door_ent['spawnflags'] = spawnflags # This ent uses a keyvalue for reversing... if door_type == 'momentary_rot_button': vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='kv', kv_name='StartDirection', kv_value_global='1' if reverse else '-1', )
def res_make_tag_fizzler(vmf: VMF, inst: Entity, res: Property): """Add an Aperture Tag Paint Gun activation fizzler. These fizzlers are created via signs, and work very specially. This must be before -250 so it runs before fizzlers and connections. """ ( sign_offset, fizz_conn_conf, inst_frame_double, inst_frame_single, blue_sign_on, blue_sign_off, oran_sign_on, oran_sign_off, ) = res.value # type: int, Optional[connections.Config], str, str, str, str, str, str import vbsp if options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizzler = None fizzler_item = None # Look for the fizzler instance we want to replace. sign_item = connections.ITEMS[inst['targetname']] for conn in list(sign_item.outputs): if conn.to_item.name in FIZZLERS: if fizzler is None: fizzler = FIZZLERS[conn.to_item.name] fizzler_item = conn.to_item else: raise ValueError('Multiple fizzlers attached to a sign!') conn.remove() # Regardless, remove the useless output. sign_item.delete_antlines() if fizzler is None: # No fizzler - remove this sign inst.remove() return if fizzler.fizz_type.id == TAG_FIZZ_ID: LOGGER.warning('Two tag signs attached to one fizzler...') inst.remove() return # Swap to the special Tag Fizzler type. fizzler.fizz_type = FIZZ_TYPES[TAG_FIZZ_ID] # And also swap the connection's type. if fizz_conn_conf is not None: fizzler_item.config = fizz_conn_conf fizzler_item.enable_cmd = fizz_conn_conf.enable_cmd fizzler_item.disable_cmd = fizz_conn_conf.disable_cmd fizzler_item.sec_enable_cmd = fizz_conn_conf.sec_enable_cmd fizzler_item.sec_disable_cmd = fizz_conn_conf.sec_disable_cmd sign_loc = ( # The actual location of the sign - on the wall Vec.from_str(inst['origin']) + Vec(0, 0, -64).rotate_by_str(inst['angles'])) fizz_norm_axis = fizzler.normal().axis() # Now deal with the visual aspect: # Blue signs should be on top. blue_enabled = inst.fixup.bool('$start_enabled') oran_enabled = inst.fixup.bool('$start_reversed') # If True, single-color signs will also turn off the other color. # This also means we always show both signs. # If both are enabled or disabled, this has no effect. disable_other = (not inst.fixup.bool('$disable_autorespawn', True) and blue_enabled != oran_enabled) # Delete fixups now, they aren't useful. inst.fixup.clear() if not blue_enabled and not oran_enabled: # Hide the sign in this case! inst.remove() inst_angle = srctools.parse_vec_str(inst['angles']) inst_normal = Vec(0, 0, 1).rotate(*inst_angle) loc = Vec.from_str(inst['origin']) if disable_other or (blue_enabled and oran_enabled): inst['file'] = inst_frame_double # On a wall, and pointing vertically if inst_normal.z == 0 and Vec(y=1).rotate(*inst_angle).z: # They're vertical, make sure blue's on top! blue_loc = Vec(loc.x, loc.y, loc.z + sign_offset) oran_loc = Vec(loc.x, loc.y, loc.z - sign_offset) # If orange is enabled, with two frames put that on top # instead since it's more important if disable_other and oran_enabled: blue_loc, oran_loc = oran_loc, blue_loc else: offset = Vec(0, sign_offset, 0).rotate(*inst_angle) blue_loc = loc + offset oran_loc = loc - offset else: inst['file'] = inst_frame_single # They're always centered blue_loc = loc oran_loc = loc if inst_normal.z != 0: # If on floors/ceilings, rotate to point at the fizzler! sign_floor_loc = sign_loc.copy() sign_floor_loc.z = 0 # We don't care about z-positions. s, l = Vec.bbox(itertools.chain.from_iterable(fizzler.emitters)) if fizz_norm_axis == 'z': # For z-axis, just compare to the center point of the emitters. sign_dir = ((s.x + l.x) / 2, (s.y + l.y) / 2, 0) - sign_floor_loc else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) if fizz_norm_axis == 'x': # Extends in Y direction other_axis = 'y' side_min = s.y side_max = l.y normal = s.x else: # Extends in X direction other_axis = 'x' side_min = s.x side_max = l.x normal = s.y # Right in line with the fizzler. Point at the closest emitter. if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. sign_dir = min( (sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_min, other_axis, normal, ), sign_floor_loc - Vec.with_axes( fizz_norm_axis, side_max, other_axis, normal, )), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec.with_axes( fizz_norm_axis, normal - sign_floor_loc[fizz_norm_axis]).norm() sign_yaw = math.degrees(math.atan2(sign_dir.y, sign_dir.x)) # Round to nearest 90 degrees # Add 45 so the switchover point is at the diagonals sign_yaw = (sign_yaw + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_yaw = int(sign_yaw - 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_yaw) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_yaw) else: raise AssertionError('Cannot be zero here!') else: # On a wall, face upright sign_angle = PETI_INST_ANGLE[inst_normal.as_tuple()] # If disable_other, we show off signs. Otherwise we don't use that sign. blue_sign = blue_sign_on if blue_enabled else blue_sign_off if disable_other else None oran_sign = oran_sign_on if oran_enabled else oran_sign_off if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=blue_sign, targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=oran_sign, targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... # Subtract the sign from the list of connections, but don't go below # zero fizzler.base_inst.fixup['$connectioncount'] = str( max( 0, srctools.conv_int(fizzler.base_inst.fixup['$connectioncount', '']) - 1)) # Find the direction the fizzler normal is. # Signs will associate with the given side! bbox_min, bbox_max = fizzler.emitters[0] sign_center = (bbox_min[fizz_norm_axis] + bbox_max[fizz_norm_axis]) / 2 # Figure out what the sides will set values to... pos_blue = False pos_oran = False neg_blue = False neg_oran = False if sign_loc[fizz_norm_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled # If it activates the paint gun, use different textures fizzler.tag_on_pos = pos_blue or pos_oran fizzler.tag_on_neg = neg_blue or neg_oran # Now make the trigger ents. We special-case these since they need to swap # depending on the sign config and position. if vbsp.GAME_MODE == 'COOP': # We need ATLAS-specific triggers pos_trig = vmf.create_ent(classname='trigger_playerteam', ) neg_trig = vmf.create_ent(classname='trigger_playerteam', ) output = 'OnStartTouchBluePlayer' else: pos_trig = vmf.create_ent(classname='trigger_multiple', ) neg_trig = vmf.create_ent( classname='trigger_multiple', spawnflags='1', ) output = 'OnStartTouch' pos_trig['origin'] = neg_trig['origin'] = fizzler.base_inst['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = local_name(fizzler.base_inst, 'trig_pos') neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_neg') pos_trig['startdisabled'] = neg_trig['startdisabled'] = ( not fizzler.base_inst.fixup.bool('start_enabled')) pos_trig.outputs = [ Output(output, neg_trig, 'Enable'), Output(output, pos_trig, 'Disable'), ] neg_trig.outputs = [ Output(output, pos_trig, 'Enable'), Output(output, neg_trig, 'Disable'), ] voice_attr = vbsp.settings['has_attr'] if blue_enabled or disable_other: # If this is blue/oran only, don't affect the other color neg_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_blue), )) pos_trig.outputs.append( Output( output, '@BlueIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_blue), )) if blue_enabled: # Add voice attributes - we have the gun and gel! voice_attr['bluegelgun'] = True voice_attr['bluegel'] = True voice_attr['bouncegun'] = True voice_attr['bouncegel'] = True if oran_enabled or disable_other: neg_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(neg_oran), )) pos_trig.outputs.append( Output( output, '@OrangeIsEnabled', 'SetValue', param=srctools.bool_as_int(pos_oran), )) if oran_enabled: voice_attr['orangegelgun'] = True voice_attr['orangegel'] = True voice_attr['speedgelgun'] = True voice_attr['speedgel'] = True if not oran_enabled and not blue_enabled: # If both are disabled, we must shutdown the gun when touching # either side - use neg_trig for that purpose! # We want to get rid of pos_trig to save ents vmf.remove_ent(pos_trig) neg_trig['targetname'] = local_name(fizzler.base_inst, 'trig_off') neg_trig.outputs.clear() neg_trig.add_out( Output(output, '@BlueIsEnabled', 'SetValue', param='0')) neg_trig.add_out( Output(output, '@OrangeIsEnabled', 'SetValue', param='0')) # Make the triggers. for bbox_min, bbox_max in fizzler.emitters: bbox_min = bbox_min.copy() - 64 * fizzler.up_axis bbox_max = bbox_max.copy() + 64 * fizzler.up_axis # The triggers are 8 units thick, with a 32-unit gap in the middle neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_norm_axis] -= 24 neg_max[fizz_norm_axis] -= 16 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_norm_axis] += 16 pos_max[fizz_norm_axis] += 24 if blue_enabled or oran_enabled: neg_trig.solids.append( vmf.make_prism( neg_min, neg_max, mat='tools/toolstrigger', ).solid, ) pos_trig.solids.append( vmf.make_prism( pos_min, pos_max, mat='tools/toolstrigger', ).solid, ) else: # If neither enabled, use one trigger neg_trig.solids.append( vmf.make_prism( neg_min, pos_max, mat='tools/toolstrigger', ).solid, )
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name( res['coopActivate', 'OnChangeToAllTrue'] ), output_deact=Output.parse_name( res['coopDeactivate', 'OnChangeToAnyFalse'] ), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin']) ) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def vactube_transform(ctx: Context) -> None: """Implements the dynamic Vactube system.""" all_nodes = list(nodes.parse(ctx.vmf)) if not all_nodes: # No vactubes. return LOGGER.info('{} vactube nodes found.', len(all_nodes)) LOGGER.debug('Nodes: {}', all_nodes) if ctx.studiomdl is None: raise ValueError('Vactubes present, but no studioMDL path provided! ' 'Set the path to studiomdl.exe in srctools.vdf.') obj_count, vac_objects, objects_code = objects.parse(ctx.vmf, ctx.pack) groups = set(objects_code) if not obj_count: raise ValueError('Vactube nodes present, but no objects. ' 'You need to add comp_vactube_objects to your map ' 'to define the contents.') LOGGER.info('{} vactube objects found.', obj_count) # Now join all the nodes to each other. # Tubes only have 90 degree bends, so a system should mostly be formed # out of about 6 different normals. So group by that. inputs_by_norm: Dict[Tuple[float, float, float], List[Tuple[Vec, nodes.Node]]] = defaultdict(list) for node in all_nodes: # Spawners have no inputs. if isinstance(node, nodes.Spawner): node.has_input = True else: inputs_by_norm[node.input_norm().as_tuple()].append( (node.vec_point(0.0), node)) norm_inputs = [(Vec(norm), node_lst) for norm, node_lst in inputs_by_norm.items()] sources: List[nodes.Spawner] = [] LOGGER.info('Linking nodes...') for node in all_nodes: # Destroyers (or Droppers) have no inputs. if isinstance(node, nodes.Destroyer): continue for dest_type in node.out_types: node.outputs[dest_type] = find_closest( norm_inputs, node, node.vec_point(1.0, dest_type), node.output_norm(dest_type), ) if isinstance(node, nodes.Spawner): sources.append(node) if node.group not in groups: group_warn = (f'Node {node} uses group "{node.group}", ' 'which has no objects registered!') if '' in groups: # Fall back to ignoring the group, using the default # blank one which is present. LOGGER.warning("{} Using blank group.", group_warn) node.group = "" else: raise ValueError(group_warn) # Run through them again, check to see if any miss inputs. for node in all_nodes: if not node.has_input: raise ValueError('No source found for junction ' f'{node.ent["targetname"]} at ({node.origin})!') LOGGER.info('Generating animations...') all_anims = animations.generate(sources) # Sort the animations by their start and end, so they ideally are consistent. all_anims.sort(key=lambda a: (a.start_node.origin, a.end_node.origin)) anim_mdl_name = Path('maps', ctx.bsp_path.stem, 'vac_anim.mdl') # Now generate the animation model. # First wipe the model. full_loc = ctx.game.path / 'models' / anim_mdl_name for ext in MDL_EXTS: try: full_loc.with_suffix(ext).unlink() except FileNotFoundError: pass with TemporaryDirectory(prefix='vactubes_') as temp_dir: # Make the reference mesh. with open(temp_dir + '/ref.smd', 'wb') as f: Mesh.build_bbox('root', 'demo', Vec(-32, -32, -32), Vec(32, 32, 32)).export(f) with open(temp_dir + '/prop.qc', 'w') as qc_file: qc_file.write(QC_TEMPLATE.format(path=anim_mdl_name)) for i, anim in enumerate(all_anims): anim.name = anim_name = f'anim_{i:03x}' qc_file.write( SEQ_TEMPLATE.format(name=anim_name, fps=animations.FPS)) with open(temp_dir + f'/{anim_name}.smd', 'wb') as f: anim.mesh.export(f) args = [ str(ctx.studiomdl), '-nop4', '-i', # Ignore warnings. '-game', str(ctx.game.path), temp_dir + '/prop.qc', ] LOGGER.info('Compiling vactube animations {}...', args) subprocess.run(args) # Ensure they're all packed. for ext in MDL_EXTS: try: f = full_loc.with_suffix(ext).open('rb') except FileNotFoundError: pass else: with f: ctx.pack.pack_file(Path('models', anim_mdl_name.with_suffix(ext)), data=f.read()) LOGGER.info('Setting up vactube ents...') # Generate the shared template. ctx.vmf.create_ent( 'prop_dynamic', targetname='_vactube_temp_mover', angles='0 270 0', origin='-16384 0 1024', model=str(Path('models', anim_mdl_name)), rendermode=10, solid=0, spawnflags=64 | 256, # Use Hitboxes for Renderbox, collision disabled. ) ctx.vmf.create_ent( 'prop_dynamic_override', # In case you use the physics model. targetname='_vactube_temp_visual', parentname='_vactube_temp_mover,move', origin='-16384 0 1024', model=nodes.CUBE_MODEL, solid=0, spawnflags=64 | 256, # Use Hitboxes for Renderbox, collision disabled. ) ctx.vmf.create_ent( 'point_template', targetname='_vactube_template', template01='_vactube_temp_mover', template02='_vactube_temp_visual', origin='-16384 0 1024', spawnflags='2', # Preserve names, remove originals. ) # Group animations by their start point. anims_by_start: Dict[nodes.Spawner, List[animations.Animation]] = defaultdict(list) for anim in all_anims: anims_by_start[anim.start_node].append(anim) # And create a dict to link droppers to the animation they want. dropper_to_anim: Dict[nodes.Dropper, animations.Animation] = {} for start_node, anims in anims_by_start.items(): spawn_maker = start_node.ent spawn_maker['classname'] = 'env_entity_maker' spawn_maker['entitytemplate'] = '_vactube_template' spawn_maker['angles'] = '0 0 0' orig_name = spawn_maker['targetname'] spawn_maker.make_unique('_vac_maker') spawn_name = spawn_maker['targetname'] if start_node.is_auto: spawn_timer = ctx.vmf.create_ent( 'logic_timer', targetname=spawn_name + '_timer', origin=start_node.origin, startdisabled='0', userandomtime='1', lowerrandombound=start_node.time_min, upperrandombound=start_node.time_max, ).make_unique() spawn_timer.add_out( Output('OnTimer', spawn_name, 'CallScriptFunction', 'make_cube')) ctx.add_io_remap( orig_name, Output('EnableTimer', spawn_timer, 'Enable'), Output('DisableTimer', spawn_timer, 'Disable'), ) ctx.add_io_remap( orig_name, Output('ForceSpawn', spawn_name, 'CallScriptFunction', 'make_cube'), ) # Now, generate the code so the VScript knows about the animations. code = [ f'// Node: {start_node.ent["targetname"]}, {start_node.origin}' ] for anim in anims: target = anim.end_node anim_speed = anim.start_node.speed pass_code = ','.join([ f'Output({time:.2f}, "{node.ent["targetname"]}", ' f'{node.tv_code(anim_speed)})' for time, node in anim.pass_points ]) cube_name = 'null' if isinstance(target, nodes.Dropper): cube_model = target.cube['model'].replace('\\', '/') cube_skin = conv_int(target.cube['skin']) try: cube_name = vac_objects[start_node.group, cube_model, cube_skin].id except KeyError: LOGGER.warning( 'Cube model "{}", skin {} is not a type of cube travelling ' 'in this vactube!\n\n' 'Add a comp_vactube_object entity with this cube model' # Mention groups if they're used, otherwise it's not important. + (f' with the group "{start_node.group}".' if start_node.group else '.'), cube_model, cube_skin, ) continue # Skip this animation so it's not broken. else: dropper_to_anim[target] = anim code.append(f'{anim.name} <- anim("{anim.name}", {anim.duration}, ' f'{cube_name}, [{pass_code}]);') spawn_maker['vscripts'] = ' '.join([ 'srctools/vac_anim.nut', objects_code[start_node.group], ctx.pack.inject_vscript('\n'.join(code)), ]) # Now, go through each dropper and generate their logic. for dropper, anim in dropper_to_anim.items(): # Pick the appropriate output to fire once left the dropper. if dropper.cube['classname'] == 'prop_monster_box': cube_input = 'BecomeMonster' else: cube_input = 'EnablePortalFunnel' ctx.add_io_remap( dropper.ent['targetname'], # Used to dissolve the existing cube when respawning. Output('FireCubeUser1', dropper.cube['targetname'], 'FireUser1'), # Tell the spawn to redirect a cube to us. Output( 'RequestSpawn', anim.start_node.ent['targetname'], 'RunScriptCode', f'{anim.name}.req_spawn = true', ), Output('CubeReleased', '!activator', cube_input), )
def res_import_template_setup(res: Property): temp_id = res['id'] force = res['force', ''].casefold().split() if 'white' in force: force_colour = template_brush.MAT_TYPES.white elif 'black' in force: force_colour = template_brush.MAT_TYPES.black elif 'invert' in force: force_colour = 'INVERT' else: force_colour = None if 'world' in force: force_type = template_brush.TEMP_TYPES.world elif 'detail' in force: force_type = template_brush.TEMP_TYPES.detail else: force_type = template_brush.TEMP_TYPES.default for size in ('2x2', '4x4', 'wall', 'special'): if size in force: force_grid = size break else: force_grid = None invert_var = res['invertVar', ''] color_var = res['colorVar', ''] replace_tex = defaultdict(list) for prop in res.find_key('replace', []): replace_tex[prop.name].append(prop.value) rem_replace_brush = True additional_ids = set() transfer_overlays = '1' try: replace_brush = res.find_key('replaceBrush') except NoKeyError: replace_brush_pos = None else: if replace_brush.has_children(): replace_brush_pos = replace_brush['Pos', '0 0 0'] additional_ids = set(map( srctools.conv_int, replace_brush['additionalIDs', ''].split(), )) rem_replace_brush = replace_brush.bool('removeBrush', True) transfer_overlays = replace_brush['transferOverlay', '1'] else: replace_brush_pos = replace_brush.value # type: str replace_brush_pos = Vec.from_str(replace_brush_pos) replace_brush_pos.z -= 64 # 0 0 0 defaults to the floor. key_values = res.find_key("Keys", []) if key_values: keys = Property("", [ key_values, res.find_key("LocalKeys", []), ]) # Ensure we have a 'origin' keyvalue - we automatically offset that. if 'origin' not in key_values: key_values['origin'] = '0 0 0' # Spawn everything as detail, so they get put into a brush # entity. force_type = template_brush.TEMP_TYPES.detail outputs = [ Output.parse(prop) for prop in res.find_children('Outputs') ] else: keys = None outputs = [] visgroup_mode = res['visgroup', 'none'].casefold() if visgroup_mode not in ('none', 'choose'): visgroup_mode = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00) if visgroup_mode == 0: visgroup_mode = 'none' # Generate the function which picks which visgroups to add to the map. if visgroup_mode == 'none': def visgroup_func(_): """none = don't add any visgroups.""" return () elif visgroup_mode == 'choose': def visgroup_func(groups): """choose = add one random group.""" return [random.choice(groups)] else: def visgroup_func(groups): """Number = percent chance for each to be added""" for group in groups: val = random.uniform(0, 100) if val <= visgroup_mode: yield group # If true, force visgroups to all be used. visgroup_force_var = res['forceVisVar', ''] return ( temp_id, dict(replace_tex), force_colour, force_grid, force_type, replace_brush_pos, rem_replace_brush, transfer_overlays, additional_ids, invert_var, color_var, visgroup_func, visgroup_force_var, keys, outputs, )
def get_input(prop_name: str): """Parse an input command.""" try: return Output.parse_name(conf[prop_name]) except IndexError: return 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_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(vmf: VMF, coll: Collisions, 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`. - `angles`: Override the instance rotation, so it is always rotated this much. - `rotation`: Apply the specified rotation before the instance's rotation. - `offset`: Offset the template from the instance's position. - `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. - `alignBindOverlay`: If set, align the bindOverlay offsets to the grid. - `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. """ if res.has_children(): orig_temp_id = res['id'] else: orig_temp_id = res.value res = Property('TemplateBrush', []) force = res['force', ''].casefold().split() if 'white' in force: conf_force_colour = texturing.Portalable.white elif 'black' in force: conf_force_colour = texturing.Portalable.black elif 'invert' in force: conf_force_colour = 'INVERT' else: conf_force_colour = None if 'world' in force: force_type = template_brush.TEMP_TYPES.world elif 'detail' in force: force_type = template_brush.TEMP_TYPES.detail else: force_type = template_brush.TEMP_TYPES.default force_grid: texturing.TileSize | None size: texturing.TileSize for size in texturing.TileSize: if size in force: force_grid = size break else: force_grid = None if 'bullseye' in force: surf_cat = texturing.GenCat.BULLSEYE elif 'special' in force or 'panel' in force: surf_cat = texturing.GenCat.PANEL else: surf_cat = texturing.GenCat.NORMAL replace_tex: dict[str, list[str]] = {} for prop in res.find_block('replace', or_blank=True): replace_tex.setdefault(prop.name, []).append(prop.value) if 'replaceBrush' in res: LOGGER.warning( 'replaceBrush command used for template "{}", which is no ' 'longer used.', orig_temp_id, ) bind_tile_pos = [ # So it's the floor block location. Vec.from_str(value) - (0, 0, 128) for value in res.find_key('BindOverlay', or_blank=True).as_array() ] align_bind_overlay = res.bool('alignBindOverlay') key_values = res.find_block("Keys", or_blank=True) if key_values: key_block = Property("", [ key_values, res.find_block("LocalKeys", or_blank=True), ]) # Ensure we have a 'origin' keyvalue - we automatically offset that. if 'origin' not in key_values: key_values['origin'] = '0 0 0' # Spawn everything as detail, so they get put into a brush # entity. force_type = template_brush.TEMP_TYPES.detail outputs = [Output.parse(prop) for prop in res.find_children('Outputs')] else: key_block = None outputs = [] # None = don't add any more. visgroup_func: Callable[[Random, list[str]], Iterable[str]] | None = None try: # allow both spellings. visgroup_prop = res.find_key('visgroups') except NoKeyError: visgroup_prop = res.find_key('visgroup', 'none') if visgroup_prop.has_children(): visgroup_instvars = list(visgroup_prop) else: visgroup_instvars = [] visgroup_mode = res['visgroup', 'none'].casefold() # Generate the function which picks which visgroups to add to the map. if visgroup_mode == 'none': pass elif visgroup_mode == 'choose': def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]: """choose = add one random group.""" return [rng.choice(groups)] else: percent = srctools.conv_float(visgroup_mode.rstrip('%'), 0.00) if percent > 0.0: def visgroup_func(rng: Random, groups: list[str]) -> Iterable[str]: """Number = percent chance for each to be added""" for group in sorted(groups): if rng.uniform(0, 100) <= percent: yield group picker_vars = [(prop.real_name, prop.value) for prop in res.find_children('pickerVars')] try: ang_override = to_matrix(Angle.from_str(res['angles'])) except LookupError: ang_override = None try: rotation = to_matrix(Angle.from_str(res['rotation'])) except LookupError: rotation = Matrix() offset = res['offset', '0 0 0'] invert_var = res['invertVar', ''] color_var = res['colorVar', ''] if color_var.casefold() == '<editor>': color_var = '<editor>' # If true, force visgroups to all be used. visgroup_force_var = res['forceVisVar', ''] sense_offset = res.vec('senseOffset') 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] = '' return place_template
def mon_camera_link(vmf: VMF) -> None: """Link cameras to monitors.""" import vbsp if not HAS_MONITOR: return ALL_CAMERAS.sort(key=lambda cam: cam.cam_pos) fog_opt = vbsp.settings['fog'] active_counts = [ srctools.conv_int(cam.inst.fixup['$start_enabled', '0']) for cam in ALL_CAMERAS ] for index, cam in enumerate(ALL_CAMERAS): # type: int, Camera if srctools.conv_int(cam.inst.fixup['$connectioncount']) == 0: continue conn_item = connections.ITEMS[cam.inst['targetname']] # Generate an input to the VScript which turns on/off this camera. # Everything's by index. conn_item.enable_cmd = (Output( '', '@camera', 'RunScriptCode', 'CamEnable({})'.format(index), ), ) conn_item.disable_cmd = (Output( '', '@camera', 'RunScriptCode', 'CamDisable({})'.format(index), ), ) for is_act, cam in zip(active_counts, ALL_CAMERAS): if is_act: start_pos = cam.cam_pos start_angles = cam.cam_angles break else: # No cameras start active, we need to be positioned elsewhere. if options.get(str, 'voice_studio_inst'): # Start at the studio, if it exists. start_pos = get_studio_pose() start_angles = '{:g} {:g} 0'.format( options.get(float, 'voice_studio_cam_pitch'), options.get(float, 'voice_studio_cam_yaw'), ) # If we start at the studio, make the ai_relationships # for turret fire start active. for relation in MONITOR_RELATIONSHIP_ENTS: relation['StartActive'] = '1' else: # Start in arrival_departure_transition_ents... start_pos = Vec(-2500, -2500, 0) start_angles = '0 90 0' cam_ent = vmf.create_ent( classname='point_camera', targetname='@camera', spawnflags='0', # Start on origin=start_pos, angles=start_angles, fov='60', # Copy fog settings from the skybox. fogEnable='1', fogMaxDensity='1', fogColor=fog_opt['primary'], fogStart=fog_opt['start'], fogEnd=fog_opt['end'], ) if not ALL_CAMERAS: return # We only need the script if we're moving at all. cam_ent['vscripts'] = 'BEE2/mon_camera.nut' cam_ent['thinkfunction'] = 'Think' # Now start adding all the variables the script needs. # Tell it the number of cameras, and how many start active. # That lets it trivially determine when they're all off. # We keep the list of active counts to reuse after. active_counts = [ srctools.conv_int(cam.inst.fixup['$start_enabled', '0']) for cam in ALL_CAMERAS ] scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_NUM', len(ALL_CAMERAS)) scriptvar_set(cam_ent, start_pos - (0, 0, 16), 'CAM_ACTIVE_NUM', sum(active_counts)) # Then add the values for each camera. We can use the setter's modes # to include the position as the actual loc. for i, (cam, active) in enumerate(zip(ALL_CAMERAS, active_counts)): scriptvar_set( cam_ent, cam.cam_pos, 'CAM_LOC', index=i, angles=cam.cam_angles, mode='pos', ) scriptvar_set( cam_ent, cam.cam_pos, 'CAM_ANGLES', index=i, angles=cam.cam_angles, mode='ang', ) scriptvar_set( cam_ent, cam.cam_pos + (0, 0, 8), 'CAM_ACTIVE', index=i, value=active, ) if options.get(str, 'voice_studio_inst'): # We have a voice studio, send values to the script. scriptvar_set(cam_ent, get_studio_pose(), 'CAM_STUDIO_LOC', mode='pos') scriptvar_set( cam_ent, get_studio_pose(), 'CAM_STUDIO_ANG', mode='ang', angles='{:g} {:g} 0'.format( options.get(float, 'voice_studio_cam_pitch'), options.get(float, 'voice_studio_cam_yaw'), ), ) use_turret = '1' if MONITOR_RELATIONSHIP_ENTS else '0' swap_chance = options.get(float, 'voice_studio_inter_chance') else: use_turret = '0' swap_chance = -1 scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_TURRET', use_turret) scriptvar_set(cam_ent, start_pos + (0, 0, 16), 'CAM_STUDIO_CHANCE', swap_chance)
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name(res['coopActivate', 'OnChangeToAllTrue']), output_deact=Output.parse_name(res['coopDeactivate', 'OnChangeToAnyFalse']), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin'])) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def parse(cls, vmf: VMF, ent: Entity, radius: float) -> 'Dropper': """Scan the map applying dropper tweaks, then create the Dropper object.""" filter_name = ent['filtername'] template_name = ent['template'] for cube_filter in vmf.search(filter_name): break else: raise ValueError( f'No filter "{filter_name}" for dropper at {ent["origin"]}!') for template in vmf.search(template_name): break else: raise ValueError( f'No template "{template_name}" for dropper at {ent["origin"]}!' ) best_cube = None best_dist = math.inf radius **= 2 ref_pos = Vec.from_str(cube_filter['origin']) for cube in vmf.by_class['prop_weighted_cube'] | vmf.by_class[ 'prop_monster_box']: dist = (Vec.from_str(cube['origin']) - ref_pos).mag_sq() if dist > radius or dist > best_dist: continue best_dist = dist best_cube = cube if best_cube is None: LOGGER.warning( 'Cube dropper at {} has no cube. Generating standard one...', ref_pos) best_cube = vmf.create_ent( 'prop_weighted_cube', angles='0 0 0', newskins='1', skintype='0', cubetype='0', skin='0', paintpower='4', model=CUBE_MODEL, ) # Now adjust the cube for dropper use. best_cube.make_unique('dropper_cube') best_cube['origin'] = ent['origin'] # Only regular cubes can disable funnelling, but frankenturrets # require being in box form. if best_cube['classname'] == 'prop_monster_box': best_cube['startasbox'] = '1' else: best_cube['allowfunnel'] = '0' # Copy the cube name to filter and dropper. cube_filter['filtername'] = best_cube['targetname'] for i in range(1, 10): if not template[f'Template{i:02}']: template[f'Template{i:02}'] = best_cube['targetname'] break else: raise ValueError(f'No spare slots for template "{template_name}"!') # Add fizzle outputs if enabled. if srctools.conv_bool(ent['autorespawn']): best_cube.outputs += [ out for out in ent.outputs if out.output.casefold() == 'onfizzled' ] ent.add_out(Output(Dropper.pass_out_name, template, 'ForceSpawn')) return Dropper(ent, template, best_cube)
def res_fix_rotation_axis(ent: Entity, res: Property): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * `func_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = srctools.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+'))) name = conditions.local_name(ent, res['name', '']) axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str(res['Pos', '0 0 0']).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def parse(item, key): """Parse the output value, handling values that aren't present.""" val = item[key, ''] if not val: return None, '' return Output.parse_name(val)