def strip_quote_data(prop: Property, _depth=0) -> Property: """Strip unused property blocks from the config files. This removes data like the captions which the compiler doesn't need. The returned property tree is a deep-copy of the original. """ children = [] for sub_prop in prop: # Make sure it's in the right nesting depth - flags might # have arbitrary props in lower depths.. if _depth == 3: # 'Line' blocks if sub_prop.name == 'trans': continue elif sub_prop.name == 'name' and 'id' in prop: continue # The name isn't needed if an ID is available elif _depth == 2 and sub_prop.name == 'name': # In the "quote" section, the name isn't used in the compiler. continue if sub_prop.has_children(): children.append( QuotePack.strip_quote_data(sub_prop, _depth + 1)) else: children.append(Property(sub_prop.real_name, sub_prop.value)) return Property(prop.real_name, children)
def add_meta(func, priority: Decimal | int, only_once=True): """Add a metacondition, which executes a function at a priority level. Used to allow users to allow adding conditions before or after a transformation like the adding of quotes. """ dec_priority = Decimal(priority) # This adds a condition result like "func" (with quotes), which cannot # be entered into property files. # The qualified name will be unique across modules. name = '"' + func.__qualname__ + '"' LOGGER.debug( "Adding metacondition ({}) with priority {!s}!", name, dec_priority, ) # We don't care about setup functions for this. RESULT_LOOKUP[name] = wrapper = CondCall(func, _get_cond_group(func)) cond = Condition( results=[Property(name, '')], priority=dec_priority, source='MetaCondition {}'.format(name) ) if only_once: cond.results.append( Property('endCondition', '') ) conditions.append(cond) ALL_META.append((name, dec_priority, wrapper))
def add_meta(func, priority, only_once=True): """Add a metacondtion, which executes a function at a priority level. Used to allow users to allow adding conditions before or after a transformation like the adding of quotes. """ # This adds a condition result like "func" (with quotes), which cannot # be entered into property files. # The qualname will be unique across modules. name = '"' + func.__qualname__ + '"' LOGGER.debug( "Adding metacondition ({}) with priority {!s}!", name, priority, ) RESULT_LOOKUP[name] = annotation_caller(func, srctools.VMF, Entity, Property) cond = Condition(results=[Property(name, '')], priority=priority, source='MetaCondition {}'.format(name)) if only_once: cond.results.append(Property('endCondition', '')) conditions.append(cond) ALL_META.append((name, priority, func))
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_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 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 export(exp_data: ExportData) -> None: """Export style var selections into the config. The .selected attribute is a dict mapping ids to the boolean value. """ # Add the StyleVars block, containing each style_var. exp_data.vbsp_conf.append( Property('StyleVars', [ Property(key, bool_as_int(val)) for key, val in exp_data.selected.items() ]))
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 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 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 __init__( self, item_id: str, versions: Dict[str, Version], def_version: Version, needs_unlock: bool = False, all_conf: Optional[Property] = None, unstyled: bool = False, isolate_versions: bool = False, glob_desc: tkMarkdown.MarkdownData = (), desc_last: bool = False, folders: Dict[Tuple[FileSystem, str], ItemVariant] = EmptyMapping, ) -> None: self.id = item_id self.versions = versions self.def_ver = def_version self.needs_unlock = needs_unlock self.all_conf = all_conf or Property(None, []) # If set or set on a version, don't look at the first version # for unstyled items. self.isolate_versions = isolate_versions self.unstyled = unstyled self.glob_desc = glob_desc self.glob_desc_last = desc_last # Dict of folders we need to have decoded. self.folders = folders
def __init__( self, music_id, selitem_data: 'SelitemData', sound: Dict[MusicChannel, List[str]], children: Dict[MusicChannel, str], config: Property = None, inst=None, sample: Dict[MusicChannel, Optional[str]] = None, pack=(), loop_len=0, synch_tbeam=False, ): self.id = music_id self.config = config or Property(None, []) self.children = children set_cond_source(config, 'Music <{}>'.format(music_id)) self.inst = inst self.sound = sound self.packfiles = list(pack) self.len = loop_len self.sample = sample self.selitem_data = selitem_data self.has_synced_tbeam = synch_tbeam
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 __init__( self, name: str, sounds: List[str], volume: Union[Tuple[Union[float, VOLUME], Union[float, VOLUME]], float, VOLUME] = VOL_NORM, channel: Channel = Channel.DEFAULT, level: Union[Tuple[Union[float, Level], Union[float, Level]], float, Level] = Level.SNDLVL_NORM, pitch: Union[Tuple[Union[float, Pitch], Union[float, Pitch]], float, Pitch] = Pitch.PITCH_NORM, # Operator stacks stack_start: Optional[Property] = None, stack_update: Optional[Property] = None, stack_stop: Optional[Property] = None, use_v2: bool = False, ) -> None: """Create a soundscript.""" self.name = name self.sounds = sounds self.channel = channel self.force_v2 = use_v2 if isinstance(volume, tuple): self.volume = volume else: self.volume = volume, volume if isinstance(level, tuple): self.level = level else: self.level = level, level if isinstance(pitch, tuple): self.pitch = pitch else: self.pitch = pitch, pitch self.stack_start = Property( '', []) if stack_start is None else stack_start # type: Property self.stack_update = Property( '', []) if stack_update is None else stack_update # type: Property self.stack_stop = Property( '', []) if stack_stop is None else stack_stop # type: Property
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 res_add_global_inst(vmf: VMF, res: Property): """Add one instance in a specific location. Options: - `allow_multiple`: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. - `name`: The targetname of the instance. If blank, the instance will be given a name of the form `inst_1234`. - `file`: The filename for the instance. - `angles`: The orientation of the instance (defaults to `0 0 0`). - `fixup_style`: The Fixup style for the instance. `0` (default) is Prefix, `1` is Suffix, and `2` is None. - `position`: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) file = instanceLocs.resolve_one(res['file'], error=True) if res.bool('allow_multiple') or file.casefold() not in conditions.GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vmf.create_ent( classname="func_instance", targetname=res['name', ''], file=file, angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = options.get(Vec, 'global_ents_loc') conditions.GLOBAL_INSTANCES.add(file.casefold()) conditions.ALL_INST.add(file.casefold()) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return conditions.RES_EXHAUSTED
def func(vmf: srctools.VMF, prop: Property): """Replacement function which performs the legacy behaviour.""" # The old system for setup functions - smuggle them in by # setting Property.value to an arbitrary object. smuggle = Property(prop.real_name, setup_wrap(vmf, prop)) def closure(ent: Entity) -> object: """Use the closure to store the smuggled setup data.""" return result_wrap(vmf, ent, smuggle) return closure
def export( self ) -> tuple[list[EditorItem], dict[RenderableType, Renderable], Property]: """Export this style, returning the vbsp_config and editoritems. This is a special case, since styles should go first in the lists. """ vbsp_config = Property(None, []) vbsp_config += self.config() return self.items, self.renderables, vbsp_config
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 _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 get(self, expected_type: type, name: str) -> Any: """Get the given option. expected_type should be the class of the value that's expected. The value can be None if unset, except for Property types (which will always have an empty block). If expected_type is an Enum, this will be used to convert the output. If it fails, a warning is produced and the first value in the enum is returned. """ try: val = self.settings[name.casefold()] except KeyError: raise TypeError( 'Option "{}" does not exist!'.format(name)) from None if val is None: if expected_type is Property: return Property(name, []) else: return None if issubclass(expected_type, Enum): enum_type = expected_type # type: Optional[Type[Enum]] expected_type = str else: enum_type = None # Don't allow subclasses (bool/int) if type(val) is not expected_type: raise ValueError('Option "{}" is {} (code expected {})'.format( name, type(val), expected_type, )) if enum_type is not None: try: return enum_type(val) # type: ignore except ValueError: LOGGER.warning( 'Option "{}" is not a valid value. ' 'Allowed values are:\n{}', name, '\n'.join([mem.value for mem in enum_type])) return next(iter(enum_type)) # type: ignore # Vec is mutable, don't allow modifying the original. if expected_type is Vec or expected_type is Property: assert isinstance(val, Vec) or isinstance(val, Property) return val.copy() else: assert isinstance(val, expected_type) return val
def res_add_global_inst(res: Property): """Add one instance in a specific location. Options: allow_multiple: Allow multiple copies of this instance. If 0, the instance will not be added if it was already added. name: The targetname of the instance. IF blank, the instance will be given a name of the form 'inst_1234'. file: The filename for the instance. Angles: The orientation of the instance (defaults to '0 0 0'). Fixup_style: The Fixup style for the instance. '0' (default) is Prefix, '1' is Suffix, and '2' is None. Position: The location of the instance. If not set, it will be placed in a 128x128 nodraw room somewhere in the map. Objects which can interact with nearby object should not be placed there. """ if not res.has_children(): res = Property('AddGlobal', [Property('File', res.value)]) if res.bool('allow_multiple') or res['file'] not in GLOBAL_INSTANCES: # By default we will skip adding the instance # if was already added - this is helpful for # items that add to original items, or to avoid # bugs. new_inst = vbsp.VMF.create_ent( classname="func_instance", targetname=res['name', ''], file=instanceLocs.resolve_one(res['file'], error=True), angles=res['angles', '0 0 0'], fixup_style=res['fixup_style', '0'], ) try: new_inst['origin'] = res['position'] except IndexError: new_inst['origin'] = vbsp_options.get(Vec, 'global_ents_loc') GLOBAL_INSTANCES.add(res['file']) if new_inst['targetname'] == '': new_inst['targetname'] = "inst_" new_inst.make_unique() return RES_EXHAUSTED
def __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 save(self, ignore_readonly: bool = False) -> None: """Save the palette file into the specified location. If ignore_readonly is true, this will ignore the `readonly` 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.root( Property('Name', self.name), Property('TransName', self.trans_name), Property('Group', self.group), Property('ReadOnly', srctools.bool_as_int(self.readonly)), Property('UUID', self.uuid.hex), 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 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.readonly 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 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 get_config( prop_block: Property, fsys: FileSystem, folder: str, pak_id='', prop_name='config', extension='.cfg', ): """Extract a config file referred to by the given property block. Looks for the prop_name key in the given prop_block. If the keyvalue has a value of "", an empty tree is returned. If it has children, a copy of them is returned. Otherwise the value is a filename in the zip which will be parsed. """ prop_block = prop_block.find_key(prop_name, "") if prop_block.has_children(): prop = prop_block.copy() prop.name = None return prop if prop_block.value == '': return Property(None, []) # Zips must use '/' for the separator, even on Windows! path = folder + '/' + prop_block.value if len(path) < 3 or path[-4] != '.': # Add extension path += extension try: return fsys.read_prop(path) except FileNotFoundError: LOGGER.warning('"{id}:{path}" not in zip!', id=pak_id, path=path) return Property(None, []) except UnicodeDecodeError: LOGGER.exception('Unable to read "{id}:{path}"', id=pak_id, path=path) raise
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 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)