def save_load_signage(props: Property = None) -> Optional[Property]: """Save or load the signage info.""" if props is None: # Save to properties. props = Property('Signage', []) for timer, slot in SLOTS_SELECTED.items(): props.append( Property( str(timer), '' if slot.contents is None else slot.contents.id, )) return props else: # Load from provided properties. for child in props: try: slot = SLOTS_SELECTED[int(child.name)] except (ValueError, TypeError): LOGGER.warning('Non-numeric timer value "{}"!', child.name) continue except KeyError: LOGGER.warning('Invalid timer value {}!', child.name) continue if child.value: try: slot.contents = Signage.by_id(child.value) except KeyError: LOGGER.warning('No signage with id "{}"!', child.value) else: slot.contents = None return None
def save_handler() -> Property: """Save the compiler pane to the palette. Note: We specifically do not save/load the following: - packfile dumping - compile counts This is because these are more system-dependent than map dependent. """ corr_prop = Property('corridor', []) props = Property('', [ Property('sshot_type', chosen_thumb.get()), Property('sshot_cleanup', str(cleanup_screenshot.get())), Property('spawn_elev', str(start_in_elev.get())), Property('player_model', PLAYER_MODELS_REV[player_model_var.get()]), Property('voiceline_priority', str(VOICE_PRIORITY_VAR.get())), corr_prop, ]) for group, win in CORRIDOR.items(): corr_prop[group] = win.chosen_id or '<NONE>' # Embed the screenshot in so we can load it later. if chosen_thumb.get() == 'CUST': # encodebytes() splits it into multiple lines, which we write # in individual blocks to prevent having a massively long line # in the file. with open(SCREENSHOT_LOC, 'rb') as f: screenshot_data = base64.encodebytes(f.read()) props.append( Property('sshot_data', [ Property('b64', data) for data in screenshot_data.decode('ascii').splitlines() ])) return props
def apply_replacements(conf: Property) -> Property: """Apply a set of replacement values to a config file, returning a new copy. The replacements are found in a 'Replacements' block in the property. These replace %values% starting and ending with percents. A double-percent allows literal percents. Unassigned values are an error. """ replace = {} new_conf = Property(conf.real_name, []) # Strip the replacement blocks from the config, and save the values. for prop in conf: if prop.name == 'replacements': for rep_prop in prop: replace[rep_prop.name.strip('%')] = rep_prop.value else: new_conf.append(prop) def rep_func(match: Match): """Does the replacement.""" var = match.group(1) if not var: # %% becomes %. return '%' try: return replace[var.casefold()] except KeyError: raise ValueError('Unresolved variable: {!r}\n{}'.format( var, replace)) for prop in new_conf.iter_tree(blocks=True): prop.name = RE_PERCENT_VAR.sub(rep_func, prop.real_name) if not prop.has_children(): prop.value = RE_PERCENT_VAR.sub(rep_func, prop.value) return new_conf
def save(self, ignore_readonly=False): """Save the palette file into the specified location. If ignore_readonly is true, this will ignore the `prevent_overwrite` property of the palette (allowing resaving those properties over old versions). Otherwise those palettes always create a new file. """ LOGGER.info('Saving "{}"!', self.name) props = Property(None, [ Property('Name', self.name), Property('TransName', self.trans_name), Property('ReadOnly', srctools.bool_as_int(self.prevent_overwrite)), Property('Items', [ Property(item_id, str(subitem)) for item_id, subitem in self.pos ]) ]) # If default, don't include in the palette file. # Remove the translated name, in case it's not going to write # properly to the file. if self.trans_name: props['Name'] = '' else: del props['TransName'] if not self.prevent_overwrite: del props['ReadOnly'] if self.settings is not None: self.settings.name = 'Settings' props.append(self.settings.copy()) # We need to write a new file, determine a valid path. # Use a hash to ensure it's a valid path (without '-' if negative) # If a conflict occurs, add ' ' and hash again to get a different # value. if self.filename is None or (self.prevent_overwrite and not ignore_readonly): hash_src = self.name while True: hash_filename = str(abs(hash(hash_src))) + PAL_EXT if os.path.isfile(hash_filename): # Add a random character to iterate the hash. hash_src += chr(random.randrange(0x10ffff)) else: file = open(os.path.join(PAL_DIR, hash_filename), 'w', encoding='utf8') self.filename = os.path.join(PAL_DIR, hash_filename) break else: file = open(os.path.join(PAL_DIR, self.filename), 'w', encoding='utf8') with file: for line in props.export(): file.write(line)
def save_signage() -> Property: """Save the signage info to settings or a palette.""" props = Property('Signage', []) for timer, slot in SLOTS_SELECTED.items(): props.append(Property( str(timer), '' if slot.contents is None else slot.contents.id, )) return props
def get_curr_settings() -> Property: """Return a property tree defining the current options.""" props = Property('', []) for opt_id, opt_func in option_handler.items(): opt_prop = opt_func() # type: Property opt_prop.name = opt_id.title() props.append(opt_prop) return props
def export(exp_data: ExportData): """Export the selected music.""" selected = exp_data.selected # type: Dict[MusicChannel, Optional[Music]] base_music = selected[MusicChannel.BASE] vbsp_config = exp_data.vbsp_conf if base_music is not None: vbsp_config += base_music.config.copy() music_conf = Property('MusicScript', []) vbsp_config.append(music_conf) to_pack = set() for channel, music in selected.items(): if music is None: continue sounds = music.sound[channel] if len(sounds) == 1: music_conf.append(Property(channel.value, sounds[0])) else: music_conf.append( Property(channel.value, [Property('snd', snd) for snd in sounds])) to_pack.update(music.packfiles) if base_music is not None: vbsp_config.set_key( ('Options', 'music_looplen'), str(base_music.len), ) vbsp_config.set_key( ('Options', 'music_sync_tbeam'), srctools.bool_as_int(base_music.has_synced_tbeam), ) vbsp_config.set_key( ('Options', 'music_instance'), base_music.inst or '', ) # If we need to pack, add the files to be unconditionally # packed. if to_pack: vbsp_config.set_key( ('PackTriggers', 'Forced'), [Property('File', file) for file in to_pack], )
def export(exp_data: ExportData) -> None: """Export the selected signage to the config, and produce the legend.""" # Timer value -> sign ID. sel_ids: list[tuple[str, str]] = exp_data.selected # Special case, arrow is never selectable. sel_ids.append(('arrow', 'SIGN_ARROW')) sel_icons: dict[int, ImgHandle] = {} conf = Property('Signage', []) for tim_id, sign_id in sel_ids: try: sign = Signage.by_id(sign_id) except KeyError: LOGGER.warning('Signage "{}" does not exist!', sign_id) continue prop_block = Property(str(tim_id), []) sty_sign = sign._serialise(prop_block, exp_data.selected_style) for sub_name, sub_id in [ ('primary', sign.prim_id), ('secondary', sign.sec_id), ]: if sub_id: try: sub_sign = Signage.by_id(sub_id) except KeyError: LOGGER.warning( 'Signage "{}"\'s {} "{}" ' 'does not exist!', sign_id, sub_name, sub_id) else: sub_block = Property(sub_name, []) sub_sign._serialise(sub_block, exp_data.selected_style) if sub_block: prop_block.append(sub_block) if prop_block: conf.append(prop_block) # Valid timer number, store to be placed on the texture. if tim_id.isdigit() and sty_sign is not None: sel_icons[int(tim_id)] = sty_sign.icon exp_data.vbsp_conf.append(conf) exp_data.resources[SIGN_LOC] = build_texture( exp_data.game, exp_data.selected_style, sel_icons, )
def export(exp_data: ExportData) -> None: """Export the selected signage to the config.""" # Timer value -> sign ID. sel_ids: List[Tuple[str, str]] = exp_data.selected # Special case, arrow is never selectable. sel_ids.append(('arrow', 'SIGN_ARROW')) conf = Property('Signage', []) for tim_id, sign_id in sel_ids: try: sign = Signage.by_id(sign_id) except KeyError: LOGGER.warning('Signage "{}" does not exist!', sign_id) continue prop_block = Property(str(tim_id), []) sign._serialise(prop_block, exp_data.selected_style) for sub_name, sub_id in [ ('primary', sign.prim_id), ('secondary', sign.sec_id), ]: if sub_id: try: sub_sign = Signage.by_id(sub_id) except KeyError: LOGGER.warning( 'Signage "{}"\'s {} "{}" ' 'does not exist!', sign_id, sub_name, sub_id) else: sub_block = Property(sub_name, []) sub_sign._serialise(sub_block, exp_data.selected_style) if sub_block: prop_block.append(sub_block) if prop_block: conf.append(prop_block) exp_data.vbsp_conf.append(conf)
def export(exp_data: ExportData) -> None: """Export all the packlists.""" pack_block = Property('PackList', []) for pack in PackList.all(): # type: PackList # Build a # "Pack_id" # { # "File" "filename" # "File" "filename" # } # block for each packlist files = [Property('File', file) for file in pack.files] pack_block.append(Property( pack.id, files, )) LOGGER.info('Writing packing list!') with open(exp_data.game.abs_path('bin/bee2/pack_list.cfg'), 'w') as pack_file: for line in pack_block.export(): pack_file.write(line)
def save_itemvar() -> Property: """Save item variables into the palette.""" prop = Property('', []) for group in CONFIG_ORDER: conf = Property(group.id, []) for widget in group.widgets: if widget.has_values: conf.append(Property(widget.id, widget.values.get())) for widget in group.multi_widgets: conf.append( Property(widget.id, [ Property(str(tim_val), var.get()) for tim_val, var in widget.values ])) prop.append(conf) return prop
def _parse_block(tok: Tokenizer, name: str) -> Property: """Parse a block into a block of properties.""" prop = Property(name, []) for token, param_name in tok: # End of our block if token is Tok.BRACE_CLOSE: return prop elif token is Tok.NEWLINE: continue elif token is not Tok.STRING: raise tok.error(token) token, param_value = tok() if token is Tok.STRING: # We have the value. pass elif token is Tok.NEWLINE: # Name by itself: '%compilenodraw' etc... # We need to check there's a newline after that - for subblocks. token, ignored = tok() while token is Tok.NEWLINE: token, ignored = tok() if token is Tok.BRACE_OPEN: prop.append(Material._parse_block(tok, param_name)) continue elif token is Tok.NEWLINE: pass elif token is Tok.BRACE_CLOSE: # End of us after single name. prop.append(Property(param_name, '')) break else: raise tok.error(token) else: raise tok.error(token) prop.append(Property(param_name, param_value)) raise tok.error('EOF without closed block!')
def _serialise(self, parent: Property, style: Style) -> None: """Write this sign's data for the style to the provided property.""" for potential_style in style.bases: try: data = self.styles[potential_style.id.upper()] break except KeyError: pass else: LOGGER.warning( 'No valid "{}" style for "{}" signage!', style.id, self.id, ) try: data = self.styles['BEE2_CLEAN'] except KeyError: return parent.append(Property('world', data.world)) parent.append(Property('overlay', data.overlay)) parent.append(Property('type', data.type))
def save_load_itemvar(prop: Property = None) -> Optional[Property]: """Save or load item variables into the palette.""" if prop is None: prop = Property('', []) for group in CONFIG_ORDER: conf = Property(group.id, []) for widget in group.widgets: # ItemVariant special case. if widget.values is not None: conf.append(Property(widget.id, widget.values.get())) for widget in group.multi_widgets: conf.append( Property(widget.id, [ Property(str(tim_val), var.get()) for tim_val, var in widget.values ])) prop.append(conf) return prop else: # Loading. for group in CONFIG_ORDER: conf = prop.find_key(group.id, []) for widget in group.widgets: if widget.values is not None: # ItemVariants try: widget.values.set(conf[widget.id]) except LookupError: pass for widget in group.multi_widgets: time_conf = conf.find_key(widget.id, []) for tim_val, var in widget.values: try: var.set(time_conf[str(tim_val)]) except LookupError: pass return None
def save_load_itemvar(prop: Property=None) -> Optional[Property]: """Save or load item variables into the palette.""" if prop is None: prop = Property('', []) for group in CONFIG_ORDER: conf = Property(group.id, []) for widget in group.widgets: # ItemVariant special case. if widget.values is not None: conf.append(Property(widget.id, widget.values.get())) for widget in group.multi_widgets: conf.append(Property(widget.id, [ Property(str(tim_val), var.get()) for tim_val, var in widget.values ])) prop.append(conf) return prop else: # Loading. for group in CONFIG_ORDER: conf = prop.find_key(group.id, []) for widget in group.widgets: if widget.values is not None: # ItemVariants try: widget.values.set(conf[widget.id]) except LookupError: pass for widget in group.multi_widgets: time_conf = conf.find_key(widget.id, []) for tim_val, var in widget.values: try: var.set(time_conf[str(tim_val)]) except LookupError: pass return None
class mat: ''' A source material file. This can be used on models or on brushes.''' def __init__(self,pth,diffuse,mat_type='LightmappedGeneric',normal=None,bump=None,glossy=None,tags=[]): if (not mat_type in ['LightmappedGeneric','VertexLitGeneric','UnlitGeneric']): raise Exception('mat_type should be LightmapperGeneric, VertexLitGeneric, or UnlitGeneric.') self.mat_type = mat_type for a,x in enumerate([diffuse,normal,bump,glossy]): if (not isinstance(x,tex) and not x is None): raise Exception(['diffuse','normal','bump','glossy'][a]+' must be of type tex.') self.path = pth self.vmt = Property(mat_type,[]) self.__mats = [x for x in [diffuse,normal,bump,glossy] if x] ''' do image processing here ''' diffuse = get_path_relative(pth,diffuse.path) self.vmt.append(Property('$basetexture',diffuse)) if normal: normal = get_path_relative(pth,normal.path) self.vmt.append(Property('$normalmap',normal)) if bump: bump = get_path_relative(pth,bump.path) self.vmt.append(Property('$bumpmap',bump)) if glossy: glossy = get_path_relative(pth,glossy.path) self.vmt.append('$envmap','1') self.vmt.append(Property('$envmapmask',glossy)) if len(tags): self.vmt.append(Property('%keywords',",".join(tags))) def compile(self): ''' output the material to a vmt ''' for a,x in enumerate(self.__mats): y = ['diffuse','normal','bump','glossy'][a] if (os.path.isfile(x.__path)): print(f'VTF texture for {y} already exists. Copying to material location.') #copyfile(x.__path, return print('This failed horribly. Yikes.') if not os.path.isfile(x.__img): raise Exception(f'Unable to find texture file for {y}.')
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 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()