def make_static_pist_setup(res: Property): instances = ( 'bottom_0', 'bottom_1', 'bottom_2', 'bottom_3', 'logic_0', 'logic_1', 'logic_2', 'logic_3', 'static_0', 'static_1', 'static_2', 'static_3', 'static_4', 'grate_low', 'grate_high', ) if res.has_children(): # Pull from config return { name: instanceLocs.resolve_one( res[name, ''], error=False, ) for name in instances } else: # Pull from editoritems if ':' in res.value: from_item, prefix = res.value.split(':', 1) else: from_item = res.value prefix = '' return { name: instanceLocs.resolve_one( '<{}:bee2_{}{}>'.format(from_item, prefix, name), error=False, ) for name in instances }
def res_camera_setup(res: Property): return { 'cam_off': Vec.from_str(res['CamOff', '']), 'yaw_off': Vec.from_str(res['YawOff', '']), 'pitch_off': Vec.from_str(res['PitchOff', '']), 'yaw_inst': instanceLocs.resolve_one(res['yawInst', '']), 'pitch_inst': instanceLocs.resolve_one(res['pitchInst', '']), 'yaw_range': srctools.conv_int(res['YawRange', ''], 90), 'pitch_range': srctools.conv_int(res['YawRange', ''], 90), }
def res_add_global_inst(res: Property): """Add one instance in a location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Origin: The location of the instance (defaults to '0 0 -10000'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. """ if res.value is not None: if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], origin=res['position', '0 0 -10000'], fixup_style=res['fixup_style', '0'], ) GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def resolve_optional(prop: Property, key: str) -> Optional[str]: """Resolve the given instance, or return None if not defined.""" try: file = prop[key] except LookupError: return None return instanceLocs.resolve_one(file)
def res_add_global_inst(res: Property): """Add one instance in a specific location. Options: - `allow_multiple`: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. - `name`: The targetname of the instance. If blank, the instance will be given a name of the form `inst_1234`. - `file`: The filename for the instance. - `angles`: The orientation of the instance (defaults to `0 0 0`). - `fixup_style`: The Fixup style for the instance. `0` (default) is Prefix, `1` is Suffix, and `2` is None. - `position`: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc') GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def res_add_global_inst(res: Property): """Add one instance in a specific location. Options: `allow_multiple`: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. `name`: The targetname of the instance. If blank, the instance will be given a name of the form `inst_1234`. `file`: The filename for the instance. `angles`: The orientation of the instance (defaults to `0 0 0`). `fixup_style`: The Fixup style for the instance. `0` (default) is Prefix, `1` is Suffix, and `2` is None. `position`: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc') GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None: """Remove instances from the map, and store off the positions.""" glass_inst = resolve_one('[glass_128]') pos = None for brush_ent in vmf.by_class['func_detail']: is_glass = False for face in brush_ent.sides(): if face.mat == consts.Special.GLASS: has_attr['glass'] = True pos = face.get_origin() is_glass = True break if is_glass: brush_ent.remove() BARRIERS[get_pos_norm(pos)] = BarrierType.GLASS for brush_ent in vmf.by_class['func_brush']: is_grating = False for face in brush_ent.sides(): if face.mat == consts.Special.GRATING: has_attr['grating'] = True pos = face.get_origin() is_grating = True break if is_grating: brush_ent.remove() BARRIERS[get_pos_norm(pos)] = BarrierType.GRATING for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() if filename == glass_inst: inst.remove() if vbsp_options.get(str, 'glass_pack') and has_attr['glass']: packing.pack_list(vmf, vbsp_options.get(str, 'glass_pack'))
def res_track_plat(res: Property): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. Values: * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets. This is used to determine all the instance filenames. * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`. Otherwise all tracks will receive the name of the platform. * `vert_suffix`: If set, add `_vert` suffixes to vertical track instance names. * `horiz_suffix`: Add suffixes to horizontal tracks (_horiz, _horiz_mirrored) * `plat_suffix`: If set, also add the above `_vert` or `_horiz` suffixes to the platform. * `vert_bottom_suffix`: If set, add '_bottom' / '_vert_bottom' to the track at the bottom of vertical platforms. * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable. """ # Get the instances from editoritems (inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single) = instanceLocs.resolve(res['orig_item']) single_plat_inst = instanceLocs.resolve_one(res['single_plat', '']) track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vbsp.VMF.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join('{!s}: {}'.format(k, v['file']) for k, v in track_instances.items())) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vbsp.VMF.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str(plat_inst['angles']) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec( 0, 0, 1).rotate(*Vec.from_str(first_track['angles'])): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'])) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst first_track.remove() continue # Next platform track_set = set() if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = (plat_inst['targetname'] + '-' + track_targets + str(ind)) # Now figure out which way the track faces: # The direction horizontal track is offset side_dir = Vec(0, 1, 0).rotate_by_str(first_track['angles']) # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) if side_dir == facing: track_facing = 'HORIZ' elif side_dir == -facing: track_facing = 'HORIZ_MIRR' else: track_facing = 'VERT' # Now add the suffixes if track_facing == 'VERT': if srctools.conv_bool(res['vert_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_vert') if srctools.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_vert') if srctools.conv_bool(res['vert_bottom_suffix', '']): # We want to find the bottom/top track which is facing the # same direction as the platform. track_dirs = { inst_top: Vec(-1, 0, 0), inst_bottom: Vec(1, 0, 0) } for inst in track_set: try: norm_off = track_dirs[inst['file'].casefold()] except KeyError: continue if norm_off.rotate_by_str(inst['angles']) == facing: conditions.add_suffix(inst, '_bottom') elif track_facing == 'HORIZ_MIRR': if srctools.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz_mirrored') if srctools.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_horiz') else: # == 'HORIZ' if srctools.conv_bool(res['horiz_suffix', '']): for inst in track_set: conditions.add_suffix(inst, '_horiz') if srctools.conv_bool(res['plat_suffix', '']): conditions.add_suffix(plat_inst, '_horiz') plat_var = res['plat_var', ''] if plat_var != '': # Skip the '_mirrored' section if needed plat_inst.fixup[plat_var] = track_facing[:5].lower() return RES_EXHAUSTED # Don't re-run
def res_make_catwalk(vmf: VMF, res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. * Instances: * `markerInst: The instance set in editoritems. * `straight_128`/`256`/`512`: Straight sections. Extends East. * `corner: An L-corner piece. Connects on North and West sides. * `TJunction`: A T-piece. Connects on all but the East side. * `crossJunction`: A X-piece. Connects on all sides. * `end`: An end piece. Connects on the East side. * `stair`: A stair. Starts East and goes Up and West. * `end_wall`: Connects a West wall to a East catwalk. * `support_wall`: A support extending from the East wall. * `support_ceil`: A support extending from the ceiling. * `support_floor`: A support extending from the floor. * `support_goo`: A floor support, designed for goo pits. * `single_wall`: A section connecting to an East wall. """ LOGGER.info("Starting catwalk generator...") marker = instanceLocs.resolve(res['markerInst']) instances = { name: instanceLocs.resolve_one(res[name, ''], error=True) for name in ( 'straight_128', 'straight_256', 'straight_512', 'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall', 'support_wall', 'support_ceil', 'support_floor', 'support_goo', 'single_wall', 'markerInst', ) } # If there are no attachments remove a catwalk piece instances['NONE'] = '' if instances['end_wall'] == '': instances['end_wall'] = instances['end'] # The directions this instance is connected by (NSEW) links = {} # type: Dict[Entity, Link] markers = {} # Find all our markers, so we can look them up by targetname. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker: continue links[inst] = Link() markers[inst['targetname']] = inst # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 origin += 64 while brushLoc.POS['world': origin].is_goo: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return RES_EXHAUSTED LOGGER.info('Connections: {}', links) LOGGER.info('Markers: {}', markers) # First loop through all the markers, adding connecting sections for marker_name, inst in markers.items(): mark_item = ITEMS[marker_name] mark_item.delete_antlines() for conn in list(mark_item.outputs): try: inst2 = markers[conn.to_item.name] except KeyError: LOGGER.warning('Catwalk connected to non-catwalk!') conn.remove() origin1 = Vec.from_str(inst['origin']) origin2 = Vec.from_str(inst2['origin']) if origin1.x != origin2.x and origin1.y != origin2.y: LOGGER.warning('Instances not aligned!') continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('Not enough room for stairs!') continue if dist > 128: # add straight sections in between place_catwalk_connections(vmf, instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = links[inst] conn_lst2 = links[inst2] if origin1.x < origin2.x: conn_lst1.E = conn_lst2.W = True elif origin2.x < origin1.x: conn_lst1.W = conn_lst2.E = True if origin1.y < origin2.y: conn_lst1.N = conn_lst2.S = True elif origin2.y < origin1.y: conn_lst1.S = conn_lst2.N = True for inst, dir_mask in links.items(): # Set the marker instances based on the attached walkways. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()] inst['file'] = instances[CATWALK_TYPES[new_type]] if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: if normal == dir_mask.conn_dir(): inst['file'] = instances['end_wall'] continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst['file'] = instances['single_wall'] inst['angles'] = INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports supp = None if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if brushLoc.POS['world': origin].is_goo: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: vmf.create_ent( classname='func_instance', origin=inst['origin'], angles=INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return RES_EXHAUSTED
def res_make_catwalk(vmf: VMF, res: Property): """Speciallised result to generate catwalks from markers. Only runs once, and then quits the condition list. Instances: MarkerInst: The instance set in editoritems. Straight_128/256/512: Straight sections. Extends East Corner: A corner piece. Connects on N and W sides. TJunction; A T-piece. Connects on all but the East side. CrossJunction: A X-piece. Connects on all sides. End: An end piece. Connects on the East side. Stair: A stair. Starts East and goes Up and West. End_wall: Connects a West wall to a East catwalk. Support_Wall: A support extending from the East wall. Support_Ceil: A support extending from the ceiling. Support_Floor: A support extending from the floor. Support_Goo: A floor support, designed for goo pits. Single_Wall: A section connecting to an East wall. """ LOGGER.info("Starting catwalk generator...") marker = instanceLocs.resolve(res['markerInst']) output_target = res['output_name', 'MARKER'] instances = { name: instanceLocs.resolve_one(res[name, ''], error=True) for name in ( 'straight_128', 'straight_256', 'straight_512', 'corner', 'tjunction', 'crossjunction', 'end', 'stair', 'end_wall', 'support_wall', 'support_ceil', 'support_floor', 'support_goo', 'single_wall', 'markerInst', ) } # If there are no attachments remove a catwalk piece instances['NONE'] = '' if instances['end_wall'] == '': instances['end_wall'] = instances['end'] # The directions this instance is connected by (NSEW) links = {} # type: Dict[Entity, Link] markers = {} # Find all our markers, so we can look them up by targetname. for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker: continue links[inst] = Link() markers[inst['targetname']] = inst # Snap the markers to the grid. If on glass it can become offset... origin = Vec.from_str(inst['origin']) origin = origin // 128 * 128 origin += 64 while brushLoc.POS['world': origin].is_goo: # The instance is in goo! Switch to floor orientation, and move # up until it's in air. inst['angles'] = '0 0 0' origin.z += 128 inst['origin'] = str(origin) if not markers: return RES_EXHAUSTED LOGGER.info('Connections: {}', links) LOGGER.info('Markers: {}', markers) # First loop through all the markers, adding connecting sections for inst in markers.values(): for conn in inst.outputs: if conn.output != output_target or conn.input != output_target: # Indicator toggles or similar, delete these entities. # Find the associated overlays too. for del_inst in vmf.by_target[conn.target]: conditions.remove_ant_toggle(del_inst) continue inst2 = markers[conn.target] LOGGER.debug('{} <-> {}', inst['targetname'], inst2['targetname']) origin1 = Vec.from_str(inst['origin']) origin2 = Vec.from_str(inst2['origin']) if origin1.x != origin2.x and origin1.y != origin2.y: LOGGER.warning('Instances not aligned!') continue y_dir = origin1.x == origin2.x # Which way the connection is if y_dir: dist = abs(origin1.y - origin2.y) else: dist = abs(origin1.x - origin2.x) vert_dist = origin1.z - origin2.z if (dist - 128) // 2 < abs(vert_dist): # The stairs are 2 long, 1 high. Check there's enough room # Subtract the last block though, since that's a corner. LOGGER.warning('Not enough room for stairs!') continue if dist > 128: # add straight sections in between place_catwalk_connections(vmf, instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = links[inst] conn_lst2 = links[inst2] if origin1.x < origin2.x: conn_lst1.E = conn_lst2.W = True elif origin2.x < origin1.x: conn_lst1.W = conn_lst2.E = True if origin1.y < origin2.y: conn_lst1.N = conn_lst2.S = True elif origin2.y < origin1.y: conn_lst1.S = conn_lst2.N = True inst.outputs.clear() # Remove the outputs now, they're useless for inst, dir_mask in links.items(): # Set the marker instances based on the attached walkways. normal = Vec(0, 0, 1).rotate_by_str(inst['angles']) new_type, inst['angles'] = utils.CONN_LOOKUP[dir_mask.as_tuple()] inst['file'] = instances[CATWALK_TYPES[new_type]] if new_type is utils.CONN_TYPES.side: # If the end piece is pointing at a wall, switch the instance. if normal.z == 0: if normal == dir_mask.conn_dir(): inst['file'] = instances['end_wall'] continue # We never have normal supports on end pieces elif new_type is utils.CONN_TYPES.none: # Unconnected catwalks on the wall switch to a special instance. # This lets players stand next to a portal surface on the wall. if normal.z == 0: inst['file'] = instances['single_wall'] inst['angles'] = INST_ANGLE[normal.as_tuple()] else: inst.remove() continue # These don't get supports otherwise # Add regular supports supp = None if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if brushLoc.POS['world': origin].is_goo: supp = instances['support_goo'] else: supp = instances['support_floor'] elif normal == (0, 0, -1): supp = instances['support_ceil'] else: supp = instances['support_wall'] if supp: vmf.create_ent( classname='func_instance', origin=inst['origin'], angles=INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return RES_EXHAUSTED
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. If a single value, this sets only the filename. Values: `file`: The filename. `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. `copy_fixup`: If true, all the $replace values from the original instance will be copied over. `move_outputs`: If true, outputs will be moved to this instance. `offset`: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. `angles`: If set, overrides the base instance angles. This does not affect the offset property. `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [Property('File', res.value)]) angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def res_cust_output(inst: Entity, res: Property): """Add an additional output to the instance with any values. Always points to the targeted item. If DecConCount is 1, connections """ ( outputs, dec_con_count, targ_conditions, force_sign_type, (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), ) = res.value over_name = '@' + inst['targetname'] + '_indicator' for toggle in vbsp.VMF.by_class['func_instance']: if toggle.fixup['indicator_name', ''] == over_name: toggle_name = toggle['targetname'] break else: toggle_name = '' # we want to ignore the toggle instance, if it exists # Build a mapping from names to targets. # This is also the set of all output items, plus indicators. targets = defaultdict(list) for out in inst.outputs: if out.target != toggle_name: targets[out.target].append(out) pan_files = instanceLocs.resolve('[indPan]') # These all require us to search through the instances. if force_sign_type or dec_con_count or targ_conditions: for con_inst in vbsp.VMF.by_class['func_instance']: # type: Entity if con_inst['targetname'] not in targets: # Not our instance continue # Is it an indicator panel, and should we be modding it? if force_sign_type is not None and con_inst['file'].casefold( ) in pan_files: # Remove the panel if force_sign_type == '': con_inst.remove() continue # Overwrite the signage instance, and then add the # appropriate outputs to control it. sign_id, sign_file_id = force_sign_type con_inst['file'] = instanceLocs.resolve_one(sign_file_id, error=True) # First delete the original outputs: for out in targets[con_inst['targetname']]: inst.outputs.remove(out) inputs = CONNECTIONS[sign_id] act_name, act_inp = inputs.in_act deact_name, deact_inp = inputs.in_deact LOGGER.info('outputs: a="{}" d="{}"\n' 'inputs: a="{}" d="{}"'.format( (sign_act_name, sign_act_out), (sign_deact_name, sign_deact_out), inputs.in_act, inputs.in_deact)) if act_inp and sign_act_out: inst.add_out( Output( inst_out=sign_act_name, out=sign_act_out, inst_in=act_name, inp=act_inp, targ=con_inst['targetname'], )) if deact_inp and sign_deact_out: inst.add_out( Output( inst_out=sign_deact_name, out=sign_deact_out, inst_in=deact_name, inp=deact_inp, targ=con_inst['targetname'], )) if dec_con_count and 'connectioncount' in con_inst.fixup: # decrease ConnectionCount on the ents, # so they can still process normal inputs try: val = int(con_inst.fixup['connectioncount']) con_inst.fixup['connectioncount'] = str(val - 1) except ValueError: # skip if it's invalid LOGGER.warning(con_inst['targetname'] + ' has invalid ConnectionCount!') if targ_conditions: for cond in targ_conditions: # type: Condition cond.test(con_inst) if outputs: for targ in targets: for out in outputs: conditions.add_output(inst, out, targ)
def res_change_instance(inst: Entity, res: Property): """Set the file to a value.""" inst['file'] = instanceLocs.resolve_one(res.value, error=True)
def res_track_plat(vmf: VMF, res: Property): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. This should be run unconditionally, not once per item. Values: * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets. This is used to determine all the instance filenames. * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`. Otherwise all tracks will receive the name of the platform. * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix to the platform. * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable. * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup variable to indicate the relative direction the top faces. """ # Get the instances from editoritems ( inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single ) = instanceLocs.resolve(res['orig_item']) single_plat_inst = instanceLocs.resolve_one(res['single_plat', '']) track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vmf.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join( '{!s}: {}'.format(k, v['file']) for k, v in track_instances.items() )) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vmf.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str( plat_inst['angles'] ) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec(0, 0, 1).rotate( *Vec.from_str(first_track['angles']) ): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'] )) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst first_track.remove() continue # Next platform track_set = set() # type: Set[Entity] if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = ( plat_inst['targetname'] + '-' + track_targets + str(ind) ) # Now figure out which way the track faces: # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) # The direction horizontal track is offset uaxis = Vec(x=1).rotate_by_str(first_track['angles']) vaxis = Vec(y=1).rotate_by_str(first_track['angles']) if uaxis == facing: plat_facing = 'vert' track_facing = 'E' elif uaxis == -facing: plat_facing = 'vert' track_facing = 'W' elif vaxis == facing: plat_facing = 'horiz' track_facing = 'N' elif vaxis == -facing: plat_facing = 'horiz' track_facing = 'S' else: raise ValueError('Facing {} is not U({}) or V({})!'.format( facing, uaxis, vaxis, )) if res.bool('plat_suffix'): conditions.add_suffix(plat_inst, '_' + plat_facing) plat_var = res['plat_var', ''] if plat_var: plat_inst.fixup[plat_var] = plat_facing track_var = res['track_var', ''] if track_var: plat_inst.fixup[track_var] = track_facing for track in track_set: track.fixup.update(plat_inst.fixup) return RES_EXHAUSTED # Don't re-run
def res_track_plat(vmf: VMF, res: Property): """Logic specific to Track Platforms. This allows switching the instances used depending on if the track is horizontal or vertical and sets the track targetnames to a useful value. This should be run unconditionally, not once per item. Values: * `orig_item`: The "<ITEM_ID>" for the track platform, with angle brackets. This is used to determine all the instance filenames. * `single_plat`: An instance used for the entire platform, if it's one rail long (and therefore can't move). * `track_name`: If set, rename track instances following the pattern `plat_name-track_nameXX`. Otherwise all tracks will receive the name of the platform. * `plat_suffix`: If set, add a `_vert` or `_horiz` suffix to the platform. * `plat_var`: If set, save the orientation (`vert`/`horiz`) to the provided $fixup variable. * `track_var`: If set, save `N`, `S`, `E`, or `W` to the provided $fixup variable to indicate the relative direction the top faces. """ # Get the instances from editoritems (inst_bot_grate, inst_bottom, inst_middle, inst_top, inst_plat, inst_plat_oscil, inst_single) = instanceLocs.resolve(res['orig_item']) single_plat_inst = instanceLocs.resolve_one(res['single_plat', '']) track_targets = res['track_name', ''] track_files = [inst_bottom, inst_middle, inst_top, inst_single] platforms = [inst_plat, inst_plat_oscil] # All the track_set in the map, indexed by origin track_instances = { Vec.from_str(inst['origin']).as_tuple(): inst for inst in vmf.by_class['func_instance'] if inst['file'].casefold() in track_files } LOGGER.debug('Track instances:') LOGGER.debug('\n'.join('{!s}: {}'.format(k, v['file']) for k, v in track_instances.items())) if not track_instances: return RES_EXHAUSTED # Now we loop through all platforms in the map, and then locate their # track_set for plat_inst in vmf.by_class['func_instance']: if plat_inst['file'].casefold() not in platforms: continue # Not a platform! LOGGER.debug('Modifying "' + plat_inst['targetname'] + '"!') plat_loc = Vec.from_str(plat_inst['origin']) # The direction away from the wall/floor/ceil normal = Vec(0, 0, 1).rotate_by_str(plat_inst['angles']) for tr_origin, first_track in track_instances.items(): if plat_loc == tr_origin: # Check direction if normal == Vec( 0, 0, 1).rotate(*Vec.from_str(first_track['angles'])): break else: raise Exception('Platform "{}" has no track!'.format( plat_inst['targetname'])) track_type = first_track['file'].casefold() if track_type == inst_single: # Track is one block long, use a single-only instance and # remove track! plat_inst['file'] = single_plat_inst first_track.remove() continue # Next platform track_set = set() # type: Set[Entity] if track_type == inst_top or track_type == inst_middle: # search left track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=-1, ) if track_type == inst_bottom or track_type == inst_middle: # search right track_scan( track_set, track_instances, first_track, middle_file=inst_middle, x_dir=+1, ) # Give every track a targetname matching the platform for ind, track in enumerate(track_set, start=1): if track_targets == '': track['targetname'] = plat_inst['targetname'] else: track['targetname'] = (plat_inst['targetname'] + '-' + track_targets + str(ind)) # Now figure out which way the track faces: # The direction of the platform surface facing = Vec(-1, 0, 0).rotate_by_str(plat_inst['angles']) # The direction horizontal track is offset uaxis = Vec(x=1).rotate_by_str(first_track['angles']) vaxis = Vec(y=1).rotate_by_str(first_track['angles']) if uaxis == facing: plat_facing = 'vert' track_facing = 'E' elif uaxis == -facing: plat_facing = 'vert' track_facing = 'W' elif vaxis == facing: plat_facing = 'horiz' track_facing = 'N' elif vaxis == -facing: plat_facing = 'horiz' track_facing = 'S' else: raise ValueError('Facing {} is not U({}) or V({})!'.format( facing, uaxis, vaxis, )) if res.bool('plat_suffix'): conditions.add_suffix(plat_inst, '_' + plat_facing) plat_var = res['plat_var', ''] if plat_var: plat_inst.fixup[plat_var] = plat_facing track_var = res['track_var', ''] if track_var: plat_inst.fixup[track_var] = track_facing for track in track_set: track.fixup.update(plat_inst.fixup) return RES_EXHAUSTED # Don't re-run
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. MUST be priority -100 so it runs before fizzlers! """ import vbsp if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['TAG']: # Abort - TAG fizzlers shouldn't appear in any other game! inst.remove() return fizz_base = fizz_name = None # Look for the fizzler instance we want to replace for targetname in inst.output_targets(): if targetname in tag_fizzlers: fizz_name = targetname fizz_base = tag_fizzlers[targetname] del tag_fizzlers[targetname] # Don't let other signs mod this one! continue else: # It's an indicator toggle, remove it and the antline to clean up. LOGGER.warning('Toggle: {}', targetname) for ent in vmf.by_target[targetname]: remove_ant_toggle(ent) inst.outputs.clear() # Remove the outptuts now, they're not valid anyway. if fizz_base is None: # No fizzler - remove this sign inst.remove() return # The distance from origin the double signs are seperated by. sign_offset = res.int('signoffset', 16) 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'])) # 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'] = res['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'] = res['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. # Grab the data saved earlier in res_find_potential_tag_fizzlers() axis, side_min, side_max, normal = tag_fizzler_locs[fizz_name] # The Z-axis fizzler (horizontal) must be treated differently. if axis == 'z': # For z-axis, just compare to the center point. # The values are really x, y, z, not what they're named. sign_dir = sign_floor_loc - (side_min, side_max, normal) else: # For the other two, we compare to the line, # or compare to the closest side (in line with the fizz) other_axis = 'x' if axis == 'y' else 'y' if abs(sign_floor_loc[other_axis] - normal) < 32: # Compare to the closest side. Use ** to swap x/y arguments # appropriately. The closest side is the one with the # smallest magnitude. vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_min', origin=sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), ) vmf.create_ent( classname='info_null', targetname=inst['targetname'] + '_max', origin=sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), ) sign_dir = min( sign_floor_loc - Vec(**{ axis: side_min, other_axis: normal, }), sign_floor_loc - Vec(**{ axis: side_max, other_axis: normal, }), key=Vec.mag, ) else: # Align just based on whether we're in front or behind. sign_dir = Vec() sign_dir[other_axis] = sign_floor_loc[other_axis] - normal sign_angle = 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_angle = (sign_angle + 45) // 90 * 90 # Rotate to fit the instances - south is down sign_angle = int(sign_angle + 90) % 360 if inst_normal.z > 0: sign_angle = '0 {} 0'.format(sign_angle) elif inst_normal.z < 0: # Flip upside-down for ceilings sign_angle = '0 {} 180'.format(sign_angle) 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' if blue_enabled else 'blue_off_sign' if disable_other else None oran_sign = 'oran_sign' if oran_enabled else 'oran_off_sign' if disable_other else None if blue_sign: vmf.create_ent( classname='func_instance', file=res[blue_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=blue_loc.join(' '), ) if oran_sign: vmf.create_ent( classname='func_instance', file=res[oran_sign, ''], targetname=inst['targetname'], angles=sign_angle, origin=oran_loc.join(' '), ) # Now modify the fizzler... fizz_brushes = list(vmf.by_class['trigger_portal_cleanser'] & vmf.by_target[fizz_name + '_brush']) if 'base_inst' in res: fizz_base['file'] = instanceLocs.resolve_one(res['base_inst'], error=True) fizz_base.outputs.clear() # Remove outputs, otherwise they break # branch_toggle entities # Subtract the sign from the list of connections, but don't go below # zero fizz_base.fixup['$connectioncount'] = str( max(0, srctools.conv_int(fizz_base.fixup['$connectioncount', ''], 0) - 1)) if 'model_inst' in res: model_inst = instanceLocs.resolve_one(res['model_inst'], error=True) for mdl_inst in vmf.by_class['func_instance']: if mdl_inst['targetname', ''].startswith(fizz_name + '_model'): mdl_inst['file'] = model_inst # Find the direction the fizzler front/back points - z=floor fizz # Signs will associate with the given side! bbox_min, bbox_max = fizz_brushes[0].get_bbox() for axis, val in zip('xyz', bbox_max - bbox_min): if val == 2: fizz_axis = axis sign_center = (bbox_min[axis] + bbox_max[axis]) / 2 break else: # A fizzler that's not 128*x*2? raise Exception('Invalid fizzler brush ({})!'.format(fizz_name)) # 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_axis] < sign_center: pos_blue = blue_enabled pos_oran = oran_enabled else: neg_blue = blue_enabled neg_oran = oran_enabled fizz_off_tex = { 'left': res['off_left'], 'center': res['off_center'], 'right': res['off_right'], 'short': res['off_short'], } fizz_on_tex = { 'left': res['on_left'], 'center': res['on_center'], 'right': res['on_right'], 'short': res['on_short'], } # If it activates the paint gun, use different textures if pos_blue or pos_oran: pos_tex = fizz_on_tex else: pos_tex = fizz_off_tex if neg_blue or neg_oran: neg_tex = fizz_on_tex else: neg_tex = fizz_off_tex 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'] = fizz_base['origin'] pos_trig['spawnflags'] = neg_trig['spawnflags'] = '1' # Clients Only pos_trig['targetname'] = fizz_name + '-trig_pos' neg_trig['targetname'] = fizz_name + '-trig_neg' pos_trig.outputs = [ Output( output, fizz_name + '-trig_neg', 'Enable', ), Output( output, fizz_name + '-trig_pos', 'Disable', ), ] neg_trig.outputs = [ Output( output, fizz_name + '-trig_pos', 'Enable', ), Output( output, fizz_name + '-trig_neg', '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'] = fizz_name + '-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')) for fizz_brush in fizz_brushes: # portal_cleanser ent, not solid! # Modify fizzler textures bbox_min, bbox_max = fizz_brush.get_bbox() for side in fizz_brush.sides(): norm = side.normal() if norm[fizz_axis] == 0: # Not the front/back: force nodraw # Otherwise the top/bottom will have the odd stripes # which won't match the sides side.mat = 'tools/toolsnodraw' continue if norm[fizz_axis] == 1: side.mat = pos_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] else: side.mat = neg_tex[vbsp.TEX_FIZZLER[side.mat.casefold()]] # The fizzler shouldn't kill cubes fizz_brush['spawnflags'] = '1' fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global', 'StartShake', )) fizz_brush.outputs.append( Output( 'OnStartTouch', '@shake_global_sound', 'PlaySound', )) # The triggers are 8 units thick, 24 from the center # (-1 because fizzlers are 2 thick on each side). neg_min, neg_max = Vec(bbox_min), Vec(bbox_max) neg_min[fizz_axis] -= 23 neg_max[fizz_axis] -= 17 pos_min, pos_max = Vec(bbox_min), Vec(bbox_max) pos_min[fizz_axis] += 17 pos_max[fizz_axis] += 23 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_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. Values: File: The filename. Fixup Style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. Copy_Fixup: If true, all the $replace values from the original instance will be copied over. move_outputs: If true, outputs will be moved to this instance. offset: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. angles: If set, overrides the base instance angles. This does not affect the offset property. fixup/localfixup: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: folded_off = res['offset'].casefold() # Offset the overlay by the given distance # Some special placeholder values: if folded_off == '<piston_start>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_top>' else: folded_off = '<piston_bottom>' elif folded_off == '<piston_end>': if srctools.conv_bool(inst.fixup['$start_up', '']): folded_off = '<piston_bottom>' else: folded_off = '<piston_top>' if folded_off == '<piston_bottom>': offset = Vec(z=srctools.conv_int(inst.fixup['$bottom_level']) * 128, ) elif folded_off == '<piston_top>': offset = Vec(z=srctools.conv_int(inst.fixup['$top_level'], 1) * 128, ) else: # Regular vector offset = Vec.from_str(conditions.resolve_value( inst, res['offset'])) offset.rotate_by_str(inst['angles', '0 0 0']) overlay_inst['origin'] = offset + Vec.from_str(inst['origin']) return overlay_inst
def gen_item_outputs(vmf: VMF): """Create outputs for all items with connections. This performs an optimization pass over items with outputs to remove redundancy, then applies all the outputs to the instances. Before this, connection count and inversion values are not valid. After this point, items may not have connections altered. """ LOGGER.info('Generating item IO...') pan_switching_check = vbsp_options.get(PanelSwitchingStyle, 'ind_pan_check_switching') pan_switching_timer = vbsp_options.get(PanelSwitchingStyle, 'ind_pan_timer_switching') pan_check_type = ITEM_TYPES['item_indicator_panel'] pan_timer_type = ITEM_TYPES['item_indicator_panel_timer'] # Apply input A/B types to connections. # After here, all connections are primary or secondary only. for item in ITEMS.values(): for conn in item.outputs: # If not a dual item, it's primary. if conn.to_item.item_type.input_type is not InputType.DUAL: conn.type = ConnType.PRIMARY continue # If already set, that is the priority. if conn.type is not ConnType.DEFAULT: continue # Our item set the type of outputs. if item.item_type.output_type is not ConnType.DEFAULT: conn.type = item.item_type.output_type else: # Use the affinity of the target. conn.type = conn.to_item.item_type.default_dual do_item_optimisation(vmf) has_timer_relay = False # We go 'backwards', creating all the inputs for each item. # That way we can change behaviour based on item counts. for item in ITEMS.values(): if item.item_type is None: continue # Check we actually have timers, and that we want the relay. if item.timer is not None and (item.item_type.timer_sound_pos is not None or item.item_type.timer_done_cmd): has_sound = item.item_type.force_timer_sound or len( item.ind_panels) > 0 add_timer_relay(item, has_sound) has_timer_relay = has_timer_relay or has_sound # Add outputs for antlines. if item.antlines or item.ind_panels: if item.timer is None: add_item_indicators(item, pan_switching_check, pan_check_type) else: add_item_indicators(item, pan_switching_timer, pan_timer_type) if not item.inputs: continue if item.item_type.input_type is InputType.DUAL: prim_inputs = [ conn for conn in item.inputs if conn.type is ConnType.PRIMARY or conn.type is ConnType.BOTH ] sec_inputs = [ conn for conn in item.inputs if conn.type is ConnType.SECONDARY or conn.type is ConnType.BOTH ] add_item_inputs( item, InputType.AND, prim_inputs, const.FixupVars.BEE_CONN_COUNT_A, item.enable_cmd, item.disable_cmd, item.item_type.invert_var, ) add_item_inputs( item, InputType.AND, sec_inputs, const.FixupVars.BEE_CONN_COUNT_B, item.sec_enable_cmd, item.sec_disable_cmd, item.item_type.sec_invert_var, ) else: # If we have commands defined, try to add locking. if item.item_type.output_unlock is not None: add_locking(item) add_item_inputs( item, item.item_type.input_type, list(item.inputs), const.FixupVars.CONN_COUNT, item.enable_cmd, item.disable_cmd, item.item_type.invert_var, ) # Check/cross instances sometimes don't match the kind of timer delay. # We also might want to swap them out. panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) for item in ITEMS.values(): desired_panel_inst = panel_check if item.timer is None else panel_timer for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup[const.FixupVars.TIM_ENABLED] = item.timer is not None if has_timer_relay: # Write this VScript out. with open('BEE2/inject/timer_sound.nut', 'w') as f: f.write( TIMER_SOUND_SCRIPT.format( snd=vbsp_options.get(str, 'timer_sound'))) LOGGER.info('Item IO generated.')
def res_add_overlay_inst(inst: Entity, res: Property): """Add another instance on top of this one. If a single value, this sets only the filename. Values: `file`: The filename. `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. `copy_fixup`: If true, all the $replace values from the original instance will be copied over. `move_outputs`: If true, outputs will be moved to this instance. `offset`: The offset (relative to the base) that the instance will be placed. Can be set to '<piston_top>' and '<piston_bottom>' to offset based on the configuration. '<piston_start>' will set it to the starting position, and '<piston_end>' will set it to the ending position. of piston platform handles. `angles`: If set, overrides the base instance angles. This does not affect the offset property. `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. If the value starts with $, the variable will be copied over. If this is present, copy_fixup will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [ Property('File', res.value) ]) angle = res['angles', inst['angles', '0 0 0']] orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return overlay_inst = vbsp.VMF.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angle, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def res_conveyor_belt(inst: Entity, res: Property): """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. `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 = 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 = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] vmf = inst.map 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 = template_brush.import_template( rail_template, pos, angles, 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=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 calc_connections( vmf: VMF, shape_frame_tex: List[str], enable_shape_frame: bool, ): """Compute item connections from the map file. This also fixes cases where items have incorrect checkmark/timer signs. Instance Traits must have been calculated. It also applies frames to shape signage to distinguish repeats. """ # First we want to match targetnames to item types. toggles = {} # type: Dict[str, Entity] overlays = defaultdict(set) # type: Dict[str, Set[Entity]] # Accumulate all the signs into groups, so the list should be 2-long: # sign_shapes[name, material][0/1] sign_shape_overlays = defaultdict(list) # type: Dict[Tuple[str, str], List[Entity]] panels = {} # type: Dict[str, Entity] panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) tbeam_polarity = { conditions.TBEAM_CONN_ACT, conditions.TBEAM_CONN_DEACT, } tbeam_io = conditions.CONNECTIONS['item_tbeam'] tbeam_io = {tbeam_io.in_act, tbeam_io.in_deact} for inst in vmf.by_class['func_instance']: inst_name = inst['targetname'] if not inst_name: continue traits = instance_traits.get(inst) if 'indicator_toggle' in traits: toggles[inst['targetname']] = inst elif 'indicator_panel' in traits: panels[inst['targetname']] = inst else: ITEMS[inst_name] = Item(inst) for over in vmf.by_class['info_overlay']: name = over['targetname'] mat = over['material'] if mat in SIGN_ORDER_LOOKUP: sign_shape_overlays[name, mat.casefold()].append(over) else: # Antlines overlays[name].add(over) # Name -> signs pairs sign_shapes = defaultdict(list) # type: Dict[str, List[ShapeSignage]] # By material index, for group frames. sign_shape_by_index = defaultdict(list) # type: Dict[int, List[ShapeSignage]] for (name, mat), sign_pair in sign_shape_overlays.items(): # It's possible - but rare - for more than 2 to be in a pair. # We have to just treat them as all in their 'pair'. # Shouldn't be an issue, it'll be both from one item... shape = ShapeSignage(sign_pair) sign_shapes[name].append(shape) sign_shape_by_index[shape.index].append(shape) # Now build the connections and items. for item in ITEMS.values(): input_items = [] # Instances we trigger inputs = defaultdict(list) # type: Dict[str, List[Output]] for out in item.inst.outputs: inputs[out.target].append(out) # inst.outputs.clear() for out_name in inputs: # Fizzler base -> model/brush outputs, skip and readd. if out_name.endswith(('_modelStart', '_modelEnd', '_brush')): # item.inst.add_out(*inputs[out_name]) continue if out_name in toggles: inst_toggle = toggles[out_name] item.antlines |= overlays[inst_toggle.fixup['indicator_name']] elif out_name in panels: pan = panels[out_name] item.ind_panels.add(pan) if pan.fixup.bool('$is_timer'): item.timer = tim = pan.fixup.int('$timer_delay') if not (1 <= tim <= 30): # These would be infinite. item.timer = None else: item.timer = None else: try: input_items.append(ITEMS[out_name]) except KeyError: raise ValueError('"{}" is not a known instance!'.format(out_name)) desired_panel_inst = panel_check if item.timer is None else panel_timer # Check/cross instances sometimes don't match the kind of timer delay. for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup['$is_timer'] = int(item.timer is not None) for inp_item in input_items: # type: Item # Default A/B type. conn_type = ConnType.DEFAULT in_outputs = inputs[inp_item.name] if 'tbeam_emitter' in inp_item.traits: # It's a funnel - we need to figure out if this is polarity, # or normal on/off. for out in in_outputs: # type: Output input_tuple = (out.inst_in, out.input) if input_tuple in tbeam_polarity: conn_type = ConnType.TBEAM_DIR break elif input_tuple in tbeam_io: conn_type = ConnType.TBEAM_IO break else: raise ValueError( 'Excursion Funnel "{}" has inputs, ' 'but no valid types!'.format(inp_item.name) ) conn = Connection( inp_item, item, conn_type, in_outputs, ) conn.add() for item in ITEMS.values(): # Copying items can fail to update the connection counts. # Make sure they're correct. if '$connectioncount' in item.inst.fixup: # Don't count the polarity outputs... item.inst.fixup['$connectioncount'] = sum( 1 for conn in item.inputs if conn.type is not ConnType.TBEAM_DIR ) if '$connectioncount_polarity' in item.inst.fixup: # Only count the polarity outputs... item.inst.fixup['$connectioncount_polarity'] = sum( 1 for conn in item.inputs if conn.type is ConnType.TBEAM_DIR ) # Make signage frames shape_frame_tex = [mat for mat in shape_frame_tex if mat] if shape_frame_tex and enable_shape_frame: for shape_mat in sign_shape_by_index.values(): # Sort by name, so which gets what frame is consistent shape_mat.sort(key=lambda shape: shape.name) for index, shape in enumerate(shape_mat): shape.repeat_group = index if index == 0: continue # First, no frames.. frame_mat = shape_frame_tex[(index-1) % len(shape_frame_tex)] for overlay in shape: frame = overlay.copy() shape.overlay_frames.append(frame) vmf.add_ent(frame) frame['material'] = frame_mat frame['renderorder'] = 1 # On top
def res_fizzler_pair(vmf: VMF, begin_inst: Entity, res: Property): """Modify the instance of a fizzler to link with its pair. Each pair will be given a name along the lines of "fizz_name-model1334". Values: - StartInst, EndInst: The instances used for each end - MidInst: An instance placed every 128 units between emitters. - SingleInst: If the models are 1 block apart, replace both with this instance. - BrushKeys, LocalBrushKeys: If specified, a brush entity will be generated from some templates at the position of the models. - StartTemp, EndTemp, SingleTemp: Templates for the above. - SingleBrush: If true, the brush will be shared among the entirety of this fizzler. - uniqueName: If true, all pairs get a unique name for themselves. if False, all instances use the base instance name. """ orig_target = begin_inst['targetname'] if 'modelEnd' in orig_target: return # We only execute starting from the start side. orig_target = orig_target[:-11] # remove "_modelStart" end_name = orig_target + '_modelEnd' # What we search for # The name all these instances get if srctools.conv_bool(res['uniqueName', '1'], True): pair_name = orig_target + '-model' + str(begin_inst.id) else: pair_name = orig_target orig_file = begin_inst['file'] begin_inst['file'] = instanceLocs.resolve_one(res['StartInst'], error=True) end_file = instanceLocs.resolve_one(res['EndInst'], error=True) mid_file = instanceLocs.resolve_one(res['MidInst', '']) single_file = instanceLocs.resolve_one(res['SingleInst', '']) begin_inst['targetname'] = pair_name brush = None if 'brushkeys' in res: begin_temp = res['StartTemp', ''] end_temp = res['EndTemp', ''] single_temp = res['SingleTemp'] if res.bool('SingleBrush'): try: brush = PAIR_FIZZ_BRUSHES[orig_target] except KeyError: pass if not brush: brush = vmf.create_ent( classname='func_brush', # default origin=begin_inst['origin'], ) conditions.set_ent_keys( brush, begin_inst, res, 'BrushKeys', ) if res.bool('SingleBrush'): PAIR_FIZZ_BRUSHES[orig_target] = brush else: begin_temp = end_temp = single_temp = None direction = Vec(0, 0, 1).rotate_by_str(begin_inst['angles']) begin_pos = Vec.from_str(begin_inst['origin']) axis_1, axis_2, main_axis = PAIR_AXES[direction.as_tuple()] for end_inst in vbsp.VMF.by_class['func_instance']: if end_inst['targetname', ''] != end_name: # Only examine this barrier hazard's instances! continue if end_inst['file'] != orig_file: # Allow adding overlays or other instances at the ends. continue end_pos = Vec.from_str(end_inst['origin']) if (begin_pos[axis_1] == end_pos[axis_1] and begin_pos[axis_2] == end_pos[axis_2]): length = int(end_pos[main_axis] - begin_pos[main_axis]) break else: LOGGER.warning('No matching pair for {}!!', orig_target) return if length == 0: if single_temp: temp_brushes = template_brush.import_template( single_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if single_file: end_inst.remove() begin_inst['file'] = single_file # Don't do anything else with end instances. return else: if begin_temp: temp_brushes = template_brush.import_template( begin_temp, Vec.from_str(begin_inst['origin']), Vec.from_str(begin_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) if end_temp: temp_brushes = template_brush.import_template( end_temp, Vec.from_str(end_inst['origin']), Vec.from_str(end_inst['angles']), force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) brush.solids.extend(temp_brushes.world) end_inst['targetname'] = pair_name end_inst['file'] = end_file if mid_file != '' and length: # Go 64 from each side, and always have at least 1 section # A 128 gap will have length = 0 for dis in range(0, abs(length) + 1, 128): new_pos = begin_pos + direction * dis vbsp.VMF.create_ent( classname='func_instance', targetname=pair_name, angles=begin_inst['angles'], file=mid_file, origin=new_pos, )