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): """Pre-parse the data for cameras.""" 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['PitchRange', ''], 90), }
def resolve_optional(prop: Property, key: str) -> str: """Resolve the given instance, or return '' if not defined.""" try: file = prop[key] except LookupError: return '' return instanceLocs.resolve_one(file) or ''
def parse_map(vmf: VMF, has_attr: Dict[str, bool]) -> None: """Find all glass/grating in the map. This removes the per-tile instances, and all original brushwork. The frames are updated with a fixup var, as appropriate. """ frame_inst = resolve('[glass_frames]', silent=True) 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() elif filename in frame_inst: # Add a fixup to allow distinguishing the type. pos = Vec.from_str(inst['origin']) // 128 * 128 + (64, 64, 64) norm = Vec(z=-1) @ Angle.from_str(inst['angles']) try: inst.fixup[consts.FixupVars.BEE_GLS_TYPE] = BARRIERS[ pos.as_tuple(), norm.as_tuple()].value except KeyError: LOGGER.warning('No glass/grating for frame at {}, {}?', pos, norm) if options.get(str, 'glass_pack') and has_attr['glass']: packing.pack_list(vmf, options.get(str, 'glass_pack'))
def res_add_global_inst(vmf: VMF, 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)]) file = instanceLocs.resolve_one(res['file'], error=True) if res.bool('allow_multiple') or file.casefold() not in conditions.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 = vmf.create_ent( classname="func_instance", targetname=res['name', ''], file=file, angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = options.get(Vec, 'global_ents_loc') conditions.GLOBAL_INSTANCES.add(file.casefold()) conditions.ALL_INST.add(file.casefold()) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return conditions.RES_EXHAUSTED
def init_seed(vmf: VMF) -> str: """Seed with the map layout. We use the position of the ambient light instances, which is unique to any given layout, but small changes won't change since only every 4th grid pos is relevant. """ amb_light = instanceLocs.resolve_one('<ITEM_POINT_LIGHT>', error=True) light_names = [] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() == amb_light: pos = Vec.from_str(inst['origin']) / 64 light_names.append( THREE_INTS.pack(round(pos.x), round(pos.y), round(pos.z))) light_names.sort() # Ensure consistent order! for name in light_names: MAP_HASH.update(name) LOGGER.debug('Map random seed: {}', MAP_HASH.hexdigest()) return b'|'.join(light_names).decode() # TODO Remove
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 options.get(str, 'glass_pack') and has_attr['glass']: packing.pack_list(vmf, options.get(str, 'glass_pack'))
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_add_overlay_inst(vmf: VMF, inst: Entity, res: Property) -> Optional[Entity]: """Add another instance on top of this one. If a single value, this sets only the filename. Values: - `file`: The filename. - `fixup_style`: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. - `copy_fixup`: If true, all the `$replace` values from the original instance will be copied over. - `move_outputs`: If true, outputs will be moved to this instance. - `offset`: The offset (relative to the base) that the instance will be placed. Can be set to `<piston_top>` and `<piston_bottom>` to offset based on the configuration. `<piston_start>` will set it to the starting position, and `<piston_end>` will set it to the ending position of the Piston Platform's handles. - `rotation`: Rotate the instance by this amount. - `angles`: If set, overrides `rotation` and the instance angles entirely. - `fixup`/`localfixup`: Keyvalues in this block will be copied to the overlay entity. - If the value starts with `$`, the variable will be copied over. - If this is present, `copy_fixup` will be disabled. """ if not res.has_children(): # Use all the defaults. res = Property('AddOverlay', [Property('File', res.value)]) if 'angles' in res: angles = Angle.from_str(res['angles']) if 'rotation' in res: LOGGER.warning('"angles" option overrides "rotation"!') else: angles = Angle.from_str(res['rotation', '0 0 0']) angles @= Angle.from_str(inst['angles', '0 0 0']) orig_name = conditions.resolve_value(inst, res['file', '']) filename = instanceLocs.resolve_one(orig_name) if not filename: if not res.bool('silentLookup'): LOGGER.warning('Bad filename for "{}" when adding overlay!', orig_name) # Don't bother making a overlay which will be deleted. return None overlay_inst = vmf.create_ent( classname='func_instance', targetname=inst['targetname', ''], file=filename, angles=angles, origin=inst['origin'], fixup_style=res['fixup_style', '0'], ) # Don't run if the fixup block exists.. if srctools.conv_bool(res['copy_fixup', '1']): if 'fixup' not in res and 'localfixup' not in res: # Copy the fixup values across from the original instance for fixup, value in inst.fixup.items(): overlay_inst.fixup[fixup] = value conditions.set_ent_keys(overlay_inst.fixup, inst, res, 'fixup') if res.bool('move_outputs', False): overlay_inst.outputs = inst.outputs inst.outputs = [] if 'offset' in res: overlay_inst['origin'] = conditions.resolve_offset(inst, res['offset']) return overlay_inst
def gen_item_outputs(vmf: VMF) -> None: """Create outputs for all items with connections. This performs an optimization pass over items with outputs to remove redundancy, then applies all the outputs to the instances. Before this, connection count and inversion values are not valid. After this point, items may not have connections altered. """ LOGGER.info('Generating item IO...') pan_switching_check = options.get(PanelSwitchingStyle, 'ind_pan_check_switching') pan_switching_timer = options.get(PanelSwitchingStyle, 'ind_pan_timer_switching') pan_check_type = ITEM_TYPES['item_indicator_panel'] pan_timer_type = ITEM_TYPES['item_indicator_panel_timer'] # For logic items without inputs, collect the instances to fix up later. dummy_logic_ents: list[Entity] = [] # Apply input A/B types to connections. # After here, all connections are primary or secondary only. for item in ITEMS.values(): for conn in item.outputs: # If not a dual item, it's primary. if conn.to_item.config.input_type is not InputType.DUAL: conn.type = ConnType.PRIMARY continue # If already set, that is the priority. if conn.type is not ConnType.DEFAULT: continue # Our item set the type of outputs. if item.config.output_type is not ConnType.DEFAULT: conn.type = item.config.output_type else: # Use the affinity of the target. conn.type = conn.to_item.config.default_dual do_item_optimisation(vmf) # We go 'backwards', creating all the inputs for each item. # That way we can change behaviour based on item counts. for item in ITEMS.values(): if item.config is None: continue # Try to add the locking IO. add_locking(item) # Check we actually have timers, and that we want the relay. if item.timer is not None and (item.config.timer_sound_pos is not None or item.config.timer_done_cmd): has_sound = item.config.force_timer_sound or len( item.ind_panels) > 0 add_timer_relay(item, has_sound) # Add outputs for antlines. if item.antlines or item.ind_panels: if item.timer is None: add_item_indicators(item, pan_switching_check, pan_check_type) else: add_item_indicators(item, pan_switching_timer, pan_timer_type) if item.config.input_type is InputType.DUAL: prim_inputs = [ conn for conn in item.inputs if conn.type is ConnType.PRIMARY or conn.type is ConnType.BOTH ] sec_inputs = [ conn for conn in item.inputs if conn.type is ConnType.SECONDARY or conn.type is ConnType.BOTH ] add_item_inputs( dummy_logic_ents, item, InputType.AND, prim_inputs, consts.FixupVars.BEE_CONN_COUNT_A, item.enable_cmd, item.disable_cmd, item.config.invert_var, item.config.spawn_fire, '_prim_inv_rl', ) add_item_inputs( dummy_logic_ents, item, InputType.AND, sec_inputs, consts.FixupVars.BEE_CONN_COUNT_B, item.sec_enable_cmd, item.sec_disable_cmd, item.config.sec_invert_var, item.config.sec_spawn_fire, '_sec_inv_rl', ) else: add_item_inputs( dummy_logic_ents, item, item.config.input_type, list(item.inputs), consts.FixupVars.CONN_COUNT, item.enable_cmd, item.disable_cmd, item.config.invert_var, item.config.spawn_fire, '_inv_rl', ) # Check/cross instances sometimes don't match the kind of timer delay. # We also might want to swap them out. panel_timer = instanceLocs.resolve_one('[indPanTimer]', error=True) panel_check = instanceLocs.resolve_one('[indPanCheck]', error=True) for item in ITEMS.values(): desired_panel_inst = panel_check if item.timer is None else panel_timer for pan in item.ind_panels: pan['file'] = desired_panel_inst pan.fixup[consts.FixupVars.TIM_ENABLED] = item.timer is not None logic_auto = vmf.create_ent('logic_auto', origin=options.get(Vec, 'global_ents_loc')) for ent in dummy_logic_ents: # Condense all these together now. # User2 is the one that enables the target. ent.remove() for out in ent.outputs: if out.output == 'OnUser2': out.output = 'OnMapSpawn' logic_auto.add_out(out) out.only_once = True LOGGER.info('Item IO generated.')
def res_change_instance(inst: Entity, res: Property): """Set the file to a value.""" inst['file'] = instanceLocs.resolve_one(res.value, error=True)
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. * Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. * `up_axis` should be set to a normal vector pointing in the new 'upward' direction. * If none are connected, a regular fizzler will be synthesized. The following fixup vars will be set to allow the shape to match the fizzler: * `$uses_nodraw` will be 1 if the fizzler nodraws surfaces behind it. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS.pop(shape_name) shape_orient = Matrix.from_angle(Angle.from_str(shape_inst['angles'])) up_axis: Vec = round(res.vec('up_axis') @ shape_orient, 6) for conn in shape_item.outputs: fizz_item = conn.to_item try: fizz = fizzler.FIZZLERS[fizz_item.name] except KeyError: continue # Detach this connection and remove traces of it. conn.remove() fizz.emitters.clear() # Remove old positions. fizz.up_axis = up_axis fizz.base_inst['origin'] = shape_inst['origin'] fizz.base_inst['angles'] = shape_inst['angles'] break else: # No fizzler, so generate a default. # We create the fizzler instance, Fizzler object, and Item object # matching it. # This is hardcoded to use regular Emancipation Fields. base_inst = conditions.add_inst( vmf, targetname=shape_name, origin=shape_inst['origin'], angles=shape_inst['angles'], file=resolve_one('<ITEM_BARRIER_HAZARD:fizz_base>'), ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizzler.FIZZ_TYPES['VALVE_MATERIAL_EMANCIPATION_GRID'], up_axis, base_inst, [], ) fizz_item = connections.Item( base_inst, connections.ITEM_TYPES['item_barrier_hazard'], ant_floor_style=shape_item.ant_floor_style, ant_wall_style=shape_item.ant_wall_style, ) connections.ITEMS[shape_name] = fizz_item # Transfer the input/outputs from us to the fizzler. for inp in list(shape_item.inputs): inp.to_item = fizz_item for conn in list(shape_item.outputs): conn.from_item = fizz_item # If the fizzler has no outputs, then strip out antlines. Otherwise, # they need to be transferred across, so we can't tell safely. if fizz_item.output_act() is None and fizz_item.output_deact() is None: shape_item.delete_antlines() else: shape_item.transfer_antlines(fizz_item) fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) fizz.has_cust_position = True # Since the fizzler is moved elsewhere, it's the responsibility of # the new item to have holes. fizz.embedded = False # So tell it whether or not it needs to do so. shape_inst.fixup['$uses_nodraw'] = fizz.fizz_type.nodraw_behind for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1) @ shape_orient + origin, Vec.from_str(vec2) @ shape_orient + origin, ) fizz.emitters.append(seg_min_max)
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 conditions.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'] = filename = instances[CATWALK_TYPES[new_type]] conditions.ALL_INST.add(filename.casefold()) 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'] conditions.ALL_INST.add(instances['end_wall'].casefold()) 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'] conditions.ALL_INST.add(instances['single_wall'].casefold()) inst['angles'] = conditions.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: conditions.add_inst( vmf, origin=inst['origin'], angles=conditions.INST_ANGLE[normal.as_tuple()], file=supp, ) LOGGER.info('Finished catwalk generation!') return conditions.RES_EXHAUSTED
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 conditions.ALL_INST.add(single_plat_inst.casefold()) first_track.remove() continue # Next platform track_set: set[Entity] = 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 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 make_static_pist(vmf: srctools.VMF, res: Property) -> Callable[[Entity], None]: """Convert a regular piston into a static version. This is done to save entities and improve lighting. If changed to static pistons, the $bottom and $top level become equal. Instances: Bottom_1/2/3: Moving piston with the given $bottom_level Logic_0/1/2/3: Additional logic instance for the given $bottom_level Static_0/1/2/3/4: A static piston at the given height. Alternatively, specify all instances via editoritems, by setting the value to the item ID optionally followed by a :prefix. """ inst_keys = ( '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 instances = { name: instanceLocs.resolve_one( res[name, ''], error=False, ) for name in inst_keys } else: # Pull from editoritems if ':' in res.value: from_item, prefix = res.value.split(':', 1) else: from_item = res.value prefix = '' instances = { name: instanceLocs.resolve_one( '<{}:bee2_{}{}>'.format(from_item, prefix, name), error=False, ) for name in inst_keys } def make_static(ent: Entity) -> None: """Make a piston static.""" bottom_pos = ent.fixup.int(consts.FixupVars.PIST_BTM, 0) if ( ent.fixup.int(consts.FixupVars.CONN_COUNT) > 0 or ent.fixup.bool(consts.FixupVars.DIS_AUTO_DROP) ): # can it move? ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = True # Use instances based on the height of the bottom position. val = instances['bottom_' + str(bottom_pos)] if val: # Only if defined ent['file'] = val logic_file = instances['logic_' + str(bottom_pos)] if logic_file: # Overlay an additional logic file on top of the original # piston. This allows easily splitting the piston logic # from the styled components logic_ent = ent.copy() logic_ent['file'] = logic_file vmf.add_ent(logic_ent) # If no connections are present, set the 'enable' value in # the logic to True so the piston can function logic_ent.fixup[consts.FixupVars.BEE_PIST_MANAGER_A] = ( ent.fixup.int(consts.FixupVars.CONN_COUNT) == 0 ) else: # we are static ent.fixup[consts.FixupVars.BEE_PIST_IS_STATIC] = False if ent.fixup.bool(consts.FixupVars.PIST_IS_UP): pos = bottom_pos = ent.fixup.int(consts.FixupVars.PIST_TOP, 1) else: pos = bottom_pos ent.fixup[consts.FixupVars.PIST_TOP] = ent.fixup[consts.FixupVars.PIST_BTM] = pos val = instances['static_' + str(pos)] if val: ent['file'] = val # Add in the grating for the bottom as an overlay. # It's low to fit the piston at minimum, or higher if needed. grate = instances[ 'grate_high' if bottom_pos > 0 else 'grate_low' ] if grate: grate_ent = ent.copy() grate_ent['file'] = grate vmf.add_ent(grate_ent) return make_static
def res_change_instance(inst: Entity, res: Property): """Set the file to a value.""" inst['file'] = filename = instanceLocs.resolve_one(res.value, error=True) conditions.ALL_INST.add(filename.casefold())