def flag_has_inst(flag: Property): """Checks if the given instance is present anywhere in the map.""" flags = instanceLocs.resolve(flag.value) return any( inst.casefold() in flags for inst in ALL_INST )
def res_unst_scaffold_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in SCAFFOLD_CONFIGS: # Store our values in the CONFIGS dictionary targ_inst, links = SCAFFOLD_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them targ_inst, links = SCAFFOLD_CONFIGS[group] for block in res.find_all("Instance"): conf = { # If set, adjusts the offset appropriately 'is_piston': srctools.conv_bool(block['isPiston', '0']), 'rotate_logic': srctools.conv_bool(block['AlterAng', '1'], True), 'off_floor': Vec.from_str(block['FloorOff', '0 0 0']), 'off_wall': Vec.from_str(block['WallOff', '0 0 0']), 'logic_start': block['startlogic', ''], 'logic_end': block['endLogic', ''], 'logic_mid': block['midLogic', ''], 'logic_start_rev': block['StartLogicRev', None], 'logic_end_rev': block['EndLogicRev', None], 'logic_mid_rev': block['EndLogicRev', None], 'inst_wall': block['wallInst', ''], 'inst_floor': block['floorInst', ''], 'inst_offset': block['offsetInst', None], # Specially rotated to face the next track! 'inst_end': block['endInst', None], } for logic_type in ('logic_start', 'logic_mid', 'logic_end'): if conf[logic_type + '_rev'] is None: conf[logic_type + '_rev'] = conf[logic_type] for inst in instanceLocs.resolve(block['file']): targ_inst[inst] = conf # We need to provide vars to link the tracks and beams. for block in res.find_all('LinkEnt'): # The name for this set of entities. # It must be a '@' name, or the name will be fixed-up incorrectly! loc_name = block['name'] if not loc_name.startswith('@'): loc_name = '@' + loc_name links[block['nameVar']] = { 'name': loc_name, # The next entity (not set in end logic) 'next': block['nextVar'], # A '*' name to reference all the ents (set on the start logic) 'all': block['allVar', None], } return group # We look up the group name to find the values.
def res_vactube_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in VAC_CONFIGS: # Store our values in the CONFIGS dictionary config, inst_configs = VAC_CONFIGS[group] = {}, {} else: # Grab the already-filled values, and add to them config, inst_configs = VAC_CONFIGS[group] for block in res.find_all("Instance"): # Configuration info for each instance set.. conf = { # The three sizes of corner instance ('corner', 1): block['corner_small_inst', ''], ('corner', 2): block['corner_medium_inst', ''], ('corner', 3): block['corner_large_inst', ''], ('corner_temp', 1): block['temp_corner_small', ''], ('corner_temp', 2): block['temp_corner_medium', ''], ('corner_temp', 3): block['temp_corner_large', ''], # Straight instances connected to the next part 'straight': block['straight_inst', ''], # Supports attach to the 4 sides of the straight part, # if there's a brush there. 'support': block['support_inst', ''], 'is_tsection': srctools.conv_bool(block['is_tsection', '0']), ('entry', 'wall'): block['entry_inst'], ('entry', 'floor'): block['entry_floor_inst'], ('entry', 'ceiling'): block['entry_ceil_inst'], 'exit': block['exit_inst'], } for prop in block.find_all("File"): try: size, file = prop.value.split(":", 1) except ValueError: size = 1 file = prop.value for inst in instanceLocs.resolve(file): inst_configs[inst] = conf, srctools.conv_int(size, 1) return group
def mon_remove_bullseyes(inst: Entity) -> Optional[object]: """Remove bullsyes used for cameras.""" if not BULLSYE_LOCS: return RES_EXHAUSTED if inst['file'].casefold() not in instanceLocs.resolve('<ITEM_CATAPULT_TARGET>'): return origin = Vec(0, 0, -64) origin.localise(Vec.from_str(inst['origin']), Vec.from_str(inst['angles'])) origin = origin.as_tuple() LOGGER.info('Pos: {} -> ', origin, BULLSYE_LOCS[origin]) if BULLSYE_LOCS[origin]: BULLSYE_LOCS[origin] -= 1 inst.remove()
def parse(cls, conf: Property): """Read in a fizzler from a config.""" fizz_id = conf['id'] item_ids = [ prop.value.casefold() for prop in conf.find_all('item_id') ] try: model_name_type = ModelName(conf['NameType', 'same'].casefold()) except ValueError: LOGGER.warning('Bad model name type: "{}"', conf['NameType']) model_name_type = ModelName.SAME model_local_name = conf['ModelName', ''] if not model_local_name: # We can't rename without a local name. model_name_type = ModelName.SAME inst = {} for inst_type, is_static in itertools.product(FizzInst, (False, True)): inst_type_name = inst_type.value + ('_static' if is_static else '') inst[inst_type, is_static] = instances = [ file for prop in conf.find_all(inst_type_name) for file in instanceLocs.resolve(prop.value) ] # Allow specifying weights to bias model locations weights = conf[inst_type_name + '_weight', ''] if weights: # Produce the weights, then process through the original # list to build a new one with repeated elements. inst[inst_type, is_static] = instances = [ instances[i] for i in conditions.weighted_random(len(instances), weights) ] # If static versions aren't given, reuse non-static ones. # We do False, True so it's already been calculated. if not instances and is_static: inst[inst_type, True] = inst[inst_type, False] if not inst[FizzInst.BASE, False]: LOGGER.warning('No base instance set! for "{}"!', fizz_id) voice_attrs = [] for prop in conf.find_all('Has'): if prop.has_children(): for child in prop: voice_attrs.append(child.name.casefold()) else: voice_attrs.append(prop.value.casefold()) pack_lists = { prop.value for prop in conf.find_all('Pack') } pack_lists_static = { prop.value for prop in conf.find_all('PackStatic') } brushes = [ FizzlerBrush.parse(prop) for prop in conf.find_all('Brush') ] beams = [] # type: List[FizzBeam] for beam_prop in conf.find_all('Beam'): offsets = [ Vec.from_str(off.value) for off in beam_prop.find_all('pos') ] keys = Property('', [ beam_prop.find_key('Keys', []), beam_prop.find_key('LocalKeys', []) ]) beams.append(FizzBeam( offsets, keys, beam_prop.int('RandSpeedMin', 0), beam_prop.int('RandSpeedMax', 0), )) try: temp_conf = conf.find_key('TemplateBrush') except NoKeyError: temp_brush_keys = temp_min = temp_max = temp_single = None else: temp_brush_keys = Property('--', [ temp_conf.find_key('Keys'), temp_conf.find_key('LocalKeys', []), ]) # Find and load the templates. temp_min = temp_conf['Left', None] temp_max = temp_conf['Right', None] temp_single = temp_conf['Single', None] return FizzlerType( fizz_id, item_ids, voice_attrs, pack_lists, pack_lists_static, model_local_name, model_name_type, brushes, beams, inst, temp_brush_keys, temp_min, temp_max, temp_single, )
def flag_file_equal(inst: Entity, flag: Property): """Evaluates True if the instance matches the given file.""" return inst['file'].casefold() in instanceLocs.resolve(flag.value)
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool], pack_list: Set[str]): """Analyse fizzler instances to assign fizzler types. Instance traits are required. The model instances and brushes will be removed from the map. """ # Item ID and model skin -> fizzler type fizz_types = {} # type: Dict[Tuple[str, int], FizzlerType] for fizz_type in FIZZ_TYPES.values(): for item_id in fizz_type.item_ids: if ':' in item_id: item_id, barrier_type = item_id.split(':') if barrier_type == 'laserfield': barrier_skin = 2 elif barrier_type == 'fizzler': barrier_skin = 0 else: LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id) fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type continue fizz_types[item_id, barrier_skin] = fizz_type else: fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type fizz_bases = {} # type: Dict[str, Entity] fizz_models = defaultdict(list) # type: Dict[str, List[Entity]] # Position and normal -> name, for output relays. fizz_pos = { } # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str] # First use traits to gather up all the instances. for inst in vmf.by_class['func_instance']: traits = instance_traits.get(inst) if 'fizzler' not in traits: continue name = inst['targetname'] if 'fizzler_model' in traits: name = name.rsplit('_model', 1)[0] fizz_models[name].append(inst) inst.remove() elif 'fizzler_base' in traits: fizz_bases[name] = inst else: LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name) continue origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) fizz_pos[origin.as_tuple(), normal.as_tuple()] = name for name, base_inst in fizz_bases.items(): models = fizz_models[name] up_axis = Vec(y=1).rotate_by_str(base_inst['angles']) # If upside-down, make it face upright. if up_axis == (0, 0, -1): up_axis = Vec(z=1) base_inst.outputs.clear() # Now match the pairs of models to each other. # The length axis is the line between them. # We don't care about the instances after this, so don't keep track. length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis() emitters = [] # type: List[Tuple[Vec, Vec]] model_pairs = {} # type: Dict[Tuple[float, float], Vec] model_skin = models[0].fixup.int('$skin') try: item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[ base_inst['file'].casefold()] fizz_type = fizz_types[item_id, model_skin] except KeyError: LOGGER.warning('Fizzler types: {}', fizz_types.keys()) raise ValueError('No fizzler type for "{}"!'.format( base_inst['file'], )) from None for attr_name in fizz_type.voice_attrs: voice_attrs[attr_name] = True for model in models: pos = Vec.from_str(model['origin']) try: other_pos = model_pairs.pop(pos.other_axes(length_axis)) except KeyError: # No other position yet, we need to find that. model_pairs[pos.other_axes(length_axis)] = pos continue min_pos, max_pos = Vec.bbox(pos, other_pos) # Move positions to the wall surface. min_pos[length_axis] -= 64 max_pos[length_axis] += 64 emitters.append((min_pos, max_pos)) FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters) # Delete all the old brushes associated with fizzlers for brush in (vmf.by_class['trigger_portal_cleanser'] | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush']): name = brush['targetname'] if not name: continue name = name.rsplit('_brush')[0] if name in FIZZLERS: brush.remove() # Check for fizzler output relays. relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>') if not relay_file: # No relay item - deactivated most likely. return for inst in vmf.by_class['func_instance']: filename = inst['file'].casefold() if filename not in relay_file: continue inst.remove() try: fizz_name = fizz_pos[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] except KeyError: # Not placed on a fizzler... continue fizz = FIZZLERS[fizz_name] # Copy over fixup values fizz.base_inst.fixup.update(inst.fixup) for out in inst.outputs: new_out = out.copy() if out.output == 'ON' and fizz.fizz_type.out_activate is not None: new_out.inst_out, new_out.output = fizz.fizz_type.out_activate elif out.output == 'OFF' and fizz.fizz_type.out_deactivate is not None: new_out.inst_out, new_out.output = fizz.fizz_type.out_deactivate else: # Not the marker's output somehow? continue fizz.base_inst.add_out(new_out)
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 fizzler_out_relay(): """Link fizzlers with a relay item so they can be given outputs.""" relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>') if not relay_file: # No relay item - deactivated most likely. return # instances fizz_models = set() # base -> connections fizz_bases = {} LOGGER.info('Item classes: {}', ITEM_CLASSES) for fizz_id in ITEM_CLASSES['itembarrierhazard']: base, model = instanceLocs.resolve( '<{}: fizz_base, fizz_model>'.format(fizz_id) ) fizz_bases[base.casefold()] = CONNECTIONS[fizz_id] fizz_models.add(model.casefold()) # targetname -> base inst, connections fizz_by_name = {} # origin, normal -> targetname pos_to_name = {} marker_inst = [] # type: List[Entity] LOGGER.info('Fizzler data: {}', locals()) for inst in vbsp.VMF.by_class['func_instance']: filename = inst['file'].casefold() name = inst['targetname'] if filename in fizz_bases: fizz_by_name[inst['targetname']] = inst, fizz_bases[filename] elif filename in fizz_models: if inst['targetname'].endswith(('_modelStart', '_modelEnd')): name = inst['targetname'].rsplit('_', 1)[0] elif filename in relay_file: marker_inst.append(inst) # Remove the marker, we don't need that... inst.remove() continue else: continue pos_to_name[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple() ] = name for inst in marker_inst: try: fizz_name = pos_to_name[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple() ] except KeyError: # Not placed on a fizzler... continue base_inst, connections = fizz_by_name[fizz_name] # Copy over fixup values for key, val in inst.fixup.items(): base_inst.fixup[key] = val for out in inst.outputs: new_out = out.copy() if out.output == 'ON': new_out.inst_out, new_out.output = connections.out_act elif out.output == 'OFF': new_out.inst_out, new_out.output = connections.out_deact else: # Not the marker's output somehow? continue base_inst.add_out(new_out)
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. * `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 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_cutout_tile(vmf: srctools.VMF, res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - `MarkerItem` is the instance file to look for (`<ITEM_BEE2_CUTOUT_TILE>`) - `floor_chance`: The percentage change for a segment in the middle of the floor to be a normal tile. - `floor_glue_chance`: The chance for any tile to be glue - this should be higher than the regular chance, as that overrides this. - `rotateMax` is the maximum angle to rotate squarebeam models. - `squarebeamsSkin` sets the skin to use for the squarebeams floor frame. - `dispBase`, if true makes the floor a displacement with random alpha. - `Materials` blocks specify the possible materials to use: - `squarebeams` is the squarebeams variant to use. - `ceilingwalls` are the sides of the ceiling section. - `floorbase` is the texture under floor sections. If `dispBase` is True this is a displacement material. - `tile_glue` is used on top of a thinner tile segment. - `clip` is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) """ marker_filenames = instanceLocs.resolve(res['markeritem']) x: float y: float max_x: float max_y: float INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_locs = set() # If any signage is present in the map, we need to force tiles to # appear at that location! for over in vmf.by_class['info_overlay']: if (over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1')): add_signage_loc(sign_locs, Vec.from_str(over['origin'])) for item in connections.ITEMS.values(): for ind_pan in item.ind_panels: loc = Vec(0, 0, -64) loc.localise( Vec.from_str(ind_pan['origin']), Vec.from_str(ind_pan['angles']), ) add_signage_loc(sign_locs, loc) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. # all_floors[z][x,y] = count floor_neighbours = defaultdict( dict) # type: Dict[float, Dict[Tuple[float, float], int]] for mat_prop in res.find_key('Materials', []): MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker_filenames: continue targ = inst['targetname'] normal = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if normal == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = Vec.from_str(inst['origin']) - 64 * normal INST_LOCS[targ] = loc item = connections.ITEMS[targ] item.delete_antlines() if item.outputs: for conn in list(item.outputs): if conn.to_item.inst['file'].casefold() in marker_filenames: io_list.append((targ, conn.to_item.name)) else: LOGGER.warning('Cutout tile connected to non-cutout!') conn.remove() # Delete the connection. else: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) # Remove all traces of this item (other than in connections lists). inst.remove() del connections.ITEMS[targ] return # TODO: Reimplement cutout tiles. for start_floor, end_floor in FLOOR_IO: box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( vmf, box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams(vmf, box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin']) # Add a player_clip brush across the whole area vmf.add_brush( vmf.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = vmf.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = vmf.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( vmf, box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append( BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append( BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append( BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append( BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundaries near tiles, then generate them. # Do it separately for each z-level: for z, xy_dict in floor_neighbours.items(): for x, y in xy_dict: # We want to count where there aren't any tiles xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict)) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = (0.8 * cur_count + 0.1 * xy_dict.get( (x - 128, y - 128), 8) + 0.1 * xy_dict.get( (x - 128, y + 128), 8) + 0.1 * xy_dict.get( (x + 128, y - 128), 8) + 0.1 * xy_dict.get( (x + 128, y + 128), 8) + 0.2 * xy_dict.get( (x - 128, y), 8) + 0.2 * xy_dict.get( (x, y - 128), 8) + 0.2 * xy_dict.get( (x, y + 128), 8) + 0.2 * xy_dict.get( (x + 128, y), 8)) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = vmf.create_ent(classname='func_detail', ) for x, y in xy_dict: convert_floor( vmf, Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_locs, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(vmf, floor_edges) return conditions.RES_EXHAUSTED
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_cutout_tile(vmf: srctools.VMF, res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ item = instanceLocs.resolve(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_loc = set(FORCE_LOCATIONS) # If any signage is present in the map, we need to force tiles to # appear at that location! for over in vmf.by_class['info_overlay']: if (over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1')): loc = Vec.from_str(over['origin']) # Sometimes (light bridges etc) a sign will be halfway between # tiles, so in that case we need to force 2 tiles. loc_min = (loc - (15, 15, 0)) // 32 * 32 # type: Vec loc_max = (loc + (15, 15, 0)) // 32 * 32 # type: Vec loc_min += (16, 16, 0) loc_max += (16, 16, 0) FORCE_LOCATIONS.add(loc_min.as_tuple()) FORCE_LOCATIONS.add(loc_max.as_tuple()) SETTINGS = { 'floor_chance': srctools.conv_int(res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int(res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int(res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int(res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float(res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res.find_key('Materials', []): MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in item: continue targ = inst['targetname'] orient = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if orient == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = (orient * -64) + Vec.from_str(inst['origin']) INST_LOCS[targ] = loc for out in inst.output_targets(): io_list.append((targ, out)) if not inst.outputs and inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) inst.remove() # Remove the instance itself from the map. for start_floor, end_floor in FLOOR_IO: if end_floor not in INST_LOCS: # Not a marker - remove this and the antline. for toggle in vmf.by_target[end_floor]: conditions.remove_ant_toggle(toggle) continue box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( vmf, box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams(vmf, box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin']) # Add a player_clip brush across the whole area vmf.add_brush( vmf.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = vmf.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = vmf.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( vmf, box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append( BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append( BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append( BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append( BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundries near tiles, then generate them. # Do it seperately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = (((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict)) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = (0.8 * cur_count + 0.1 * xy_dict.get( (x - 128, y - 128), 8) + 0.1 * xy_dict.get( (x - 128, y + 128), 8) + 0.1 * xy_dict.get( (x + 128, y - 128), 8) + 0.1 * xy_dict.get( (x + 128, y + 128), 8) + 0.2 * xy_dict.get( (x - 128, y), 8) + 0.2 * xy_dict.get( (x, y - 128), 8) + 0.2 * xy_dict.get( (x, y + 128), 8) + 0.2 * xy_dict.get( (x + 128, y), 8)) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = vmf.create_ent(classname='func_detail', ) for x, y in xy_dict: convert_floor( vmf, Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_loc, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(vmf, floor_edges) conditions.reallocate_overlays(overlay_ids) return conditions.RES_EXHAUSTED
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value(node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value(node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED) ): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value(node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value(node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE(base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def parse_map(vmf: VMF, voice_attrs: Dict[str, bool]) -> None: """Analyse fizzler instances to assign fizzler types. Instance traits are required. The model instances and brushes will be removed from the map. Needs connections to be parsed. """ # Item ID and model skin -> fizzler type fizz_types = {} # type: Dict[Tuple[str, int], FizzlerType] for fizz_type in FIZZ_TYPES.values(): for item_id in fizz_type.item_ids: if ':' in item_id: item_id, barrier_type = item_id.split(':') if barrier_type == 'laserfield': barrier_skin = 2 elif barrier_type == 'fizzler': barrier_skin = 0 else: LOGGER.error('Invalid barrier type ({}) for "{}"!', barrier_type, item_id) fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type continue fizz_types[item_id, barrier_skin] = fizz_type else: fizz_types[item_id, 0] = fizz_type fizz_types[item_id, 2] = fizz_type fizz_bases = {} # type: Dict[str, Entity] fizz_models = defaultdict(list) # type: Dict[str, List[Entity]] # Position and normal -> name, for output relays. fizz_pos = {} # type: Dict[Tuple[Tuple[float, float, float], Tuple[float, float, float]], str] # First use traits to gather up all the instances. for inst in vmf.by_class['func_instance']: traits = instance_traits.get(inst) if 'fizzler' not in traits: continue name = inst['targetname'] if 'fizzler_model' in traits: name = name.rsplit('_model', 1)[0] fizz_models[name].append(inst) inst.remove() elif 'fizzler_base' in traits: fizz_bases[name] = inst else: LOGGER.warning('Fizzler "{}" has non-base, non-model instance?', name) continue origin = Vec.from_str(inst['origin']) normal = Vec(z=1).rotate_by_str(inst['angles']) fizz_pos[origin.as_tuple(), normal.as_tuple()] = name for name, base_inst in fizz_bases.items(): models = fizz_models[name] up_axis = Vec(y=1).rotate_by_str(base_inst['angles']) # If upside-down, make it face upright. if up_axis == (0, 0, -1): up_axis = Vec(z=1) base_inst.outputs.clear() # Now match the pairs of models to each other. # The length axis is the line between them. # We don't care about the instances after this, so don't keep track. length_axis = Vec(z=1).rotate_by_str(base_inst['angles']).axis() emitters = [] # type: List[Tuple[Vec, Vec]] model_pairs = {} # type: Dict[Tuple[float, float], Vec] model_skin = models[0].fixup.int('$skin') try: item_id, item_subtype = instanceLocs.ITEM_FOR_FILE[base_inst['file'].casefold()] fizz_type = fizz_types[item_id, model_skin] except KeyError: LOGGER.warning('Fizzler types: {}', fizz_types.keys()) raise ValueError('No fizzler type for "{}"!'.format( base_inst['file'], )) from None for attr_name in fizz_type.voice_attrs: voice_attrs[attr_name] = True for model in models: pos = Vec.from_str(model['origin']) try: other_pos = model_pairs.pop(pos.other_axes(length_axis)) except KeyError: # No other position yet, we need to find that. model_pairs[pos.other_axes(length_axis)] = pos continue min_pos, max_pos = Vec.bbox(pos, other_pos) # Move positions to the wall surface. min_pos[length_axis] -= 64 max_pos[length_axis] += 64 emitters.append((min_pos, max_pos)) FIZZLERS[name] = Fizzler(fizz_type, up_axis, base_inst, emitters) # Delete all the old brushes associated with fizzlers for brush in ( vmf.by_class['trigger_portal_cleanser'] | vmf.by_class['trigger_hurt'] | vmf.by_class['func_brush'] ): name = brush['targetname'] if not name: continue name = name.rsplit('_brush')[0] if name in FIZZLERS: brush.remove() # Check for fizzler output relays. relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>', silent=True) if not relay_file: # No relay item - deactivated most likely. return for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in relay_file: continue inst.remove() relay_item = connections.ITEMS[inst['targetname']] try: fizz_name = fizz_pos[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple() ] fizz_item = connections.ITEMS[fizz_name] except KeyError: # Not placed on a fizzler, or a fizzler with no IO # - ignore, and destroy. for out in list(relay_item.outputs): out.remove() for out in list(relay_item.inputs): out.remove() del connections.ITEMS[relay_item.name] continue # Copy over fixup values fizz_item.inst.fixup.update(inst.fixup) # Copy over the timer delay set in the relay. fizz_item.timer = relay_item.timer # Transfer over antlines. fizz_item.antlines |= relay_item.antlines fizz_item.shape_signs += relay_item.shape_signs fizz_item.ind_panels |= relay_item.ind_panels # Remove the relay item so it doesn't get added to the map. del connections.ITEMS[relay_item.name] for conn in list(relay_item.outputs): conn.from_item = fizz_item
def flag_has_inst(flag: Property): """Checks if the given instance is present anywhere in the map.""" flags = instanceLocs.resolve(flag.value) return any(inst.casefold() in flags for inst in ALL_INST)
def res_make_catwalk(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'] connections = {} # The directions this instance is connected by (NSEW) markers = {} # Find all our markers, so we can look them up by targetname. for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() not in marker: continue # [North, South, East, West ] connections[inst] = [False, False, False, False] 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 # type: Vec origin += 64 while origin.as_tuple() in conditions.GOO_LOCS: # 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: {}', connections) 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 vbsp.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 LOGGER.debug('Dist = {}, Vert = {}', dist, vert_dist) 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(instances, origin1, origin2) # Update the lists based on the directions that were set conn_lst1 = connections[inst] conn_lst2 = connections[inst2] if origin1.x < origin2.x: conn_lst1[2] = True # E conn_lst2[3] = True # W elif origin2.x < origin1.x: conn_lst1[3] = True # W conn_lst2[2] = True # E if origin1.y < origin2.y: conn_lst1[0] = True # N conn_lst2[1] = True # S elif origin2.y < origin1.y: conn_lst1[1] = True # S conn_lst2[0] = True # N inst.outputs.clear() # Remove the outputs now, they're useless for inst, dir_mask in connections.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[tuple(dir_mask)] 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: # Treat booleans as ints to get the direction the connection is # in - True == 1, False == 0 conn_dir = Vec( x=dir_mask[2] - dir_mask[3], # +E, -W y=dir_mask[0] - dir_mask[1], # +N, -S, z=0, ) if normal == 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 if normal == (0, 0, 1): # If in goo, use different supports! origin = Vec.from_str(inst['origin']) origin.z -= 128 if origin.as_tuple() in conditions.GOO_LOCS: 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: vbsp.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 associate_faith_plates(vmf: VMF) -> None: """Parse through the map, collecting all faithplate segments. Tiling, instancelocs and connections must have been parsed first. Once complete all targets have been removed. This is done as a meta-condition to allow placing tiles we will attach to. """ # Find all the triggers and targets first. triggers: Dict[str, Entity] = {} helper_trigs: Dict[str, Entity] = {} paint_trigs: Dict[str, Entity] = {} for trig in vmf.by_class['trigger_catapult']: name = trig['targetname'] # Conveniently, we can determine what sort of catapult was made by # examining the local name used. if name.endswith('-helperTrigger'): helper_trigs[name[:-14]] = trig # Also store None in the main trigger if no key is there, # so we can detect missing main triggers... triggers.setdefault(name[:-14], None) elif name.endswith('-trigger'): triggers[name[:-8]] = trig # Remove the original relay inputs. We need to keep the output # to the helper if necessary. trig.outputs[:] = [out for out in trig.outputs if not out.inst_in] elif name.endswith('-catapult'): # Paint droppers. paint_trigs[name[:-9]] = trig else: LOGGER.warning('Unknown trigger "{}"?', name) target_to_pos: Dict[str, Union[Vec, tiling.TileDef]] = {} for targ in vmf.by_class['info_target']: name = targ['targetname'] # All should be faith targets, with this name. if not name.endswith('-target'): LOGGER.warning('Unknown info_target "{}" @ {}?', name, targ['origin']) continue name = name[:-7] # Find the tile we're attached to. Unfortunately no angles, so we # have to try both directions. origin = Vec.from_str(targ['origin']) # If the plate isn't on a tile (placed on goo for example), # use the direct position. tile = Vec.from_str(targ['origin']) grid_pos: Vec = origin // 128 * 128 + 64 norm = (origin - grid_pos).norm() # If we're on the floor above the top of goo, move down to the surface. block_type = brushLoc.POS['world':tile - (0, 0, 64)] if block_type.is_goo and block_type.is_top: tile.z -= 32 for norm in [norm, -norm]: # Try both directions. try: tile = tiling.TILES[(origin - 64 * norm).as_tuple(), norm.as_tuple(), ] break except KeyError: pass # We don't need the entity anymore, we'll regenerate them later. targ.remove() target_to_pos[name] = tile # Loop over instances, recording plates and moving targets into the tiledefs. instances: Dict[str, Entity] = {} faith_targ_file = instanceLocs.resolve('<ITEM_CATAPULT_TARGET>') for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in faith_targ_file: inst.remove() # Don't keep the targets. origin = Vec.from_str(inst['origin']) norm = Vec(z=1).rotate_by_str(inst['angles']) try: tile = tiling.TILES[(origin - 128 * norm).as_tuple(), norm.as_tuple()] except KeyError: LOGGER.warning('No tile for bullseye at {}!', origin - 64 * norm) continue tile.bullseye_count += 1 tile.add_portal_helper() else: instances[inst['targetname']] = inst # Now, combine into plate objects for each. for name, trig in triggers.items(): if trig is None: raise ValueError(f'Faith plate {name} has a helper ' 'trigger but no main trigger!') try: pos = target_to_pos[name] except KeyError: # No position, it's a straight plate. PLATES[name] = StraightPlate(instances[name], trig, helper_trigs[name]) else: # Target position, angled plate. PLATES[name] = AngledPlate(instances[name], trig, pos) # And paint droppers for name, trig in paint_trigs.items(): try: pos = target_to_pos[name] except KeyError: LOGGER.warning('No target for paint dropper {}!', name) continue # Target position, angled plate. PLATES[name] = PaintDropper(instances[name], trig, pos)
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) conf_rope_off = res.vec('RopePos') conf_toggle_targ = res['toggleTarg', ''] beam_conf = res.find_key('BeamKeys', []) glow_conf = res.find_key('GlowKeys', []) cable_conf = res.find_key('CableKeys', []) if beam_conf: # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = beam_conf.int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) else: conf_beam_flags = 0 conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None if not nodes: # None at all. return conditions.RES_EXHAUSTED # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({node, neighbour})) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: node.transfer_antlines(group.item) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add(frozenset({neighbour, node})) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) if conf_toggle_targ: # Make the group info_target into a texturetoggle. toggle = group.item.inst toggle['classname'] = 'env_texturetoggle' toggle['target'] = conditions.local_name(group.nodes[0].inst, conf_toggle_targ) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] # For cables, it's a bit trickier than the beams. # The cable ent itself is the one which decides what it links to, # so we need to potentially make endpoint cables at locations with # only "incoming" lines. # So this dict is either a targetname to indicate cables with an # outgoing connection, or the entity for endpoints without an outgoing # connection. cable_points = {} # type: Dict[Item, Union[Entity, str]] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) if glow_conf: # First add the sprite at the right height. sprite = vmf.create_ent('env_sprite') for prop in glow_conf: sprite[prop.name] = conditions.resolve_value( node.inst, prop.value) sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) elif beam_conf: # If beams but not sprites, we need a target. vmf.create_ent( 'info_target', origin=sprite_pos, targetname=NAME_SPR(base_name, i), ) if beam_conf: # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') for prop in beam_conf: beam[prop.name] = conditions.resolve_value( node.inst, prop.value) beam['origin'] = beam['targetpoint'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start if beam_conf: for i, (node_a, node_b) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node_a.inst, res, 'BeamKeys') beam['origin'] = beam['targetpoint'] = node_a.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node_a]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node_b]) beam['spawnflags'] = conf_beam_flags # We have a couple different situations to deal with here. # Either end could Not exist, be Unlinked, or be Linked = 8 combos. # Always flip so we do A to B. # AB | # NN | Make 2 new ones, one is an endpoint. # NU | Flip, do UN. # NL | Make A, link A to B. Both are linked. # UN | Make B, link A to B. B is unlinked. # UU | Link A to B, A is now linked, B is unlinked. # UL | Link A to B. Both are linked. # LN | Flip, do NL. # LU | Flip, do UL # LL | Make A, link A to B. Both are linked. if cable_conf: rope_ind = 0 # Uniqueness value. for node_a, node_b in group.links: state_a, ent_a = RopeState.from_node(cable_points, node_a) state_b, ent_b = RopeState.from_node(cable_points, node_b) if (state_a is RopeState.LINKED or (state_a is RopeState.NONE and state_b is RopeState.UNLINKED)): # Flip these, handle the opposite order. state_a, state_b = state_b, state_a ent_a, ent_b = ent_b, ent_a node_a, node_b = node_b, node_a pos_a = conf_rope_off.copy() pos_a.localise( Vec.from_str(node_a.inst['origin']), Vec.from_str(node_a.inst['angles']), ) pos_b = conf_rope_off.copy() pos_b.localise( Vec.from_str(node_b.inst['origin']), Vec.from_str(node_b.inst['angles']), ) # Need to make the A rope if we don't have one that's unlinked. if state_a is not RopeState.UNLINKED: rope_a = vmf.create_ent('move_rope') for prop in beam_conf: rope_a[prop.name] = conditions.resolve_value( node_a.inst, prop.value) rope_a['origin'] = pos_a rope_ind += 1 rope_a['targetname'] = NAME_CABLE(base_name, rope_ind) else: # It is unlinked, so it's the rope to use. rope_a = ent_a # Only need to make the B rope if it doesn't have one. if state_b is RopeState.NONE: rope_b = vmf.create_ent('move_rope') for prop in beam_conf: rope_b[prop.name] = conditions.resolve_value( node_b.inst, prop.value) rope_b['origin'] = pos_b rope_ind += 1 name_b = rope_b['targetname'] = NAME_CABLE( base_name, rope_ind) cable_points[node_b] = rope_b # Someone can use this. elif state_b is RopeState.UNLINKED: # Both must be unlinked, we aren't using this link though. name_b = ent_b['targetname'] else: # Linked, we just have the name. name_b = ent_b # By here, rope_a should be an unlinked rope, # and name_b should be a name to link to. rope_a['nextkey'] = name_b # Figure out how much slack to give. # If on floor, we need to be taut to have clearance. if on_floor(node_a) or on_floor(node_b): rope_a['slack'] = 60 else: rope_a['slack'] = 125 # We're always linking A to B, so A is always linked! if state_a is not RopeState.LINKED: cable_points[node_a] = rope_a['targetname'] return conditions.RES_EXHAUSTED
def res_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_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() inst = None for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() del inst # Make sure we don't use this later. if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name(res['coopActivate', 'OnChangeToAllTrue']), output_deact=Output.parse_name(res['coopDeactivate', 'OnChangeToAnyFalse']), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin'])) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=vbsp_options.get(Vec, "global_ents_loc"), angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst1, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst1, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def parse(cls, conf: Property): """Read in a fizzler from a config.""" fizz_id = conf['id'] item_ids = [prop.value.casefold() for prop in conf.find_all('item_id')] try: model_name_type = ModelName(conf['NameType', 'same'].casefold()) except ValueError: LOGGER.warning('Bad model name type: "{}"', conf['NameType']) model_name_type = ModelName.SAME model_local_name = conf['ModelName', ''] if not model_local_name: # We can't rename without a local name. model_name_type = ModelName.SAME inst = {} for inst_type, is_static in itertools.product(FizzInst, (False, True)): inst_type_name = inst_type.value + ('_static' if is_static else '') inst[inst_type, is_static] = instances = [ file for prop in conf.find_all(inst_type_name) for file in instanceLocs.resolve(prop.value) ] # Allow specifying weights to bias model locations weights = conf[inst_type_name + '_weight', ''] if weights: # Produce the weights, then process through the original # list to build a new one with repeated elements. inst[inst_type, is_static] = instances = [ instances[i] for i in conditions.weighted_random( len(instances), weights) ] # If static versions aren't given, reuse non-static ones. # We do False, True so it's already been calculated. if not instances and is_static: inst[inst_type, True] = inst[inst_type, False] if not inst[FizzInst.BASE, False]: LOGGER.warning('No base instance set! for "{}"!', fizz_id) voice_attrs = [] for prop in conf.find_all('Has'): if prop.has_children(): for child in prop: voice_attrs.append(child.name.casefold()) else: voice_attrs.append(prop.value.casefold()) out_activate = conf['OutActivate', None] if out_activate is not None: out_activate = Output.parse_name(out_activate) out_deactivate = conf['OutDeactivate', None] if out_deactivate is not None: out_deactivate = Output.parse_name(out_deactivate) pack_lists = {prop.value for prop in conf.find_all('Pack')} pack_lists_static = { prop.value for prop in conf.find_all('PackStatic') } brushes = [FizzlerBrush.parse(prop) for prop in conf.find_all('Brush')] beams = [] # type: List[FizzBeam] for beam_prop in conf.find_all('Beam'): offsets = [ Vec.from_str(off.value) for off in beam_prop.find_all('pos') ] keys = Property('', [ beam_prop.find_key('Keys', []), beam_prop.find_key('LocalKeys', []) ]) beams.append( FizzBeam( offsets, keys, beam_prop.int('RandSpeedMin', 0), beam_prop.int('RandSpeedMax', 0), )) try: temp_conf = conf.find_key('TemplateBrush') except NoKeyError: temp_brush_keys = temp_min = temp_max = temp_single = None else: temp_brush_keys = Property('--', [ temp_conf.find_key('Keys'), temp_conf.find_key('LocalKeys', []), ]) # Find and load the templates. temp_min = temp_conf['Left', None] temp_max = temp_conf['Right', None] temp_single = temp_conf['Single', None] return FizzlerType( fizz_id, item_ids, voice_attrs, pack_lists, pack_lists_static, model_local_name, model_name_type, out_activate, out_deactivate, brushes, beams, inst, temp_brush_keys, temp_min, temp_max, temp_single, )
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_cutout_tile(vmf: srctools.VMF, res: Property): """Generate random quarter tiles, like in Destroyed or Retro maps. - "MarkerItem" is the instance to look for. - "TileSize" can be "2x2" or "4x4". - rotateMax is the amount of degrees to rotate squarebeam models. Materials: - "squarebeams" is the squarebeams variant to use. - "ceilingwalls" are the sides of the ceiling section. - "floorbase" is the texture under floor sections. - "tile_glue" is used on top of a thinner tile segment. - "clip" is the player_clip texture used over floor segments. (This allows customising the surfaceprop.) - "Floor4x4Black", "Ceil2x2White" and other combinations can be used to override the textures used. """ marker_filenames = instanceLocs.resolve(res['markeritem']) INST_LOCS = {} # Map targetnames -> surface loc CEIL_IO = [] # Pairs of ceil inst corners to cut out. FLOOR_IO = [] # Pairs of floor inst corners to cut out. overlay_ids = {} # When we replace brushes, we need to fix any overlays # on that surface. MATS.clear() floor_edges = [] # Values to pass to add_floor_sides() at the end sign_locs = set() # If any signage is present in the map, we need to force tiles to # appear at that location! for over in vmf.by_class['info_overlay']: if ( over['material'].casefold() in FORCE_TILE_MATS and # Only check floor/ceiling overlays over['basisnormal'] in ('0 0 1', '0 0 -1') ): add_signage_loc(sign_locs, Vec.from_str(over['origin'])) for item in connections.ITEMS.values(): for ind_pan in item.ind_panels: loc = Vec(0, 0, -64) loc.localise( Vec.from_str(ind_pan['origin']), Vec.from_str(ind_pan['angles']), ) add_signage_loc(sign_locs, loc) SETTINGS = { 'floor_chance': srctools.conv_int( res['floorChance', '100'], 100), 'ceil_chance': srctools.conv_int( res['ceilingChance', '100'], 100), 'floor_glue_chance': srctools.conv_int( res['floorGlueChance', '0']), 'ceil_glue_chance': srctools.conv_int( res['ceilingGlueChance', '0']), 'rotate_beams': int(srctools.conv_float( res['rotateMax', '0']) * BEAM_ROT_PRECISION), 'beam_skin': res['squarebeamsSkin', '0'], 'base_is_disp': srctools.conv_bool(res['dispBase', '0']), 'quad_floor': res['FloorSize', '4x4'].casefold() == '2x2', 'quad_ceil': res['CeilingSize', '4x4'].casefold() == '2x2', } random.seed(vbsp.MAP_RAND_SEED + '_CUTOUT_TILE_NOISE') noise = SimplexNoise(period=4 * 40) # 4 tiles/block, 50 blocks max # We want to know the number of neighbouring tile cutouts before # placing tiles - blocks away from the sides generate fewer tiles. floor_neighbours = defaultdict(dict) # all_floors[z][x,y] = count for mat_prop in res.find_key('Materials', []): MATS[mat_prop.name].append(mat_prop.value) if SETTINGS['base_is_disp']: # We want the normal brushes to become nodraw. MATS['floorbase_disp'] = MATS['floorbase'] MATS['floorbase'] = ['tools/toolsnodraw'] # Since this uses random data for initialisation, the alpha and # regular will use slightly different patterns. alpha_noise = SimplexNoise(period=4 * 50) else: alpha_noise = None for key, default in TEX_DEFAULT: if key not in MATS: MATS[key] = [default] # Find our marker ents for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in marker_filenames: continue targ = inst['targetname'] normal = Vec(0, 0, 1).rotate_by_str(inst['angles', '0 0 0']) # Check the orientation of the marker to figure out what to generate if normal == (0, 0, 1): io_list = FLOOR_IO else: io_list = CEIL_IO # Reuse orient to calculate where the solid face will be. loc = Vec.from_str(inst['origin']) - 64 * normal INST_LOCS[targ] = loc item = connections.ITEMS[targ] item.delete_antlines() if item.outputs: for conn in list(item.outputs): if conn.to_item.inst['file'].casefold() in marker_filenames: io_list.append((targ, conn.to_item.name)) else: LOGGER.warning('Cutout tile connected to non-cutout!') conn.remove() # Delete the connection. else: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 128x128 tile segment. io_list.append((targ, targ)) # Remove all traces of this item (other than in connections lists). inst.remove() del connections.ITEMS[targ] for start_floor, end_floor in FLOOR_IO: box_min = Vec(INST_LOCS[start_floor]) box_min.min(INST_LOCS[end_floor]) box_max = Vec(INST_LOCS[start_floor]) box_max.max(INST_LOCS[end_floor]) if box_min.z != box_max.z: continue # They're not in the same level! z = box_min.z if SETTINGS['rotate_beams']: # We have to generate 1 model per 64x64 block to do rotation... gen_rotated_squarebeams( vmf, box_min - (64, 64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'], max_rot=SETTINGS['rotate_beams'], ) else: # Make the squarebeams props, using big models if possible gen_squarebeams( vmf, box_min + (-64, -64, 0), box_max + (64, 64, -8), skin=SETTINGS['beam_skin'] ) # Add a player_clip brush across the whole area vmf.add_brush(vmf.make_prism( p1=box_min - (64, 64, FLOOR_DEPTH), p2=box_max + (64, 64, 0), mat=MATS['clip'][0], ).solid) # Add a noportal_volume covering the surface, in case there's # room for a portal. noportal_solid = vmf.make_prism( # Don't go all the way to the sides, so it doesn't affect wall # brushes. p1=box_min - (63, 63, 9), p2=box_max + (63, 63, 0), mat='tools/toolsinvisible', ).solid noportal_ent = vmf.create_ent( classname='func_noportal_volume', origin=box_min.join(' '), ) noportal_ent.solids.append(noportal_solid) if SETTINGS['base_is_disp']: # Use displacements for the base instead. make_alpha_base( vmf, box_min + (-64, -64, 0), box_max + (64, 64, 0), noise=alpha_noise, ) for x, y in utils.iter_grid( min_x=int(box_min.x), max_x=int(box_max.x) + 1, min_y=int(box_min.y), max_y=int(box_max.y) + 1, stride=128, ): # Build the set of all positions.. floor_neighbours[z][x, y] = -1 # Mark borders we need to fill in, and the angle (for func_instance) # The wall is the face pointing inwards towards the bottom brush, # and the ceil is the ceiling of the block above the bordering grid # points. for x in range(int(box_min.x), int(box_max.x) + 1, 128): # North floor_edges.append(BorderPoints( wall=Vec(x, box_max.y + 64, z - 64), ceil=Vec_tuple(x, box_max.y + 128, z), rot=270, )) # South floor_edges.append(BorderPoints( wall=Vec(x, box_min.y - 64, z - 64), ceil=Vec_tuple(x, box_min.y - 128, z), rot=90, )) for y in range(int(box_min.y), int(box_max.y) + 1, 128): # East floor_edges.append(BorderPoints( wall=Vec(box_max.x + 64, y, z - 64), ceil=Vec_tuple(box_max.x + 128, y, z), rot=180, )) # West floor_edges.append(BorderPoints( wall=Vec(box_min.x - 64, y, z - 64), ceil=Vec_tuple(box_min.x - 128, y, z), rot=0, )) # Now count boundaries near tiles, then generate them. # Do it separately for each z-level: for z, xy_dict in floor_neighbours.items(): # type: float, dict for x, y in xy_dict: # type: float, float # We want to count where there aren't any tiles xy_dict[x, y] = ( ((x - 128, y - 128) not in xy_dict) + ((x - 128, y + 128) not in xy_dict) + ((x + 128, y - 128) not in xy_dict) + ((x + 128, y + 128) not in xy_dict) + ((x - 128, y) not in xy_dict) + ((x + 128, y) not in xy_dict) + ((x, y - 128) not in xy_dict) + ((x, y + 128) not in xy_dict) ) max_x = max_y = 0 weights = {} # Now the counts are all correct, compute the weight to apply # for tiles. # Adding the neighbouring counts will make a 5x5 area needed to set # the center to 0. for (x, y), cur_count in xy_dict.items(): max_x = max(x, max_x) max_y = max(y, max_y) # Orthrogonal is worth 0.2, diagonal is worth 0.1. # Not-present tiles would be 8 - the maximum tile_count = ( 0.8 * cur_count + 0.1 * xy_dict.get((x - 128, y - 128), 8) + 0.1 * xy_dict.get((x - 128, y + 128), 8) + 0.1 * xy_dict.get((x + 128, y - 128), 8) + 0.1 * xy_dict.get((x + 128, y + 128), 8) + 0.2 * xy_dict.get((x - 128, y), 8) + 0.2 * xy_dict.get((x, y - 128), 8) + 0.2 * xy_dict.get((x, y + 128), 8) + 0.2 * xy_dict.get((x + 128, y), 8) ) # The number ranges from 0 (all tiles) to 12.8 (no tiles). # All tiles should still have a small chance to generate tiles. weights[x, y] = min((tile_count + 0.5) / 8, 1) # Share the detail entity among same-height tiles.. detail_ent = vmf.create_ent( classname='func_detail', ) for x, y in xy_dict: convert_floor( vmf, Vec(x, y, z), overlay_ids, MATS, SETTINGS, sign_locs, detail_ent, noise_weight=weights[x, y], noise_func=noise, ) add_floor_sides(vmf, floor_edges) conditions.reallocate_overlays(overlay_ids) 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 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 fizzler_out_relay(): """Link fizzlers with a relay item so they can be given outputs.""" relay_file = instanceLocs.resolve('<ITEM_BEE2_FIZZLER_OUT_RELAY>') if not relay_file: # No relay item - deactivated most likely. return # instances fizz_models = set() # base -> connections fizz_bases = {} for fizz_id in ITEMS_WITH_CLASS[const.ItemClass.FIZZLER]: base, model = instanceLocs.resolve( '<{}: fizz_base, fizz_model>'.format(fizz_id)) fizz_bases[base.casefold()] = CONNECTIONS[fizz_id] fizz_models.add(model.casefold()) # targetname -> base inst, connections fizz_by_name = {} # origin, normal -> targetname pos_to_name = {} marker_inst = [] # type: List[Entity] LOGGER.info('Fizzler data: {}', locals()) for inst in vbsp.VMF.by_class['func_instance']: filename = inst['file'].casefold() name = inst['targetname'] if filename in fizz_bases: fizz_by_name[inst['targetname']] = inst, fizz_bases[filename] elif filename in fizz_models: if inst['targetname'].endswith(('_modelStart', '_modelEnd')): name = inst['targetname'].rsplit('_', 1)[0] elif filename in relay_file: marker_inst.append(inst) # Remove the marker, we don't need that... inst.remove() continue else: continue pos_to_name[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] = name for inst in marker_inst: try: fizz_name = pos_to_name[ Vec.from_str(inst['origin']).as_tuple(), Vec(0, 0, 1).rotate_by_str(inst['angles']).as_tuple()] except KeyError: # Not placed on a fizzler... continue base_inst, connections = fizz_by_name[fizz_name] # Copy over fixup values for key, val in inst.fixup.items(): base_inst.fixup[key] = val for out in inst.outputs: new_out = out.copy() if out.output == 'ON': new_out.inst_out, new_out.output = connections.out_act elif out.output == 'OFF': new_out.inst_out, new_out.output = connections.out_deact else: # Not the marker's output somehow? continue base_inst.add_out(new_out)
def res_antlaser(vmf: VMF, res: Property): """The condition to generate AntLasers. This is executed once to modify all instances. """ conf_inst = instanceLocs.resolve(res['instance']) conf_glow_height = Vec(z=res.float('GlowHeight', 48) - 64) conf_las_start = Vec(z=res.float('LasStart') - 64) # Grab a copy of the beam spawnflags so we can set our own options. conf_beam_flags = res.find_key('BeamKeys', []).int('spawnflags') # Mask out certain flags. conf_beam_flags &= ( 0 | 1 # Start On | 2 # Toggle | 4 # Random Strike | 8 # Ring | 16 # StartSparks | 32 # EndSparks | 64 # Decal End #| 128 # Shade Start #| 256 # Shade End #| 512 # Taper Out ) conf_outputs = [ Output.parse(prop) for prop in res if prop.name in ('onenabled', 'ondisabled') ] # Find all the markers. nodes = {} # type: Dict[str, Item] for inst in vmf.by_class['func_instance']: if inst['file'].casefold() not in conf_inst: continue name = inst['targetname'] try: # Remove the item - it's no longer going to exist after # we're done. nodes[name] = connections.ITEMS.pop(name) except KeyError: raise ValueError('No item for "{}"?'.format(name)) from None # Now find every connected group, recording inputs, outputs and links. todo = set(nodes.values()) groups = [] # type: List[Group] # Node -> is grouped already. node_pairing = dict.fromkeys(nodes.values(), False) while todo: start = todo.pop() # Synthesise the Item used for logic. # We use a random info_target to manage the IO data. group = Group(start) groups.append(group) for node in group.nodes: # If this node has no non-node outputs, destroy the antlines. has_output = False node_pairing[node] = True for conn in list(node.outputs): neighbour = conn.to_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, a target of our logic. conn.from_item = group.item has_output = True continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((node, neighbour)) # If we have a real output, we need to transfer it. # Otherwise we can just destroy it. if has_output: group.item.antlines.update(node.antlines) group.item.ind_panels.update(node.ind_panels) group.item.shape_signs.extend(node.shape_signs) else: node.delete_antlines() # Do the same for inputs, so we can catch that. for conn in list(node.inputs): neighbour = conn.from_item todo.discard(neighbour) pair_state = node_pairing.get(neighbour, None) if pair_state is None: # Not a node, an input to the group. conn.to_item = group.item continue elif pair_state is False: # Another node. group.nodes.append(neighbour) # else: True, node already added. # For nodes, connect link. conn.remove() group.links.add((neighbour, node)) # Now every node is in a group. Generate the actual entities. for group in groups: # We generate two ent types. For each marker, we add a sprite # and a beam pointing at it. Then for each connection # another beam. # Choose a random antlaser name to use for our group. base_name = group.nodes[0].name out_enable = [Output('', '', 'FireUser2')] out_disable = [Output('', '', 'FireUser1')] for output in conf_outputs: if output.output.casefold() == 'onenabled': out_enable.append(output.copy()) else: out_disable.append(output.copy()) group.item.enable_cmd = tuple(out_enable) group.item.disable_cmd = tuple(out_disable) # Node -> index for targetnames. indexes = {} # type: Dict[Item, int] for i, node in enumerate(group.nodes, start=1): indexes[node] = i node.name = base_name # First add the sprite at the right height. sprite_pos = conf_glow_height.copy() sprite_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) sprite = vmf.create_ent('env_sprite') conditions.set_ent_keys(sprite, node.inst, res, 'GlowKeys') sprite['origin'] = sprite_pos sprite['targetname'] = NAME_SPR(base_name, i) # Now the beam going from below up to the sprite. beam_pos = conf_las_start.copy() beam_pos.localise( Vec.from_str(node.inst['origin']), Vec.from_str(node.inst['angles']), ) beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node.inst, res, 'BeamKeys') beam['origin'] = beam_pos beam['targetname'] = NAME_BEAM_LOW(base_name, i) beam['LightningStart'] = beam['targetname'] beam['LightningEnd'] = NAME_SPR(base_name, i) beam['spawnflags'] = conf_beam_flags | 128 # Shade Start for i, (node1, node2) in enumerate(group.links): beam = vmf.create_ent('env_beam') conditions.set_ent_keys(beam, node1.inst, res, 'BeamKeys') beam['origin'] = node1.inst['origin'] beam['targetname'] = NAME_BEAM_CONN(base_name, i) beam['LightningStart'] = NAME_SPR(base_name, indexes[node1]) beam['LightningEnd'] = NAME_SPR(base_name, indexes[node2]) beam['spawnflags'] = conf_beam_flags return conditions.RES_EXHAUSTED
def res_resizeable_trigger(vmf: VMF, res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewInst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The VMF output to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The `instance:name;Output` outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The `instance:name;Output` outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) marker_names = set() for inst in vmf.by_class['func_instance']: if inst['file'].casefold() in marker: marker_names.add(inst['targetname']) # Unconditionally delete from the map, so it doesn't # appear even if placed wrongly. inst.remove() if not marker_names: # No markers in the map - abort return RES_EXHAUSTED item_id = res['markerItem'] # Synthesise the item type used for the final trigger. item_type_sp = connections.ItemType( id=item_id + ':TRIGGER', output_act=Output.parse_name(res['triggerActivate', 'OnStartTouchAll']), output_deact=Output.parse_name(res['triggerDeactivate', 'OnEndTouchAll']), ) # For Coop, we add a logic_coop_manager in the mix so both players can # be handled. try: coop_var = res['coopVar'] except LookupError: coop_var = item_type_coop = None coop_only_once = False else: coop_only_once = res.bool('coopOnce') item_type_coop = connections.ItemType( id=item_id + ':TRIGGER_COOP', output_act=Output.parse_name( res['coopActivate', 'OnChangeToAllTrue'] ), output_deact=Output.parse_name( res['coopDeactivate', 'OnChangeToAnyFalse'] ), ) # Display preview overlays if it's preview mode, and the config is true pre_act = pre_deact = None if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] preview_scale = res.float('previewScale', 0.25) # None if not found. with suppress(LookupError): pre_act = Output.parse(res.find_key('previewActivate')) with suppress(LookupError): pre_deact = Output.parse(res.find_key('previewDeactivate')) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' preview_scale = 0.25 # Now go through each brush. # We do while + pop to allow removing both names each loop through. todo_names = set(marker_names) while todo_names: targ = todo_names.pop() mark1 = connections.ITEMS.pop(targ) for conn in mark1.outputs: if conn.to_item.name in marker_names: mark2 = conn.to_item conn.remove() # Delete this connection. todo_names.discard(mark2.name) del connections.ITEMS[mark2.name] break else: if not mark1.inputs: # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. mark2 = mark1 else: # It's a marker with an input, the other in the pair # will handle everything. # But reinstate it in ITEMS. connections.ITEMS[targ] = mark1 continue inst1 = mark1.inst inst2 = mark2.inst is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst1.fixup.bool(coop_var) or inst2.fixup.bool(coop_var) ) bbox_min, bbox_max = Vec.bbox( Vec.from_str(inst1['origin']), Vec.from_str(inst2['origin']) ) origin = (bbox_max + bbox_min) / 2 # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vmf.create_ent( classname='trigger_multiple', # Default targetname=targ, origin=origin, angles='0 0 0', ) trig_ent.solids = [ vmf.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent = manager = vmf.create_ent( classname='logic_coop_manager', targetname=conditions.local_name(inst, 'man'), origin=origin, ) item = connections.Item( out_ent, item_type_coop, mark1.ant_floor_style, mark1.ant_wall_style, ) if coop_only_once: # Kill all the ents when both players are present. manager.add_out( Output('OnChangeToAllTrue', manager, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', manager, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', manager, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', manager, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', manager, 'SetStateBFalse'), ) else: item = connections.Item( trig_ent, item_type_sp, mark1.ant_floor_style, mark1.ant_wall_style, ) # Register, and copy over all the antlines. connections.ITEMS[item.name] = item item.ind_panels = mark1.ind_panels | mark2.ind_panels item.antlines = mark1.antlines | mark2.antlines item.shape_signs = mark1.shape_signs + mark2.shape_signs if preview_mat: preview_brush = vmf.create_ent( classname='func_brush', parentname=targ, origin=origin, Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vmf.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: pre_inst = vmf.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=inst2['origin'], ) if pre_act is not None: out = pre_act.copy() out.inst_out, out.output = item.output_act() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) if pre_deact is not None: out = pre_deact.copy() out.inst_out, out.output = item.output_deact() out.target = conditions.local_name(pre_inst, out.target) out_ent.add_out(out) for conn in mark1.outputs | mark2.outputs: conn.from_item = item return RES_EXHAUSTED
def res_resizeable_trigger(res: Property): """Replace two markers with a trigger brush. This is run once to affect all of an item. Options: * `markerInst`: <ITEM_ID:1,2> value referencing the marker instances, or a filename. * `markerItem`: The item's ID * `previewConf`: A item config which enables/disables the preview overlay. * `previewinst`: An instance to place at the marker location in preview mode. This should contain checkmarks to display the value when testing. * `previewMat`: If set, the material to use for an overlay func_brush. The brush will be parented to the trigger, so it vanishes once killed. It is also non-solid. * `previewScale`: The scale for the func_brush materials. * `previewActivate`, `previewDeactivate`: The `instance:name;Input` value to turn the previewInst on and off. * `triggerActivate, triggerDeactivate`: The outputs used when the trigger turns on or off. * `coopVar`: The instance variable which enables detecting both Coop players. The trigger will be a trigger_playerteam. * `coopActivate, coopDeactivate`: The outputs used when coopVar is enabled. These should be suitable for a logic_coop_manager. * `coopOnce`: If true, kill the manager after it first activates. * `keys`: A block of keyvalues for the trigger brush. Origin and targetname will be set automatically. * `localkeys`: The same as above, except values will be changed to use instance-local names. """ marker = instanceLocs.resolve(res['markerInst']) markers = {} for inst in vbsp.VMF.by_class['func_instance']: if inst['file'].casefold() in marker: markers[inst['targetname']] = inst if not markers: # No markers in the map - abort return RES_EXHAUSTED trig_act = res['triggerActivate', 'OnStartTouchAll'] trig_deact = res['triggerDeactivate', 'OnEndTouchAll'] coop_var = res['coopVar', None] coop_act = res['coopActivate', 'OnChangeToAllTrue'] coop_deact = res['coopDeactivate', 'OnChangeToAnyFalse'] coop_only_once = res.bool('coopOnce') marker_connection = conditions.CONNECTIONS[res['markerItem'].casefold()] mark_act_name, mark_act_out = marker_connection.out_act mark_deact_name, mark_deact_out = marker_connection.out_deact del marker_connection # Display preview overlays if it's preview mode, and the config is true if vbsp.IS_PREVIEW and vbsp_options.get_itemconf(res['previewConf', ''], False): preview_mat = res['previewMat', ''] preview_inst_file = res['previewInst', ''] pre_act_name, pre_act_inp = Output.parse_name(res['previewActivate', '']) pre_deact_name, pre_deact_inp = Output.parse_name( res['previewDeactivate', '']) preview_scale = srctools.conv_float(res['previewScale', '0.25'], 0.25) else: # Deactivate the preview_ options when publishing. preview_mat = preview_inst_file = '' pre_act_name = pre_deact_name = None pre_act_inp = pre_deact_inp = '' preview_scale = 0.25 # Now convert each brush # Use list() to freeze it, allowing us to delete from the dict for targ, inst in list(markers.items()): # type: str, VLib.Entity for out in inst.output_targets(): if out in markers: other = markers[out] # type: Entity del markers[out] # Don't let it get repeated break else: if inst.fixup['$connectioncount'] == '0': # If the item doesn't have any connections, 'connect' # it to itself so we'll generate a 1-block trigger. other = inst else: continue # It's a marker with an input, the other in the pair # will handle everything. for ent in {inst, other}: # Only do once if inst == other ent.remove() is_coop = coop_var is not None and vbsp.GAME_MODE == 'COOP' and ( inst.fixup.bool(coop_var) or other.fixup.bool(coop_var)) bbox_min, bbox_max = Vec.bbox(Vec.from_str(inst['origin']), Vec.from_str(other['origin'])) # Extend to the edge of the blocks. bbox_min -= 64 bbox_max += 64 out_ent = trig_ent = vbsp.VMF.create_ent( classname='trigger_multiple', # Default # Use the 1st instance's name - that way other inputs control the # trigger itself. targetname=targ, origin=inst['origin'], angles='0 0 0', ) trig_ent.solids = [ vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.TRIGGER, ).solid, ] # Use 'keys' and 'localkeys' blocks to set all the other keyvalues. conditions.set_ent_keys(trig_ent, inst, res) if is_coop: trig_ent['spawnflags'] = '1' # Clients trig_ent['classname'] = 'trigger_playerteam' out_ent_name = conditions.local_name(inst, 'man') out_ent = vbsp.VMF.create_ent(classname='logic_coop_manager', targetname=out_ent_name, origin=inst['origin']) if coop_only_once: # Kill all the ents when both players are present. out_ent.add_out( Output('OnChangeToAllTrue', out_ent_name, 'Kill'), Output('OnChangeToAllTrue', targ, 'Kill'), ) trig_ent.add_out( Output('OnStartTouchBluePlayer', out_ent_name, 'SetStateATrue'), Output('OnStartTouchOrangePlayer', out_ent_name, 'SetStateBTrue'), Output('OnEndTouchBluePlayer', out_ent_name, 'SetStateAFalse'), Output('OnEndTouchOrangePlayer', out_ent_name, 'SetStateBFalse'), ) act_out = coop_act deact_out = coop_deact else: act_out = trig_act deact_out = trig_deact if preview_mat: preview_brush = vbsp.VMF.create_ent( classname='func_brush', parentname=targ, origin=inst['origin'], Solidity='1', # Not solid drawinfastreflection='1', # Draw in goo.. # Disable shadows and lighting.. disableflashlight='1', disablereceiveshadows='1', disableshadowdepth='1', disableshadows='1', ) preview_brush.solids = [ # Make it slightly smaller, so it doesn't z-fight with surfaces. vbsp.VMF.make_prism( bbox_min + 0.5, bbox_max - 0.5, mat=preview_mat, ).solid, ] for face in preview_brush.sides(): face.scale = preview_scale if preview_inst_file: vbsp.VMF.create_ent( classname='func_instance', targetname=targ + '_preview', file=preview_inst_file, # Put it at the second marker, since that's usually # closest to antlines if present. origin=other['origin'], ) if pre_act_name and trig_act: out_ent.add_out( Output( trig_act, targ + '_preview', inst_in=pre_act_name, inp=pre_act_inp, )) if pre_deact_name and trig_deact: out_ent.add_out( Output( trig_deact, targ + '_preview', inst_in=pre_deact_name, inp=pre_deact_inp, )) # Now copy over the outputs from the markers, making it work. for out in inst.outputs + other.outputs: # Skip the output joining the two markers together. if out.target == other['targetname']: continue if out.inst_out == mark_act_name and out.output == mark_act_out: ent_out = act_out elif out.inst_out == mark_deact_name and out.output == mark_deact_out: ent_out = deact_out else: continue # Skip this output - it's somehow invalid for this item. if not ent_out: continue # Allow setting the output to "" to skip out_ent.add_out( Output( ent_out, out.target, inst_in=out.inst_in, inp=out.input, param=out.params, delay=out.delay, times=out.times, )) return RES_EXHAUSTED