def iter_lines(conf: Property) -> Iterator[Property]: """Iterate over the varios line blocks.""" yield from conf.find_all("Quotes", "Group", "Quote", "Line") yield from conf.find_all("Quotes", "Midchamber", "Quote", "Line") for group in conf.find_children("Quotes", "CoopResponses"): if group.has_children(): yield from group
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_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_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 resolve_inst(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 load_settings(pit: Property): if not pit: SETTINGS.clear() # No pits are permitted.. return SETTINGS.update({ 'use_skybox': srctools.conv_bool(pit['teleport', '0']), 'tele_dest': pit['tele_target', '@goo_targ'], 'tele_ref': pit['tele_ref', '@goo_ref'], 'off_x': srctools.conv_int(pit['off_x', '0']), 'off_y': srctools.conv_int(pit['off_y', '0']), 'skybox': pit['sky_inst', ''], 'skybox_ceil': pit['sky_inst_ceil', ''], 'targ': pit['targ_inst', ''], 'blend_light': pit['blend_light', ''] }) for inst_type in ( 'support', 'side', 'corner', 'double', 'triple', 'pillar', ): vals = [ prop.value for prop in pit.find_all(inst_type + '_inst') ] if len(vals) == 0: vals = [""] PIT_INST[inst_type] = vals
def load_settings(pit: Property): if not pit: SETTINGS.clear() # No pits are permitted.. return SETTINGS.update({ 'use_skybox': srctools.conv_bool(pit['teleport', '0']), 'tele_dest': pit['tele_target', '@goo_targ'], 'tele_ref': pit['tele_ref', '@goo_ref'], 'off_x': srctools.conv_int(pit['off_x', '0']), 'off_y': srctools.conv_int(pit['off_y', '0']), 'skybox': pit['sky_inst', ''], 'skybox_ceil': pit['sky_inst_ceil', ''], 'targ': pit['targ_inst', ''], 'blend_light': pit['blend_light', ''] }) for inst_type in ( 'support', 'side', 'corner', 'double', 'triple', 'pillar', ): vals = [ prop.value for prop in pit.find_all(inst_type + '_inst') ] if len(vals) == 0: vals = [""] PIT_INST[inst_type] = vals
def res_vactube_setup(res: Property): group = res['group', 'DEFAULT_GROUP'] if group not in VAC_CONFIGS: # Store our values in the CONFIGS dictionary inst_configs = VAC_CONFIGS[group] = {} else: # Grab the already-filled values, and add to them inst_configs = VAC_CONFIGS[group] def get_temp(key): try: temp_id = block['temp_' + key] except LookupError: return None try: return template_brush.get_template(temp_id) except template_brush.InvalidTemplateName: LOGGER.warning('Invalid template "{}" for vactube group {}!', temp_id, group) return None for block in res.find_all("Instance"): # Configuration info for each instance set.. conf = Config( # The three sizes of corner instance inst_corner=[ block['corner_small_inst', ''], block['corner_medium_inst', ''], block['corner_large_inst', ''], ], temp_corner=[ get_temp('corner_small'), get_temp('corner_medium'), get_temp('corner_large'), ], # Straight instances connected to the next part inst_straight=block['straight_inst', ''], # Supports attach to the 4 sides of the straight part, # if there's a brush there. inst_support=block['support_inst', ''], inst_entry_floor=block['entry_floor_inst'], inst_entry_wall=block['entry_inst'], inst_entry_ceil=block['entry_ceil_inst'], inst_exit=block['exit_inst'], ) for prop in block.find_all("File"): try: size_str, file = prop.value.split(":", 1) # Users enter 1-3, use 0-2 in code. size = srctools.conv_int(size_str, 1) - 1 except ValueError: size = 0 file = prop.value for inst in instanceLocs.resolve(file): inst_configs[inst] = conf, size return group
def generate_fizzler_sides(self, conf: Property): fizz_colors = {} mat_path = self.abs_path('bee2/materials/BEE2/fizz_sides/side_color_') for brush_conf in conf.find_all('Fizzlers', 'Fizzler', 'Brush'): fizz_color = brush_conf['Side_color', ''] if fizz_color: fizz_colors[Vec.from_str(fizz_color).as_tuple()] = ( brush_conf.float('side_alpha', 1), brush_conf['side_vortex', fizz_color]) if fizz_colors: os.makedirs(self.abs_path('bee2/materials/BEE2/fizz_sides/'), exist_ok=True) for fizz_color, (alpha, fizz_vortex_color) in fizz_colors.items(): file_path = mat_path + '{:02X}{:02X}{:02X}.vmt'.format( round(fizz_color.x * 255), round(fizz_color.y * 255), round(fizz_color.z * 255), ) with open(file_path, 'w') as f: f.write( FIZZLER_EDGE_MAT.format(Vec(fizz_color), fizz_vortex_color)) if alpha != 1: # Add the alpha value, but replace 0.5 -> .5 to save a char. f.write('$outputintensity {}\n'.format( format(alpha, 'g').replace('0.', '.'))) f.write(FIZZLER_EDGE_MAT_PROXY)
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 set_cond_source(props: Property, source: str) -> None: """Set metadata for Conditions in the given config blocks. This generates '__src__' keyvalues in Condition blocks with info like the source object ID and originating file, so errors can be traced back to the config file creating it. """ for cond in props.find_all('Conditions', 'Condition'): cond['__src__'] = source
def res_timed_relay_setup(res: Property): var = res['variable', consts.FixupVars.TIM_DELAY] name = res['targetname'] disabled = res['disabled', '0'] flags = res['spawnflags', '0'] final_outs = [ Output.parse(subprop) for prop in res.find_all('FinalOutputs') for subprop in prop ] rep_outs = [ Output.parse(subprop) for prop in res.find_all('RepOutputs') for subprop in prop ] # Never use the comma seperator in the final output for consistency. for out in itertools.chain(rep_outs, final_outs): out.comma_sep = False return var, name, disabled, flags, final_outs, rep_outs
def clean_editor_models(self, editoritems: Property): """The game is limited to having 1024 models loaded at once. Editor models are always being loaded, so we need to keep the number small. Go through editoritems, and disable (by renaming to .mdl_dis) unused ones. """ # If set, force them all to be present. force_on = GEN_OPTS.get_bool('Debug', 'force_all_editor_models') used_models = { mdl.value.rsplit('.', 1)[0].casefold() for mdl in editoritems.find_all( 'Item', 'Editor', 'Subtype', 'Model', 'ModelName', ) } mdl_count = 0 for mdl_folder in [ self.abs_path('bee2/models/props_map_editor/'), self.abs_path('bee2_dev/models/props_map_editor/'), ]: if not os.path.exists(mdl_folder): continue for file in os.listdir(mdl_folder): if not file.endswith(('.mdl', '.mdl_dis')): continue mdl_count += 1 file_no_ext, ext = os.path.splitext(file) if force_on or file_no_ext.casefold() in used_models: new_ext = '.mdl' else: new_ext = '.mdl_dis' if new_ext != ext: try: os.remove( os.path.join(mdl_folder, file_no_ext + new_ext)) except FileNotFoundError: pass os.rename( os.path.join(mdl_folder, file_no_ext + ext), os.path.join(mdl_folder, file_no_ext + new_ext), ) LOGGER.info('{}/{} editor models used.', len(used_models), mdl_count)
def gen_sound_manifest(additional, excludes): """Generate a new game_sounds_manifest.txt file. This includes all the current scripts defined, plus any custom ones. Excludes is a list of scripts to remove from the listing - this allows overriding the sounds without VPK overrides. """ if not additional: return # Don't pack, there aren't any new sounds.. orig_manifest = os.path.join( '..', SOUND_MAN_FOLDER.get(CONF['game_id', ''], 'portal2'), 'scripts', 'game_sounds_manifest.txt', ) try: with open(orig_manifest) as f: props = Property.parse(f, orig_manifest).find_key( 'game_sounds_manifest', [], ) except FileNotFoundError: # Assume no sounds props = Property('game_sounds_manifest', []) scripts = [prop.value for prop in props.find_all('precache_file')] for script in additional: scripts.append(script) for script in excludes: try: scripts.remove(script) except ValueError: LOGGER.warning( '"{}" should be excluded, but it\'s' ' not in the manifest already!', script, ) # Build and unbuild it to strip other things out - Valve includes a bogus # 'new_sound_scripts_must_go_below_here' entry.. new_props = Property('game_sounds_manifest', [Property('precache_file', file) for file in scripts]) inject_loc = os.path.join('bee2', 'inject', 'soundscript_manifest.txt') with open(inject_loc, 'w') as f: for line in new_props.export(): f.write(line) LOGGER.info('Written new soundscripts_manifest..')
def res_timed_relay_setup(res: Property): var = res['variable', consts.FixupVars.TIM_DELAY] name = res['targetname'] disabled = res['disabled', '0'] flags = res['spawnflags', '0'] final_outs = [ Output.parse(subprop) for prop in res.find_all('FinalOutputs') for subprop in prop ] rep_outs = [ Output.parse(subprop) for prop in res.find_all('RepOutputs') for subprop in prop ] # Never use the comma seperator in the final output for consistency. for out in itertools.chain(rep_outs, final_outs): out.comma_sep = False return var, name, disabled, flags, final_outs, rep_outs
def res_cust_antline_setup(res: Property): def find(cat): """Helper to reduce code duplication.""" return [p.value for p in res.find_all(cat)] # Allow overriding these options. If unset use the style's value - the # amount of destruction will usually be the same. broken_chance = res.float( 'broken_antline_chance', vbsp_options.get(float, 'broken_antline_chance'), ) broken_dist = res.int( 'broken_antline_distance', vbsp_options.get(int, 'broken_antline_distance'), ) toggle_inst = res['instance', ''] toggle_out = list(res.find_all('addOut')) # These textures are required - the base ones. straight_tex = find('straight') corner_tex = find('corner') # Arguments to pass to setAntlineMat straight_args = [ straight_tex, find('straightFloor') or (), # Extra broken antline textures / options, if desired. broken_chance, broken_dist, find('brokenStraight') or (), find('brokenStraightFloor') or (), ] # The same but for corners. corner_args = [ corner_tex, find('cornerFloor') or (), broken_chance, broken_dist, find('brokenCorner') or (), find('brokenCornerFloor') or (), ] if not straight_tex or not corner_tex: # If we don't have two textures, something's wrong. Remove this result. LOGGER.warning('custAntline has no textures!') return None else: return straight_args, corner_args, toggle_inst, toggle_out
def read_configs(conf: Property) -> None: """Read in the fizzler data.""" for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'): fizz = FizzlerType.parse(fizz_conf) if fizz.id in FIZZ_TYPES: raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id)) FIZZ_TYPES[fizz.id] = fizz LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES)) if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']: return # In Aperture Tag, we don't have portals. For fizzler types which block # portals (trigger_portal_cleanser), additionally fizzle paint. for fizz in FIZZ_TYPES.values(): for brush in fizz.brushes: if brush.keys['classname'].casefold() == 'trigger_portal_cleanser': brush_name = brush.name # Retrieve what key is used for start-disabled. brush_start_disabled = None for key_map in [brush.keys, brush.local_keys]: if brush_start_disabled is None: for key, value in key_map.items(): if key.casefold() == 'startdisabled': brush_start_disabled = value break break # Jump past else. else: # No fizzlers in this item. continue # Add a paint fizzler brush to these fizzlers. fizz.brushes.append( FizzlerBrush( brush_name, textures={ TexGroup.TRIGGER: const.Tools.TRIGGER, }, keys={ 'classname': 'trigger_paint_cleanser', 'startdisabled': brush_start_disabled or '0', 'spawnflags': '9', }, local_keys={}, outputs=[], singular=True, ))
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 read_configs(conf: Property) -> None: """Read in the fizzler data.""" for fizz_conf in conf.find_all('Fizzlers', 'Fizzler'): fizz = FizzlerType.parse(fizz_conf) if fizz.id in FIZZ_TYPES: raise ValueError('Duplicate fizzler ID "{}"'.format(fizz.id)) FIZZ_TYPES[fizz.id] = fizz LOGGER.info('Loaded {} fizzlers.', len(FIZZ_TYPES)) if vbsp_options.get(str, 'game_id') != utils.STEAM_IDS['APTAG']: return # In Aperture Tag, we don't have portals. For fizzler types which block # portals (trigger_portal_cleanser), additionally fizzle paint. for fizz in FIZZ_TYPES.values(): for brush in fizz.brushes: if brush.keys['classname'].casefold() == 'trigger_portal_cleanser': brush_name = brush.name # Retrieve what key is used for start-disabled. brush_start_disabled = None for key_map in [brush.keys, brush.local_keys]: if brush_start_disabled is None: for key, value in key_map.items(): if key.casefold() == 'startdisabled': brush_start_disabled = value break break # Jump past else. else: # No fizzlers in this item. continue # Add a paint fizzler brush to these fizzlers. fizz.brushes.append(FizzlerBrush( brush_name, textures={ TexGroup.TRIGGER: const.Tools.TRIGGER, }, keys={ 'classname': 'trigger_paint_cleanser', 'startdisabled': brush_start_disabled or '0', 'spawnflags': '9', }, local_keys={}, outputs=[], singular=True, ))
def res_cust_output_setup(res: Property): conds = [ Condition.parse(sub_res) for sub_res in res if sub_res.name == 'targcondition' ] outputs = list(res.find_all('addOut')) dec_con_count = srctools.conv_bool(res["decConCount", '0'], False) sign_type = IND_PANEL_TYPES.get(res['sign_type', None], None) if sign_type is None: sign_act = sign_deact = (None, '') else: # The outputs which trigger the sign. sign_act = Output.parse_name(res['sign_activate', '']) sign_deact = Output.parse_name(res['sign_deactivate', '']) return outputs, dec_con_count, conds, sign_type, sign_act, sign_deact
def parse(cls, prop: Property) -> 'AntType': """Parse this from a property block.""" broken_chance = prop.float('broken_chance') tex_straight: List[AntTex] = [] tex_corner: List[AntTex] = [] brok_straight: List[AntTex] = [] brok_corner: List[AntTex] = [] for ant_list, name in zip( [tex_straight, tex_corner, brok_straight, brok_corner], ('straight', 'corner', 'broken_straight', 'broken_corner'), ): for sub_prop in prop.find_all(name): ant_list.append(AntTex.parse(sub_prop)) return cls( tex_straight, tex_corner, brok_straight, brok_corner, broken_chance, )
def parse(cls, prop: Property): """Parse this from a property block.""" broken_chance = prop.float('broken_chance') tex_straight = [] tex_corner = [] brok_straight = [] brok_corner = [] for ant_list, name in zip( [tex_straight, tex_corner, brok_straight, brok_corner], ('straight', 'corner', 'broken_straight', 'broken_corner'), ): for sub_prop in prop.find_all(name): ant_list.append(AntTex.parse(sub_prop)) return cls( tex_straight, tex_corner, brok_straight, brok_corner, broken_chance, )
def gen_part_manifest(additional): """Generate a new particle system manifest file. This includes all the current ones defined, plus any custom ones. """ if not additional: return # Don't pack, there aren't any new particles.. orig_manifest = os.path.join( '..', GAME_FOLDER.get(CONF['game_id', ''], 'portal2'), 'particles', 'particles_manifest.txt', ) try: with open(orig_manifest) as f: props = Property.parse(f, orig_manifest).find_key( 'particles_manifest', [], ) except FileNotFoundError: # Assume no particles props = Property('particles_manifest', []) parts = [prop.value for prop in props.find_all('file')] for particle in additional: parts.append(particle) # Build and unbuild it to strip comments and similar lines. new_props = Property('particles_manifest', [Property('file', file) for file in parts]) inject_loc = os.path.join('bee2', 'inject', 'particles_manifest.txt') with open(inject_loc, 'w') as f: for line in new_props.export(): f.write(line) LOGGER.info('Written new particles_manifest..')
def desc_parse( info: Property, desc_id: str = '', *, prop_name: str = 'description', ) -> tkMarkdown.MarkdownData: """Parse the description blocks, to create data which matches richTextBox. """ has_warning = False lines = [] for prop in info.find_all(prop_name): if prop.has_children(): for line in prop: if line.name and not has_warning: LOGGER.warning('Old desc format: {}', desc_id) has_warning = True lines.append(line.value) else: lines.append(prop.value) return tkMarkdown.convert('\n'.join(lines))
def parse(cls, prop: Property) -> AntType: """Parse this from a property block.""" broken_chance = prop.float('broken_chance') tex_straight: list[AntTex] = [] tex_corner: list[AntTex] = [] brok_straight: list[AntTex] = [] brok_corner: list[AntTex] = [] for ant_list, name in zip( [tex_straight, tex_corner, brok_straight, brok_corner], ('straight', 'corner', 'broken_straight', 'broken_corner'), ): for sub_prop in prop.find_all(name): ant_list.append(AntTex.parse(sub_prop)) if broken_chance < 0.0: LOGGER.warning('Antline broken chance must be between 0-100, got "{}"!', prop['broken_chance']) broken_chance = 0.0 if broken_chance > 100.0: LOGGER.warning('Antline broken chance must be between 0-100, got "{}"!', prop['broken_chance']) broken_chance = 100.0 if broken_chance == 0.0: brok_straight.clear() brok_corner.clear() # Cannot have broken corners if corners/straights are the same. if not tex_corner: brok_corner.clear() return cls( tex_straight, tex_corner, brok_straight, brok_corner, broken_chance, )
def edit_panel(vmf: VMF, inst: Entity, props: Property, create: bool) -> None: """Implements SetPanelOptions and CreatePanel.""" orient = Matrix.from_angle(Angle.from_str(inst['angles'])) normal: Vec = round(props.vec('normal', 0, 0, 1) @ orient, 6) origin = Vec.from_str(inst['origin']) uaxis, vaxis = Vec.INV_AXIS[normal.axis()] points: set[tuple[float, float, float]] = set() if 'point' in props: for prop in props.find_all('point'): points.add( conditions.resolve_offset(inst, prop.value, zoff=-64).as_tuple()) elif 'pos1' in props and 'pos2' in props: pos1, pos2 = Vec.bbox( conditions.resolve_offset(inst, props['pos1', '-48 -48 0'], zoff=-64), conditions.resolve_offset(inst, props['pos2', '48 48 0'], zoff=-64), ) points.update(map(Vec.as_tuple, Vec.iter_grid(pos1, pos2, 32))) else: # Default to the full tile. points.update({(Vec(u, v, -64.0) @ orient + origin).as_tuple() for u in [-48.0, -16.0, 16.0, 48.0] for v in [-48.0, -16.0, 16.0, 48.0]}) tiles_to_uv: dict[tiling.TileDef, set[tuple[int, int]]] = defaultdict(set) for pos in points: try: tile, u, v = tiling.find_tile(Vec(pos), normal, force=create) except KeyError: continue tiles_to_uv[tile].add((u, v)) if not tiles_to_uv: LOGGER.warning('"{}": No tiles found for panels!', inst['targetname']) return # If bevels is provided, parse out the overall world positions. bevel_world: set[tuple[int, int]] | None try: bevel_prop = props.find_key('bevel') except NoKeyError: bevel_world = None else: bevel_world = set() if bevel_prop.has_children(): # Individually specifying offsets. for bevel_str in bevel_prop.as_array(): bevel_point = Vec.from_str(bevel_str) @ orient + origin bevel_world.add( (int(bevel_point[uaxis]), int(bevel_point[vaxis]))) elif srctools.conv_bool(bevel_prop.value): # Fill the bounding box. bbox_min, bbox_max = Vec.bbox(map(Vec, points)) off = Vec.with_axes(uaxis, 32, vaxis, 32) bbox_min -= off bbox_max += off for pos in Vec.iter_grid(bbox_min, bbox_max, 32): if pos.as_tuple() not in points: bevel_world.add((pos[uaxis], pos[vaxis])) # else: No bevels. panels: list[tiling.Panel] = [] for tile, uvs in tiles_to_uv.items(): if create: panel = tiling.Panel( None, inst, tiling.PanelType.NORMAL, thickness=4, bevels=(), ) panel.points = uvs tile.panels.append(panel) else: for panel in tile.panels: if panel.same_item(inst) and panel.points == uvs: break else: LOGGER.warning('No panel to modify found for "{}"!', inst['targetname']) continue panels.append(panel) pan_type = '<nothing?>' try: pan_type = conditions.resolve_value(inst, props['type']) panel.pan_type = tiling.PanelType(pan_type.lower()) except LookupError: pass except ValueError: raise ValueError('Unknown panel type "{}"!'.format(pan_type)) if 'thickness' in props: panel.thickness = srctools.conv_int( conditions.resolve_value(inst, props['thickness'])) if panel.thickness not in (2, 4, 8): raise ValueError( '"{}": Invalid panel thickess {}!\n' 'Must be 2, 4 or 8.', inst['targetname'], panel.thickness, ) if bevel_world is not None: panel.bevels.clear() for u, v in bevel_world: # Convert from world points to UV positions. u = (u - tile.pos[uaxis] + 48) // 32 v = (v - tile.pos[vaxis] + 48) // 32 # Cull outside here, we wont't use them. if -1 <= u <= 4 and -1 <= v <= 4: panel.bevels.add((u, v)) if 'offset' in props: panel.offset = conditions.resolve_offset(inst, props['offset']) panel.offset -= Vec.from_str(inst['origin']) if 'template' in props: # We only want the template inserted once. So remove it from all but one. if len(panels) == 1: panel.template = inst.fixup.substitute(props['template']) else: panel.template = '' if 'nodraw' in props: panel.nodraw = srctools.conv_bool( inst.fixup.substitute(props['nodraw'], allow_invert=True)) if 'seal' in props: panel.seal = srctools.conv_bool( inst.fixup.substitute(props['seal'], allow_invert=True)) if 'move_bullseye' in props: panel.steals_bullseye = srctools.conv_bool( inst.fixup.substitute(props['move_bullseye'], allow_invert=True)) if 'keys' in props or 'localkeys' in props: # First grab the existing ent, so we can edit it. # These should all have the same value, unless they were independently # edited with mismatching point sets. In that case destroy all those existing ones. existing_ents: set[Entity | None] = {panel.brush_ent for panel in panels} try: [brush_ent] = existing_ents except ValueError: LOGGER.warning( 'Multiple independent panels for "{}" were made, then the ' 'brush entity was edited as a group! Discarding ' 'individual ents...', inst['targetname']) for brush_ent in existing_ents: if brush_ent is not None and brush_ent in vmf.entities: brush_ent.remove() brush_ent = None if brush_ent is None: brush_ent = vmf.create_ent('') old_pos = brush_ent.keys.pop('origin', None) conditions.set_ent_keys(brush_ent, inst, props) if not brush_ent['classname']: if create: # This doesn't make sense, you could just omit the prop. LOGGER.warning( 'No classname provided for panel "{}"!', inst['targetname'], ) # Make it a world brush. brush_ent.remove() brush_ent = None else: # We want to do some post-processing. # Localise any origin value. if 'origin' in brush_ent.keys: pos = Vec.from_str(brush_ent['origin']) pos.localise( Vec.from_str(inst['origin']), Angle.from_str(inst['angles']), ) brush_ent['origin'] = pos elif old_pos is not None: brush_ent['origin'] = old_pos # If it's func_detail, clear out all the keys. # Particularly `origin`, but the others are useless too. if brush_ent['classname'] == 'func_detail': brush_ent.clear_keys() brush_ent['classname'] = 'func_detail' for panel in panels: panel.brush_ent = brush_ent
def res_reshape_fizzler(vmf: VMF, shape_inst: Entity, res: Property): """Convert a fizzler connected via the output to a new shape. This allows for different placing of fizzler items. Each `segment` parameter should be a `x y z;x y z` pair of positions that represent the ends of the fizzler. `up_axis` should be set to a normal vector pointing in the new 'upward' direction. `default` is the ID of a fizzler type which should be used if no outputs are fired. """ shape_name = shape_inst['targetname'] shape_item = connections.ITEMS[shape_name] for conn in shape_item.outputs: fizz_name = conn.inp.name try: fizz = fizzler.FIZZLERS[fizz_name] except KeyError: LOGGER.warning( 'Reshaping fizzler with non-fizzler output! Ignoring!') continue break else: # No fizzler - create one. conn = None fizz_type = fizzler.FIZZ_TYPES[res['default']] base_inst = vmf.create_ent( targetname=shape_name, classname='func_instance', origin=shape_inst['origin'], file=fizz_type.inst[fizzler.FizzInst.BASE][0], ) base_inst.fixup.update(shape_inst.fixup) fizz = fizzler.FIZZLERS[shape_name] = fizzler.Fizzler( fizz_type, Vec(), base_inst, [], ) # Detach this connection and remove traces of it. if conn: conn.remove() if shape_item.ind_toggle: remove_ant_toggle(shape_item.ind_toggle) fizz_base = fizz.base_inst fizz_base['origin'] = shape_inst['origin'] origin = Vec.from_str(shape_inst['origin']) shape_angles = Vec.from_str(shape_inst['angles']) fizz.up_axis = res.vec('up_axis').rotate(*shape_angles) fizz.emitters.clear() for seg_prop in res.find_all('Segment'): vec1, vec2 = seg_prop.value.split(';') seg_min_max = Vec.bbox( Vec.from_str(vec1).rotate(*shape_angles) + origin, Vec.from_str(vec2).rotate(*shape_angles) + origin, ) fizz.emitters.append(seg_min_max)
def parse(item_id: str, conf: Property): """Read the item type info from the given config.""" def get_outputs(prop_name): """Parse all the outputs with this name.""" return [ Output.parse(prop) for prop in conf.find_all(prop_name) # Allow blank to indicate no output. if prop.value != '' ] enable_cmd = get_outputs('enable_cmd') disable_cmd = get_outputs('disable_cmd') lock_cmd = get_outputs('lock_cmd') unlock_cmd = get_outputs('unlock_cmd') inf_lock_only = conf.bool('inf_lock_only') timer_done_cmd = get_outputs('timer_done_cmd') if 'timer_sound_pos' in conf: timer_sound_pos = conf.vec('timer_sound_pos') force_timer_sound = conf.bool('force_timer_sound') else: timer_sound_pos = None force_timer_sound = False try: input_type = InputType( conf['Type', 'default'].casefold() ) except ValueError: raise ValueError('Invalid input type "{}": {}'.format( item_id, conf['type'], )) from None invert_var = conf['invertVar', '0'] try: spawn_fire = FeatureMode(conf['spawnfire', 'never'].casefold()) except ValueError: # Older config option - it was a bool for always/never. spawn_fire_bool = conf.bool('spawnfire', None) if spawn_fire_bool is None: raise # Nope, not a bool. spawn_fire = FeatureMode.ALWAYS if spawn_fire_bool else FeatureMode.NEVER try: sec_spawn_fire = FeatureMode(conf['sec_spawnfire', 'never'].casefold()) except ValueError: # Default to primary value. sec_spawn_fire = FeatureMode.NEVER if input_type is InputType.DUAL: sec_enable_cmd = get_outputs('sec_enable_cmd') sec_disable_cmd = get_outputs('sec_disable_cmd') try: default_dual = CONN_TYPE_NAMES[ conf['Default_Dual', 'primary'].casefold() ] except KeyError: raise ValueError('Invalid default type for "{}": {}'.format( item_id, conf['Default_Dual'], )) from None # We need an affinity to use when nothing else specifies it. if default_dual is ConnType.DEFAULT: raise ValueError('Must specify a default type for "{}"!'.format( item_id, )) from None sec_invert_var = conf['sec_invertVar', '0'] else: # No dual type, set to dummy values. sec_enable_cmd = [] sec_disable_cmd = [] default_dual = ConnType.DEFAULT sec_invert_var = '' try: output_type = CONN_TYPE_NAMES[ conf['DualType', 'default'].casefold() ] except KeyError: raise ValueError('Invalid output affinity for "{}": {}'.format( item_id, conf['DualType'], )) from None def get_input(prop_name: str): """Parse an input command.""" try: return Output.parse_name(conf[prop_name]) except IndexError: return None out_act = get_input('out_activate') out_deact = get_input('out_deactivate') out_lock = get_input('out_lock') out_unlock = get_input('out_unlock') timer_start = timer_stop = None if 'out_timer_start' in conf: timer_start = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_start') if prop.value ] if 'out_timer_stop' in conf: timer_stop = [ Output.parse_name(prop.value) for prop in conf.find_all('out_timer_stop') if prop.value ] return Config( item_id, default_dual, input_type, spawn_fire, invert_var, enable_cmd, disable_cmd, sec_spawn_fire, sec_invert_var, sec_enable_cmd, sec_disable_cmd, output_type, out_act, out_deact, lock_cmd, unlock_cmd, out_lock, out_unlock, inf_lock_only, timer_sound_pos, timer_done_cmd, force_timer_sound, timer_start, timer_stop, )
def res_add_shuffle_group(vmf: VMF, res: Property) -> Callable[[Entity], None]: """Pick from a pool of instances to randomise decoration. For each sub-condition that succeeds, a random instance is placed, with a fixup set to a value corresponding to the condition. Parameters: - Var: The fixup variable to set on each item. This is used to tweak it to match the condition. - Conditions: Each value here is the value to produce if this instance is required. The contents of the block is then a condition flag to check. - Pool: A list of instances to randomly allocate to the conditions. There should be at least as many pool values as there are conditions. - Seed: Value to modify the seed with before placing. """ conf_variable = res['var'] conf_seed = 'sg' + res['seed', ''] conf_pools: dict[str, list[str]] = {} for prop in res.find_children('pool'): if prop.has_children(): raise ValueError('Instances in pool cannot be a property block!') conf_pools.setdefault(prop.name, []).append(prop.value) # (flag, value, pools) conf_selectors: list[tuple[list[Property], str, frozenset[str]]] = [] for prop in res.find_all('selector'): conf_value = prop['value', ''] conf_flags = list(prop.find_children('conditions')) try: picked_pools = prop['pools'].casefold().split() except LookupError: picked_pools = frozenset(conf_pools) else: for pool_name in picked_pools: if pool_name not in conf_pools: raise ValueError(f'Unknown pool name {pool_name}!') conf_selectors.append( (conf_flags, conf_value, frozenset(picked_pools))) all_pools = [(name, inst) for name, instances in conf_pools.items() for inst in instances] all_pools.sort() # Ensure consistent order. def add_group(inst: Entity) -> None: """Place the group.""" rng = rand.seed(b'shufflegroup', conf_seed, inst) pools = all_pools.copy() for (flags, value, potential_pools) in conf_selectors: for flag in flags: if not conditions.check_flag(vmf, flag, inst): break else: # Succeeded. allowed_inst = [(name, inst) for (name, inst) in pools if name in potential_pools] name, filename = rng.choice(allowed_inst) pools.remove((name, filename)) vmf.create_ent( 'func_instance', targetname=inst['targetname'], file=filename, angles=inst['angles'], origin=inst['origin'], fixup_style='0', ).fixup[conf_variable] = value return add_group
def res_fix_rotation_axis(ent: Entity, res: Property): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * `func_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = srctools.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum(map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') )) name = conditions.local_name(ent, res['name', '']) axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str( res['Pos', '0 0 0'] ).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out(Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=( 1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def find_group_quotes( vmf: VMF, group: Property, mid_quotes, allow_mid_voices, use_dings, conf, mid_name: str, player_flag_set: Set[str], ) -> Iterator[PossibleQuote]: """Scan through a group, looking for applicable quote options.""" is_mid = (group.name == 'midchamber') if is_mid: group_id = 'MIDCHAMBER' else: group_id = group['name'].upper() all_quotes = list(group.find_all('quote')) valid_quotes = 0 for quote in all_quotes: valid_quote = True for flag in quote: name = flag.name if name in ('priority', 'name', 'id', 'line') or name.startswith('line_'): # Not flags! continue if not conditions.check_flag(vmf, flag, fake_inst): valid_quote = False break if not valid_quote: continue valid_quotes += 1 poss_quotes = [] line_mid_quotes = [] for line in mode_quotes(quote, player_flag_set): line_id = line['id', line['name', '']].casefold() # Check if the ID is enabled! if conf.get_bool(group_id, line_id, True): if allow_mid_voices and is_mid: line_mid_quotes.append((line, use_dings, mid_name)) else: poss_quotes.append(line) else: LOGGER.info( 'Line "{}" is disabled..', line['name', '??'], ) if line_mid_quotes: mid_quotes.append(line_mid_quotes) if poss_quotes: yield PossibleQuote( quote['priority', '0'], poss_quotes, ) LOGGER.info('"{}": {}/{} quotes..', group_id, valid_quotes, len(all_quotes))
def res_conveyor_belt(vmf: VMF, inst: Entity, res: Property) -> None: """Create a conveyor belt. * Options: * `SegmentInst`: Generated at each square. (`track` is the name of the path to attach to.) * `TrackTeleport`: Set the track points so they teleport trains to the start. * `Speed`: The fixup or number for the train speed. * `MotionTrig`: If set, a trigger_multiple will be spawned that `EnableMotion`s weighted cubes. The value is the name of the relevant filter. * `EndOutput`: Adds an output to the last track. The value is the same as outputs in VMFs. `RotateSegments`: If true (default), force segments to face in the direction of movement. * `BeamKeys`: If set, a list of keyvalues to use to generate an env_beam travelling from start to end. The origin is treated specially - X is the distance from walls, y is the distance to the side, and z is the height. `RailTemplate`: A template for the track sections. This is made into a non-solid func_brush, combining all sections. * `NoPortalFloor`: If set, add a `func_noportal_volume` on the floor under the track. * `PaintFizzler`: If set, add a paint fizzler underneath the belt. """ move_dist = inst.fixup.int('$travel_distance') if move_dist <= 2: # There isn't room for a conveyor, so don't bother. inst.remove() return orig_orient = Matrix.from_angle(Angle.from_str(inst['angles'])) move_dir = Vec(1, 0, 0) @ Angle.from_str(inst.fixup['$travel_direction']) move_dir = move_dir @ orig_orient start_offset = inst.fixup.float('$starting_position') teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = instanceLocs.resolve_one(res['SegmentInst', '']) rail_template = res['RailTemplate', None] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos norm = orig_orient.up() if res.bool('rotateSegments', True): orient = Matrix.from_basis(x=move_dir, z=norm) inst['angles'] = orient.to_angle() else: orient = orig_orient # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place tracks at the top, so they don't appear inside wall sections. track_start: Vec = start_pos + 48 * norm track_end: Vec = end_pos + 48 * norm for index, pos in enumerate(track_start.iter_line(track_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != track_end: seg_inst = conditions.add_inst( vmf, targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=orient, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = template_brush.import_template( vmf, rail_template, pos, orient, force_type=template_brush.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=track_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. try: beam_keys = res.find_key('BeamKeys') except LookupError: pass else: beam = vmf.create_ent(classname='env_beam') beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam']) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ) @ orient beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ) @ orient # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out( Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append( vmf.make_prism( start_pos + Vec(72, -56, 58) @ orient, end_pos + Vec(-72, 56, 144) @ orient, mat=consts.Tools.TRIGGER, ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=track_start, ) floor_noportal.solids.append( vmf.make_prism( start_pos + Vec(-60, -60, -66) @ orient, end_pos + Vec(60, 60, -60) @ orient, mat=consts.Tools.INVISIBLE, ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48) @ orient, end_pos + Vec(64, 64, 56) @ orient, mat=consts.Tools.INVISIBLE, ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = consts.Tools.TRIGGER
def build_instance_data(editoritems: Property): """Build a property tree listing all of the instances for each item. as well as another listing the input and output commands. VBSP uses this to reduce duplication in VBSP_config files. This additionally strips custom instance definitions from the original list. """ instance_locs = Property("AllInstances", []) cust_inst = Property("CustInstances", []) commands = Property("Connections", []) item_classes = Property("ItemClasses", []) root_block = Property(None, [instance_locs, item_classes, cust_inst, commands]) for item in editoritems.find_all("Item"): instance_block = Property(item["Type"], []) instance_locs.append(instance_block) comm_block = Property(item["Type"], []) for inst_block in item.find_all("Exporting", "instances"): for inst in inst_block.value[:]: # type: Property if inst.name.isdigit(): # Direct Portal 2 value instance_block.append(Property("Instance", inst["Name"])) else: # It's a custom definition, remove from editoritems inst_block.value.remove(inst) # Allow the name to start with 'bee2_' also to match # the <> definitions - it's ignored though. name = inst.name if name[:5] == "bee2_": name = name[5:] cust_inst.set_key( (item["type"], name), # Allow using either the normal block format, # or just providing the file - we don't use the # other values. inst["name"] if inst.has_children() else inst.value, ) # Look in the Inputs and Outputs blocks to find the io definitions. # Copy them to property names like 'Input_Activate'. for io_type in ("Inputs", "Outputs"): for block in item.find_all("Exporting", io_type, CONN_NORM): for io_prop in block: comm_block[io_type[:-1] + "_" + io_prop.real_name] = io_prop.value # The funnel item type is special, having the additional input type. # Handle that specially. if item["type"] == "item_tbeam": for block in item.find_all("Exporting", "Inputs", CONN_FUNNEL): for io_prop in block: comm_block["TBEAM_" + io_prop.real_name] = io_prop.value # Fizzlers don't work correctly with outputs. This is a signal to # conditions.fizzler, but it must be removed in editoritems. if item["ItemClass", ""].casefold() == "itembarrierhazard": for block in item.find_all("Exporting", "Outputs"): if CONN_NORM in block: del block[CONN_NORM] # Record the itemClass for each item type. item_classes[item["type"]] = item["ItemClass", "ItemBase"] # Only add the block if the item actually has IO. if comm_block.value: commands.append(comm_block) return root_block.export()
def res_cust_fizzler(base_inst: Entity, res: Property): """Customises the various components of a custom fizzler item. This should be executed on the base instance. Brush and MakeLaserField are not permitted on laserfield barriers. When executed, the $is_laser variable will be set on the base. Options: * ModelName: sets the targetname given to the model instances. * UniqueModel: If true, each model instance will get a suffix to allow unique targetnames. * Brush: A brush entity that will be generated (the original is deleted.) This cannot be used on laserfields. * Name is the instance name for the brush * Left/Right/Center/Short/Nodraw are the textures used * Keys are a block of keyvalues to be set. Targetname and Origin are auto-set. * Thickness will change the thickness of the fizzler if set. By default it is 2 units thick. * Outputs is a block of outputs (laid out like in VMFs). The targetnames will be localised to the instance. * MergeBrushes, if true will merge this brush set into one entity for each fizzler. This is useful for non-fizzlers to reduce the entity count. * SimplifyBrush, if true will merge the three parts into one brush. All sides will receive the "nodraw" texture at 0.25 scale. * MaterialModify generates material_modify_controls to control the brush. One is generated for each texture used in the brush. This has subkeys 'name' and 'var' - the entity name and shader variable to be modified. MergeBrushes must be enabled if this is present. * MakeLaserField generates a brush stretched across the whole area. * Name, keys and thickness are the same as the regular Brush. * Texture/Nodraw are the textures. * Width is the pixel width of the laser texture, used to scale it correctly. """ model_name = res['modelname', None] make_unique = res.bool('UniqueModel') fizz_name = base_inst['targetname', ''] # search for the model instances model_targetnames = ( fizz_name + '_modelStart', fizz_name + '_modelEnd', ) is_laser = False for inst in vbsp.VMF.by_class['func_instance']: if inst['targetname'] in model_targetnames: if inst.fixup['skin', '0'] == '2': is_laser = True if model_name is not None: if model_name == '': inst['targetname'] = base_inst['targetname'] else: inst['targetname'] = ( base_inst['targetname'] + '-' + model_name ) if make_unique: inst.make_unique() for key, value in base_inst.fixup.items(): inst.fixup[key] = value base_inst.fixup['$is_laser'] = is_laser new_brush_config = list(res.find_all('brush')) if len(new_brush_config) == 0: return # No brush modifications if is_laser: # This is a laserfield! We can't edit those brushes! LOGGER.warning('CustFizzler executed on LaserField!') return # Record which materialmodify controls are used, so we can add if needed. # Conf id -> (brush_name, conf, [textures]) modify_controls = {} for orig_brush in ( vbsp.VMF.by_class['trigger_portal_cleanser'] & vbsp.VMF.by_target[fizz_name + '_brush']): orig_brush.remove() for config in new_brush_config: new_brush = orig_brush.copy() # Unique to the particular config property & fizzler name conf_key = (id(config), fizz_name) if config.bool('SimplifyBrush'): # Replace the brush with a simple one of the same size. bbox_min, bbox_max = new_brush.get_bbox() new_brush.solids = [vbsp.VMF.make_prism( bbox_min, bbox_max, mat=const.Tools.NODRAW, ).solid] should_merge = config.bool('MergeBrushes') if should_merge and conf_key in FIZZ_BRUSH_ENTS: # These are shared by both ents, but new_brush won't be added to # the map. (We need it though for the widening code to work). FIZZ_BRUSH_ENTS[conf_key].solids.extend(new_brush.solids) else: vbsp.VMF.add_ent(new_brush) # Don't allow restyling it vbsp.IGNORED_BRUSH_ENTS.add(new_brush) new_brush.clear_keys() # Wipe the original keyvalues new_brush['origin'] = orig_brush['origin'] new_brush['targetname'] = conditions.local_name( base_inst, config['name'], ) # All ents must have a classname! new_brush['classname'] = 'trigger_portal_cleanser' conditions.set_ent_keys( new_brush, base_inst, config, ) for out_prop in config.find_children('Outputs'): out = Output.parse(out_prop) out.comma_sep = False out.target = conditions.local_name( base_inst, out.target ) new_brush.add_out(out) if should_merge: # The first brush... FIZZ_BRUSH_ENTS[conf_key] = new_brush mat_mod_conf = config.find_key('MaterialModify', []) if mat_mod_conf: try: used_materials = modify_controls[id(mat_mod_conf)][2] except KeyError: used_materials = set() modify_controls[id(mat_mod_conf)] = ( new_brush['targetname'], mat_mod_conf, used_materials ) # It can only parent to one brush, so it can't attach # to them all properly. if not should_merge: raise Exception( "MaterialModify won't work without MergeBrushes!" ) else: used_materials = None laserfield_conf = config.find_key('MakeLaserField', None) if laserfield_conf.value is not None: # Resize the brush into a laserfield format, without # the 128*64 parts. If the brush is 128x128, we can # skip the resizing since it's already correct. laser_tex = laserfield_conf['texture', const.Special.LASERFIELD] nodraw_tex = laserfield_conf['nodraw', const.Tools.NODRAW] tex_width = laserfield_conf.int('texwidth', 512) is_short = False for side in new_brush.sides(): if side == const.Fizzler.SHORT: is_short = True break if is_short: for side in new_brush.sides(): if side == const.Fizzler.SHORT: side.mat = laser_tex side.uaxis.offset = 0 side.scale = 0.25 else: side.mat = nodraw_tex else: # The hard part - stretching the brush. convert_to_laserfield( new_brush, laser_tex, nodraw_tex, tex_width, ) if used_materials is not None: used_materials.add(laser_tex.casefold()) else: # Just change the textures for side in new_brush.sides(): try: tex_cat = TEX_FIZZLER[side.mat.casefold()] side.mat = config[tex_cat] except (KeyError, IndexError): # If we fail, just use the original textures pass else: if used_materials is not None and tex_cat != 'nodraw': used_materials.add(side.mat.casefold()) widen_amount = config.float('thickness', 2.0) if widen_amount != 2: for brush in new_brush.solids: conditions.widen_fizz_brush( brush, thickness=widen_amount, ) for brush_name, config, textures in modify_controls.values(): skip_if_static = config.bool('dynamicOnly', True) if skip_if_static and base_inst.fixup['$connectioncount'] == '0': continue mat_mod_name = config['name', 'modify'] var = config['var', '$outputintensity'] if not var.startswith('$'): var = '$' + var for tex in textures: vbsp.VMF.create_ent( classname='material_modify_control', origin=base_inst['origin'], targetname=conditions.local_name(base_inst, mat_mod_name), materialName='materials/' + tex + '.vmt', materialVar=var, parentname=brush_name, )
def res_conveyor_belt(inst: Entity, res: Property): """Create a conveyor belt. Options: SegmentInst: Generated at each square. ('track' is the name of the path.) TrackTeleport: Set the track points so they teleport trains to the start. Speed: The fixup or number for the train speed. MotionTrig: If set, a trigger_multiple will be spawned that EnableMotions weighted cubes. The value is the name of the relevant filter. EndOutput: Adds an output to the last track. The value is the same as outputs in VMFs. RotateSegments: If true (default), force segments to face in the direction of movement RailTemplate: A template for the railings. This is made into a non-solid func_brush, combining all sections. """ move_dist = srctools.conv_int(inst.fixup['$travel_distance']) if move_dist <= 2: # There isn't room for a catwalk, so don't bother. inst.remove() return move_dir = Vec(1, 0, 0).rotate_by_str(inst.fixup['$travel_direction']) move_dir.rotate_by_str(inst['angles']) start_offset = srctools.conv_float(inst.fixup['$starting_position'], 0) teleport_to_start = res.bool('TrackTeleport', True) segment_inst_file = res['SegmentInst', ''] rail_template = res['RailTemplate', None] vmf = inst.map if segment_inst_file: segment_inst_file = conditions.resolve_inst(segment_inst_file)[0] track_speed = res['speed', None] start_pos = Vec.from_str(inst['origin']) end_pos = start_pos + move_dist * move_dir if start_offset > 0: # If an oscillating platform, move to the closest side.. offset = start_offset * move_dir # The instance is placed this far along, so move back to the end. start_pos -= offset end_pos -= offset if start_offset > 0.5: # Swap the direction of movement.. start_pos, end_pos = end_pos, start_pos inst['origin'] = start_pos # Find the angle which generates an instance pointing in the direction # of movement, with the same normal. norm = Vec(z=1).rotate_by_str(inst['angles']) for roll in range(0, 360, 90): angles = move_dir.to_angle(roll) if Vec(z=1).rotate(*angles) == norm: break else: raise ValueError( "Can't find angles to give a" ' z={} and x={}!'.format(norm, move_dir) ) if res.bool('rotateSegments', True): inst['angles'] = angles else: angles = Vec.from_str(inst['angles']) # Add the EnableMotion trigger_multiple seen in platform items. # This wakes up cubes when it starts moving. motion_filter = res['motionTrig', None] # Disable on walls, or if the conveyor can't be turned on. if norm != (0, 0, 1) or inst.fixup['$connectioncount'] == '0': motion_filter = None track_name = conditions.local_name(inst, 'segment_{}') rail_temp_solids = [] last_track = None # Place beams at the top, so they don't appear inside wall sections. beam_start = start_pos + 48 * norm # type: Vec beam_end = end_pos + 48 * norm # type: Vec for index, pos in enumerate(beam_start.iter_line(beam_end, stride=128), start=1): track = vmf.create_ent( classname='path_track', targetname=track_name.format(index) + '-track', origin=pos, spawnflags=0, orientationtype=0, # Don't rotate ) if track_speed is not None: track['speed'] = track_speed if last_track: last_track['target'] = track['targetname'] if index == 1 and teleport_to_start: track['spawnflags'] = 16 # Teleport here.. last_track = track # Don't place at the last point - it doesn't teleport correctly, # and would be one too many. if segment_inst_file and pos != end_pos: seg_inst = vmf.create_ent( classname='func_instance', targetname=track_name.format(index), file=segment_inst_file, origin=pos, angles=angles, ) seg_inst.fixup.update(inst.fixup) if rail_template: temp = conditions.import_template( rail_template, pos, angles, force_type=conditions.TEMP_TYPES.world, add_to_map=False, ) rail_temp_solids.extend(temp.world) if rail_temp_solids: vmf.create_ent( classname='func_brush', origin=beam_start, spawnflags=1, # Ignore +USE solidity=1, # Not solid vrad_brush_cast_shadows=1, drawinfastreflection=1, ).solids = rail_temp_solids if teleport_to_start: # Link back to the first track.. last_track['target'] = track_name.format(1) + '-track' # Generate an env_beam pointing from the start to the end of the track. beam_keys = res.find_key('BeamKeys', []) if beam_keys.value: beam = vmf.create_ent(classname='env_beam') # 3 offsets - x = distance from walls, y = side, z = height beam_off = beam_keys.vec('origin', 0, 63, 56) for prop in beam_keys: beam[prop.real_name] = prop.value # Localise the targetname so it can be triggered.. beam['LightningStart'] = beam['targetname'] = conditions.local_name( inst, beam['targetname', 'beam'] ) del beam['LightningEnd'] beam['origin'] = start_pos + Vec( -beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) beam['TargetPoint'] = end_pos + Vec( +beam_off.x, beam_off.y, beam_off.z, ).rotate(*angles) # Allow adding outputs to the last path_track. for prop in res.find_all('EndOutput'): output = Output.parse(prop) output.output = 'OnPass' output.inst_out = None output.comma_sep = False output.target = conditions.local_name(inst, output.target) last_track.add_out(output) if motion_filter is not None: motion_trig = vmf.create_ent( classname='trigger_multiple', targetname=conditions.local_name(inst, 'enable_motion_trig'), origin=start_pos, filtername=motion_filter, startDisabled=1, wait=0.1, ) motion_trig.add_out(Output('OnStartTouch', '!activator', 'ExitDisabledState')) # Match the size of the original... motion_trig.solids.append(vmf.make_prism( start_pos + Vec(72, -56, 58).rotate(*angles), end_pos + Vec(-72, 56, 144).rotate(*angles), mat='tools/toolstrigger', ).solid) if res.bool('NoPortalFloor'): # Block portals on the floor.. floor_noportal = vmf.create_ent( classname='func_noportal_volume', origin=beam_start, ) floor_noportal.solids.append(vmf.make_prism( start_pos + Vec(-60, -60, -66).rotate(*angles), end_pos + Vec(60, 60, -60).rotate(*angles), mat='tools/toolsinvisible', ).solid) # A brush covering under the platform. base_trig = vmf.make_prism( start_pos + Vec(-64, -64, 48).rotate(*angles), end_pos + Vec(64, 64, 56).rotate(*angles), mat='tools/toolsinvisible', ).solid vmf.add_brush(base_trig) # Make a paint_cleanser under the belt.. if res.bool('PaintFizzler'): pfizz = vmf.create_ent( classname='trigger_paint_cleanser', origin=start_pos, ) pfizz.solids.append(base_trig.copy()) for face in pfizz.sides(): face.mat = 'tools/toolstrigger'
def 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 improve_item(item: Property) -> None: """Improve editoritems formats in various ways. This operates inplace. """ # OccupiedVoxels does not allow specifying 'volume' regions like # EmbeddedVoxel. Implement that. # First for 32^2 cube sections. for voxel_part in item.find_all("Exporting", "OccupiedVoxels", "SurfaceVolume"): if 'subpos1' not in voxel_part or 'subpos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs SubPos1 and SubPos2)!', item['type'], ) continue voxel_part.name = "Voxel" pos_1 = None voxel_subprops = list(voxel_part) voxel_part.clear() for prop in voxel_subprops: if prop.name not in ('subpos', 'subpos1', 'subpos2'): voxel_part.append(prop) continue pos_2 = Vec.from_str(prop.value) if pos_1 is None: pos_1 = pos_2 continue bbox_min, bbox_max = Vec.bbox(pos_1, pos_2) pos_1 = None for pos in Vec.iter_grid(bbox_min, bbox_max): voxel_part.append( Property("Surface", [ Property("Pos", str(pos)), ])) if pos_1 is not None: LOGGER.warning( 'Item {} has only half of SubPos bbox!', item['type'], ) # Full blocks for occu_voxels in item.find_all("Exporting", "OccupiedVoxels"): for voxel_part in list(occu_voxels.find_all("Volume")): del occu_voxels['Volume'] if 'pos1' not in voxel_part or 'pos2' not in voxel_part: LOGGER.warning( 'Item {} has invalid OccupiedVoxels part ' '(needs Pos1 and Pos2)!', item['type']) continue voxel_part.name = "Voxel" bbox_min, bbox_max = Vec.bbox( voxel_part.vec('pos1'), voxel_part.vec('pos2'), ) del voxel_part['pos1'] del voxel_part['pos2'] for pos in Vec.iter_grid(bbox_min, bbox_max): new_part = voxel_part.copy() new_part['Pos'] = str(pos) occu_voxels.append(new_part)
def load_config(conf: Property): """Setup all the generators from the config data.""" global SPECIAL, OVERLAYS global_options = { prop.name: prop.value for prop in conf.find_children('Options') } # Give generators access to the global settings. Generator.global_settings.update( parse_options( # Pass it to both, the second will fail too. global_options, global_options, )) data: Dict[Any, Tuple[Dict[str, Any], Dict[str, List[str]]]] = {} gen_cat: GenCat gen_orient: Optional[Orient] gen_portal: Optional[Portalable] # Use this to allow alternate names for generators. conf_for_gen: Dict[Tuple[GenCat, Optional[Orient], Optional[Portalable]], Property, ] = {} for prop in conf: if prop.name in ('options', 'antlines'): continue if '.' in prop.name: try: gen_cat_name, gen_portal_raw, gen_orient_raw = prop.name.split( '.') gen_cat = GEN_CATS[gen_cat_name] gen_orient = ORIENTS[gen_orient_raw] gen_portal = Portalable(gen_portal_raw) except (KeyError, ValueError): LOGGER.warning('Could not parse texture generator type "{}"!', prop.name) continue conf_for_gen[gen_cat, gen_orient, gen_portal] = prop else: try: gen_cat = GEN_CATS[prop.name] except KeyError: LOGGER.warning('Unknown texture generator type "{}"!', prop.name) continue conf_for_gen[gen_cat, None, None] = prop for gen_key, tex_defaults in TEX_DEFAULTS.items(): if isinstance(gen_key, GenCat): # It's a non-tile generator. is_tile = False gen_cat = gen_key try: gen_conf = conf_for_gen[gen_key, None, None] except KeyError: gen_conf = Property(gen_key.value, []) else: # Tile-type generator is_tile = True try: gen_conf = conf_for_gen[gen_key] except KeyError: gen_conf = Property('', []) if not gen_conf.has_children(): # Special case - using a single value to indicate that all # textures are the same. gen_conf = Property( gen_conf.real_name, [ Property('4x4', gen_conf.value), Property( 'Options', [ # Clumping isn't useful since it's all the same. Property('Algorithm', 'RAND'), ]) ]) textures = {} # First parse the options. options = parse_options( { prop.name: prop.value for prop in gen_conf.find_children('Options') }, global_options) # Now do textures. if is_tile: # Tile generator, always have all tile sizes, and # only use the defaults if no textures were specified. for tex_name in TileSize: textures[tex_name] = [ prop.value for prop in gen_conf.find_all(str(tex_name)) ] # In case someone switches them around, add on 2x1 to 1x2 textures. textures[TileSize.TILE_2x1] += [ prop.value for prop in gen_conf.find_all('1x2') ] if not any(textures.values()): for tex_name, tex_default in tex_defaults.items(): textures[tex_name] = [tex_default] else: # Non-tile generator, use defaults for each value for tex_name, tex_default in tex_defaults.items(): textures[tex_name] = tex = [ prop.value for prop in gen_conf.find_all(str(tex_name)) ] if not tex and tex_default: tex.append(tex_default) data[gen_key] = options, textures # Next, do a check to see if any texture names were specified that # we don't recognise. extra_keys = {prop.name for prop in gen_conf} extra_keys.discard('options') # Not a texture name, but valid. if isinstance(gen_key, GenCat): extra_keys.difference_update(map(str.casefold, tex_defaults.keys())) else: # The defaults are just the size values. extra_keys.difference_update(map(str, TileSize)) if extra_keys: LOGGER.warning('{}: Unknown texture names {}', format_gen_key(gen_key), ', '.join(sorted(extra_keys))) # Now complete textures for tile types, # copying over data from other generators. for gen_key, tex_defaults in TEX_DEFAULTS.items(): if isinstance(gen_key, GenCat): continue gen_cat, gen_orient, gen_portal = gen_key options, textures = data[gen_key] if not any(textures.values()) and gen_cat is not GenCat.NORMAL: # For the additional categories of tiles, we copy the entire # NORMAL one over if it's not set. textures.update(data[GenCat.NORMAL, gen_orient, gen_portal][1]) if not textures[TileSize.TILE_4x4]: raise ValueError('No 4x4 tile set for "{}"!'.format(gen_key)) # Copy 4x4, 2x2, 2x1 textures to the 1x1 size if the option was set. # Do it before inheriting tiles, so there won't be duplicates. if options['mixtiles']: block_tex = textures[TileSize.TILE_1x1] block_tex += textures[TileSize.TILE_4x4] block_tex += textures[TileSize.TILE_2x2] block_tex += textures[TileSize.TILE_2x1] # We need to do more processing. for orig, targ in TILE_INHERIT: if not textures[targ]: textures[targ] = textures[orig].copy() # Now finally create the generators. for gen_key, tex_defaults in TEX_DEFAULTS.items(): options, textures = data[gen_key] if isinstance(gen_key, tuple): # Check the algorithm to use. algo = options['algorithm'] gen_cat, gen_orient, gen_portal = gen_key try: generator: Type[Generator] = GEN_CLASSES[algo] # type: ignore except KeyError: raise ValueError('Invalid algorithm "{}" for {}!'.format( algo, gen_key)) else: # Signage, Overlays always use the Random generator. generator = GenRandom gen_cat = gen_key gen_orient = gen_portal = None GENERATORS[gen_key] = gen = generator(gen_cat, gen_orient, gen_portal, options, textures) # Allow it to use the default enums as direct lookups. if isinstance(gen, GenRandom): if gen_portal is None: gen.set_enum(tex_defaults.items()) else: # Tiles always use TileSize. gen.set_enum((size.value, size) for size in TileSize) SPECIAL = GENERATORS[GenCat.SPECIAL] OVERLAYS = GENERATORS[GenCat.OVERLAYS]
def res_fix_rotation_axis(vmf: VMF, ent: Entity, res: Property): """Properly setup rotating brush entities to match the instance. This uses the orientation of the instance to determine the correct spawnflags to make it rotate in the correct direction. This can either modify an existing entity (which may be in an instance), or generate a new one. The generated brush will be 2x2x2 units large, and always set to be non-solid. For both modes: - `Axis`: specifies the rotation axis local to the instance. - `Reversed`: If set, flips the direction around. - `Classname`: Specifies which entity, since the spawnflags required varies. For application to an existing entity: - `ModifyTarget`: The local name of the entity to modify. For brush generation mode: - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Inst_targ`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * [`func_door_rotating`](https://developer.valvesoftware.com/wiki/func_door_rotating) * [`func_platrot`](https://developer.valvesoftware.com/wiki/func_platrot) * [`func_rot_button`](https://developer.valvesoftware.com/wiki/func_rot_button) * [`func_rotating`](https://developer.valvesoftware.com/wiki/func_rotating) * [`momentary_rot_button`](https://developer.valvesoftware.com/wiki/momentary_rot_button) """ des_axis = res['axis', 'z'].casefold() reverse = res.bool('reversed') door_type = res['classname', 'func_door_rotating'] orient = Matrix.from_angle(Angle.from_str(ent['angles'])) axis = round(Vec.with_axes(des_axis, 1) @ orient, 6) if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse axis = abs(axis) try: flag_values = FLAG_ROTATING[door_type] except KeyError: LOGGER.warning('Unknown rotating brush type "{}"!', door_type) return name = res['ModifyTarget', ''] door_ent: Entity | None if name: name = conditions.local_name(ent, name) setter_loc = ent['origin'] door_ent = None spawnflags = 0 else: # Generate a brush. name = conditions.local_name(ent, res['name', '']) pos = res.vec('Pos') @ Angle.from_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) setter_loc = str(pos) door_ent = vmf.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) # Extra stuff to apply to the flags (USE, toggle, etc) spawnflags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+') # Make the door always non-solid! )) | flag_values.get('solid_flags', 0) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vmf.make_prism(pos - 1, pos + 1).solid] # Add or remove flags as needed for flag, value in zip( ('x', 'y', 'z', 'rev'), [axis.x > 1e-6, axis.y > 1e-6, axis.z > 1e-6, reverse], ): if flag not in flag_values: continue if door_ent is not None: if value: spawnflags |= flag_values[flag] else: spawnflags &= ~flag_values[flag] else: # Place a KV setter to set this. vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='flags', kv_name=flag_values[flag], kv_value_global=value, ) if door_ent is not None: door_ent['spawnflags'] = spawnflags # This ent uses a keyvalue for reversing... if door_type == 'momentary_rot_button': vmf.create_ent( 'comp_kv_setter', origin=setter_loc, target=name, mode='kv', kv_name='StartDirection', kv_value_global='1' if reverse else '-1', )
def 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 build_instance_data(editoritems: Property): """Build a property tree listing all of the instances for each item. as well as another listing the input and output commands. VBSP uses this to reduce duplication in VBSP_config files. This additionally strips custom instance definitions from the original list. """ instance_locs = Property("AllInstances", []) cust_inst = Property("CustInstances", []) commands = Property("Connections", []) item_classes = Property("ItemClasses", []) root_block = Property(None, [ instance_locs, item_classes, cust_inst, commands, ]) for item in editoritems.find_all("Item"): instance_block = Property(item['Type'], []) instance_locs.append(instance_block) comm_block = Property(item['Type'], []) for inst_block in item.find_all("Exporting", "instances"): for inst in inst_block.value[:]: # type: Property if inst.name.isdigit(): # Direct Portal 2 value instance_block.append( Property('Instance', inst['Name']) ) else: # It's a custom definition, remove from editoritems inst_block.value.remove(inst) # Allow the name to start with 'bee2_' also to match # the <> definitions - it's ignored though. name = inst.name if name[:5] == 'bee2_': name = name[5:] cust_inst.set_key( (item['type'], name), # Allow using either the normal block format, # or just providing the file - we don't use the # other values. inst['name'] if inst.has_children() else inst.value, ) # Look in the Inputs and Outputs blocks to find the io definitions. # Copy them to property names like 'Input_Activate'. for io_type in ('Inputs', 'Outputs'): for block in item.find_all('Exporting', io_type, CONN_NORM): for io_prop in block: comm_block[ io_type[:-1] + '_' + io_prop.real_name ] = io_prop.value # The funnel item type is special, having the additional input type. # Handle that specially. if item['type'].casefold() == 'item_tbeam': for block in item.find_all('Exporting', 'Inputs', CONN_FUNNEL): for io_prop in block: comm_block['TBeam_' + io_prop.real_name] = io_prop.value # Fizzlers don't work correctly with outputs. This is a signal to # conditions.fizzler, but it must be removed in editoritems. if item['ItemClass', ''].casefold() == 'itembarrierhazard': for block in item.find_all('Exporting', 'Outputs'): if CONN_NORM in block: del block[CONN_NORM] # Record the itemClass for each item type. item_classes[item['type']] = item['ItemClass', 'ItemBase'] # Only add the block if the item actually has IO. if comm_block.value: commands.append(comm_block) return root_block.export()
def res_fix_rotation_axis(ent: Entity, res: Property): """Generate a `func_rotating`, `func_door_rotating` or any similar entity. This uses the orientation of the instance to detemine the correct spawnflags to make it rotate in the correct direction. The brush will be 2x2x2 units large, and always set to be non-solid. - `Pos` and `name` are local to the instance, and will set the `origin` and `targetname` respectively. - `Keys` are any other keyvalues to be be set. - `Flags` sets additional spawnflags. Multiple values may be separated by `+`, and will be added together. - `Classname` specifies which entity will be created, as well as which other values will be set to specify the correct orientation. - `AddOut` is used to add outputs to the generated entity. It takes the options `Output`, `Target`, `Input`, `Param` and `Delay`. If `Inst_targ` is defined, it will be used with the input to construct an instance proxy input. If `OnceOnly` is set, the output will be deleted when fired. Permitted entities: * `func_rotating` * `func_door_rotating` * `func_rot_button` * `func_platrot` """ des_axis = res['axis', 'z'].casefold() reverse = srctools.conv_bool(res['reversed', '0']) door_type = res['classname', 'func_door_rotating'] # Extra stuff to apply to the flags (USE, toggle, etc) flags = sum( map( # Add together multiple values srctools.conv_int, res['flags', '0'].split('+'))) name = conditions.local_name(ent, res['name', '']) axis = Vec(**{des_axis: 1}).rotate_by_str(ent['angles', '0 0 0']) pos = Vec.from_str(res['Pos', '0 0 0']).rotate_by_str(ent['angles', '0 0 0']) pos += Vec.from_str(ent['origin', '0 0 0']) door_ent = vbsp.VMF.create_ent( classname=door_type, targetname=name, origin=pos.join(' '), ) conditions.set_ent_keys(door_ent, ent, res) for output in res.find_all('AddOut'): door_ent.add_out( Output( out=output['Output', 'OnUse'], inp=output['Input', 'Use'], targ=output['Target', ''], inst_in=output['Inst_targ', None], param=output['Param', ''], delay=srctools.conv_float(output['Delay', '']), times=(1 if srctools.conv_bool(output['OnceOnly', False]) else -1), )) # Generate brush door_ent.solids = [vbsp.VMF.make_prism(pos - 1, pos + 1).solid] if axis.x > 0 or axis.y > 0 or axis.z > 0: # If it points forward, we need to reverse the rotating door reverse = not reverse flag_values = FLAG_ROTATING[door_type] # Make the door always non-solid! flags |= flag_values.get('solid_flags', 0) # Add or remove flags as needed. # flags |= bit sets it to 1. # flags |= ~bit sets it to 0. if axis.x != 0: flags |= flag_values.get('x', 0) else: flags &= ~flag_values.get('x', 0) if axis.y != 0: flags |= flag_values.get('y', 0) else: flags &= ~flag_values.get('y', 0) if axis.z != 0: flags |= flag_values.get('z', 0) else: flags &= ~flag_values.get('z', 0) if door_type == 'momentary_rot_button': door_ent['startdirection'] = '1' if reverse else '-1' else: if reverse: flags |= flag_values.get('rev', 0) else: flags &= ~flag_values.get('rev', 0) door_ent['spawnflags'] = str(flags)
def res_linked_cube_dropper(drp_inst: Entity, res: Property): """Link a cube and dropper together, to preplace the cube at a location.""" time = drp_inst.fixup.int('$timer_delay') # Portal 2 bug - when loading existing maps, timers are set to 3... if not (3 < time <= 30): # Infinite or 3-second - this behaviour is disabled.. return try: cube_inst, cube_type, resp_out_name, resp_out = LINKED_CUBES[time] except KeyError: raise Exception('Unknown cube "linkage" value ({}) in dropper!'.format( time, )) # Force the dropper to match the cube.. # = cube_type # Set auto-drop to False (so there isn't two cubes), # and auto-respawn to True (so it actually functions). drp_inst.fixup['$disable_autodrop'] = '1' drp_inst.fixup['$disable_autorespawn'] = '0' fizz_out_name, fizz_out = Output.parse_name(res['FizzleOut']) # Output to destroy the cube when the dropper is triggered externally. drp_inst.add_out(Output( inst_out=fizz_out_name, out=fizz_out, targ=local_name(cube_inst, 'cube'), inp='Dissolve', only_once=True, )) # Cube items don't have proxies, so we need to use AddOutput # after it's created (@relay_spawn_3's time). try: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] except KeyError: relay_spawn_3 = GLOBAL_INPUT_ENTS['@relay_spawn_3'] = cube_inst.map.create_ent( classname='logic_relay', targetname='@relay_spawn_3', origin=cube_inst['origin'], ) respawn_inp = list(res.find_all('RespawnIn')) # There's some voice-logic specific to companion cubes. respawn_inp.extend(res.find_all( 'RespawnCcube' if drp_inst.fixup['$cube_type'] == '1' else 'RespawnCube' )) for inp in respawn_inp: resp_in_name, resp_in = inp.value.split(':', 1) out = Output( out='OnFizzled', targ=drp_inst, inst_in=resp_in_name, inp=resp_in, only_once=True, ) relay_spawn_3.add_out(Output( out='OnTrigger', targ=local_name(cube_inst, 'cube'), inp='AddOutput', param=out.gen_addoutput(), only_once=True, delay=0.01, ))