def __exit__(self, exc_type, exc_val, exc_tb) -> bool: """Write the constructed models to the cache file and remove unused models.""" if exc_type is not None or exc_val is not None: return False data = [] used_mdls = set() for key, mdl in self._built_models.items(): if mdl.used: data.append((key, mdl.name, mdl.result)) used_mdls.add(mdl.name.casefold()) with AtomicWriter(self.model_folder_abs / 'manifest.bin', is_bytes=True) as f: pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) for mdl_file in self.model_folder_abs.glob('*'): if mdl_file.suffix not in {'.mdl', '.phy', '.vtx', '.vvd'}: continue # Strip all suffixes. if mdl_file.name[:mdl_file.name.find('.')].casefold() in used_mdls: continue LOGGER.info('Culling {}...', mdl_file) try: mdl_file.unlink() except FileNotFoundError: pass
def __init__( self, filename: str, *, in_conf_folder: bool = True, auto_load: bool = True, ) -> None: """Initialise the config file. `filename` is the name of the config file, in the `root` directory. If `auto_load` is true, this file will immediately be read and parsed. If in_conf_folder is set, The folder is relative to the 'config/' folder in the BEE2 folder. """ super().__init__() self.has_changed = False if filename is not None: if in_conf_folder: self.filename = utils.conf_location('config/' + filename) else: self.filename = filename self.writer = AtomicWriter(self.filename) self.has_changed = False if auto_load: self.load() else: self.filename = self.writer = None
def replace_lump(self, new_name: str, lump: Union[BSP_LUMPS, 'Lump'], new_data: bytes): """Write out the BSP file, replacing a lump with the given bytes. """ if isinstance(lump, BSP_LUMPS): lump = self.lumps[lump] # type: Lump with open(self.filename, 'rb') as file: data = file.read() before_lump = data[self.header_off:lump.offset] after_lump = data[lump.offset + lump.length:] del data # This contains the entire file, we don't want to keep # this memory around for long. # Adjust the length to match the new data block. len_change = len(new_data) - lump.length lump.length = len(new_data) # Find all lumps after this one, and adjust offsets. # The order of headers doesn't need to match data order! for other_lump in self.lumps.values(): # Not >=, that would adjust us too! if other_lump.offset > lump.offset: other_lump.offset += len_change with AtomicWriter(new_name, is_bytes=True) as file: self.write_header(file) file.write(before_lump) file.write(new_data) file.write(after_lump) # Game lumps need their data to apply offsets. # We're not adding/removing headers, so we can rewrite in-place. game_lump = self.lumps[BSP_LUMPS.GAME_LUMP] if game_lump.offset > lump.offset: file.seek(game_lump.offset) file.write(struct.pack('i', len(self.game_lumps))) for lump_id, ( flags, version, file_off, file_len, ) in self.game_lumps.items(): self.game_lumps[lump_id] = ( flags, version, file_off + len_change, file_len, ) file.write( struct.pack( '<4s HH ii', lump_id[::-1], flags, version, file_off + len_change, file_len, ))
def write_settings() -> None: """Write the settings to disk.""" props = get_curr_settings() props.name = None with AtomicWriter( str(utils.conf_location('config/config.vdf')), is_bytes=False, ) as file: for line in props.export(): file.write(line)
def replace_lump(self, new_name, lump, new_data: bytes): """Write out the BSP file, replacing a lump with the given bytes. """ if isinstance(lump, BSP_LUMPS): lump = self.lumps[lump] with open(self.filename, 'rb') as file: data = file.read() before_lump = data[self.header_off:lump.offset] after_lump = data[lump.offset + lump.length:] del data # This contains the entire file, we don't want to keep # this memory around for long. # Adjust the length to match the new data block. lump.length = len(new_data) with AtomicWriter(new_name, is_bytes=True) as file: self.write_header(file) file.write(before_lump) file.write(new_data) file.write(after_lump)
def save(self, filename=None) -> None: """Write the BSP back into the given file.""" game_lumps = list(self.game_lumps.values()) # Lock iteration order. with AtomicWriter(filename or self.filename, is_bytes=True) as file: # type: BinaryIO # Needed to allow writing out the header before we know the position # data will be. defer = DeferredWrites(file) if isinstance(self.version, VERSIONS): version = self.version.value else: version = self.version file.write(struct.pack(HEADER_1, BSP_MAGIC, version)) # Write headers. for lump_name in BSP_LUMPS: lump = self.lumps[lump_name] defer.defer(lump_name, '<ii') file.write( struct.pack( HEADER_LUMP, 0, # offset 0, # length lump.version, bytes(lump.ident), )) # After lump headers, the map revision... file.write(struct.pack(HEADER_2, self.map_revision)) # Then each lump. for lump_name in LUMP_WRITE_ORDER: # Write out the actual data. lump = self.lumps[lump_name] if lump_name is BSP_LUMPS.GAME_LUMP: # Construct this right here. lump_start = file.tell() file.write(struct.pack('<i', len(game_lumps))) for game_lump in game_lumps: file.write( struct.pack( '<4s HH', game_lump.id[::-1], game_lump.flags, game_lump.version, )) defer.defer(game_lump.id, '<i', write=True) file.write(struct.pack('<i', len(game_lump.data))) # Now write data. for game_lump in game_lumps: defer.set_data(game_lump.id, file.tell()) file.write(game_lump.data) # Length of the game lump is current - start. defer.set_data( lump_name, lump_start, file.tell() - lump_start, ) else: # Normal lump. defer.set_data(lump_name, file.tell(), len(lump.data)) file.write(lump.data) # Apply all the deferred writes. defer.write()
def parse(path: Path) -> Tuple[ Config, Game, FileSystemChain, Set[FileSystem], Set[Plugin], ]: """From some directory, locate and parse the config file. This then constructs and customises each object according to config options. The first srctools.vdf file found in a parent directory is parsed. If none can be found, it tries to find the first subfolder of 'common/' and writes a default copy there. FileNotFoundError is raised if none can be found. This returns: * The config. * Parsed gameinfo. * The chain of filesystems. * A packing blacklist. * A list of plugins. """ conf = Config(OPTIONS) # If the path is a folder, add a dummy folder so parents yields it. # That way we check for a config in this folder. if not path.suffix: path /= 'unused' for folder in path.parents: conf_path = folder / CONF_NAME if conf_path.exists(): LOGGER.info('Config path: "{}"', conf_path.absolute()) with open(conf_path) as f: props = Property.parse(f, conf_path) conf.path = conf_path conf.load(props) break else: LOGGER.warning('Cannot find a valid config file!') # Apply all the defaults. conf.load(Property(None, [])) # Try to write out a default file in the game folder. for folder in path.parents: if folder.parent.stem == 'common': break else: # Give up, write to working directory. folder = Path() conf.path = folder / CONF_NAME LOGGER.warning('Writing default to "{}"', conf.path) with AtomicWriter(str(conf.path)) as f: conf.save(f) game = Game((folder / conf.get(str, 'gameinfo')).resolve()) fsys_chain = game.get_filesystem() blacklist = set() # type: Set[FileSystem] if not conf.get(bool, 'pack_vpk'): for fsys, prefix in fsys_chain.systems: if isinstance(fsys, VPKFileSystem): blacklist.add(fsys) game_root = game.root for prop in conf.get(Property, 'searchpaths'): # type: Property if prop.has_children(): raise ValueError('Config "searchpaths" value cannot have children.') assert isinstance(prop.value, str) if prop.value.endswith('.vpk'): fsys = VPKFileSystem(str((game_root / prop.value).resolve())) else: fsys = RawFileSystem(str((game_root / prop.value).resolve())) if prop.name in ('prefix', 'priority'): fsys_chain.add_sys(fsys, priority=True) elif prop.name == 'nopack': blacklist.add(fsys) elif prop.name in ('path', 'pack'): fsys_chain.add_sys(fsys) else: raise ValueError( 'Unknown searchpath ' 'key "{}"!'.format(prop.real_name) ) plugins = set() # type: Set[Plugin] # find all the plugins and make plugin objects out of them for prop in conf.get(Property, 'plugins'): # type: Property if prop.has_children(): raise ValueError('Config "plugins" value cannot have children.') assert isinstance(prop.value, str) path = (game_root / Path(prop.value)).resolve() if prop.name in ("path", "recursive"): if not path.is_dir(): raise ValueError("'{}' is not a directory".format(path)) # want to recursive glob if key is recursive pattern = "*.py" if prop.name == "path" else "**/*.py" #find all .py files, make Plugins for p in path.glob(pattern): plugins.add(Plugin(path / p)) elif prop.name == "single": plugins.add(Plugin(path)) else: raise ValueError("Unknown plugins key {}".format(prop.real_name)) return conf, game, fsys_chain, blacklist, plugins
def save(self, filename=None) -> None: """Write the BSP back into the given file.""" # This gets difficult. The offsets need to be written before we know # what they are. So write empty bytes, record that location then go # back to fill them in after we actually determine where they are. # We use either BSP_LUMPS enums or game-lump byte IDs for dict keys. # Location of the header field. fixup_loc = {} # type: Dict[Union[BSP_LUMPS, bytes], int] # The data to write. fixup_data = {} # type: Dict[Union[BSP_LUMPS, bytes], bytes] game_lumps = list(self.game_lumps.values()) # Lock iteration order. with AtomicWriter(filename or self.filename, is_bytes=True) as file: # type: BinaryIO file.write(BSP_MAGIC) if isinstance(self.version, VERSIONS): file.write(struct.pack('<i', self.version.value)) else: file.write(struct.pack('<i', self.version)) # Write headers. for lump_name in BSP_LUMPS: lump = self.lumps[lump_name] fixup_loc[lump_name] = file.tell() file.write( struct.pack( '<8xi4s', # offset, # length, lump.version, bytes(lump.ident), )) # After lump headers, the map revision... file.write(struct.pack('<i', self.map_revision)) # Then each lump. for lump_name in LUMP_WRITE_ORDER: # Write out the actual data. lump = self.lumps[lump_name] if lump_name is BSP_LUMPS.GAME_LUMP: # Construct this right here. lump_start = file.tell() file.write(struct.pack('<i', len(game_lumps))) for game_lump in game_lumps: file.write( struct.pack( '<4s HH', game_lump.id[::-1], game_lump.flags, game_lump.version, )) fixup_loc[ game_lump.id] = file.tell() # Offset goes here. file.write(struct.pack('<4xi', len(game_lump.data))) # Now write data. for game_lump in game_lumps: fixup_data[game_lump.id] = struct.pack( '<i', file.tell()) file.write(game_lump.data) # Length of the game lump is current - start. fixup_data[lump_name] = struct.pack( '<ii', lump_start, file.tell() - lump_start, ) else: # Normal lump. fixup_data[lump_name] = struct.pack( '<ii', file.tell(), len(lump.data), ) file.write(lump.data) # Now apply all the fixups we deferred. for fixup_key in fixup_loc: file.seek(fixup_loc[fixup_key]) file.write(fixup_data[fixup_key])
def parse( path: Path ) -> Tuple[Config, Game, FileSystemChain, Set[FileSystem], PluginFinder, ]: """From some directory, locate and parse the config file. This then constructs and customises each object according to config options. The first srctools.vdf file found in a parent directory is parsed. If none can be found, it tries to find the first subfolder of 'common/' and writes a default copy there. FileNotFoundError is raised if none can be found. This returns: * The config. * Parsed gameinfo. * The chain of filesystems. * A packing blacklist. * The plugin loader. """ conf = Config(OPTIONS) # If the path is a folder, add a dummy folder so parents yields it. # That way we check for a config in this folder. if not path.suffix: path /= 'unused' for folder in path.parents: conf_path = folder / CONF_NAME if conf_path.exists(): LOGGER.info('Config path: "{}"', conf_path.absolute()) with open(conf_path) as f: props = Property.parse(f, conf_path) conf.path = conf_path conf.load(props) break else: LOGGER.warning('Cannot find a valid config file!') # Apply all the defaults. conf.load(Property(None, [])) # Try to write out a default file in the game folder. for folder in path.parents: if folder.parent.stem == 'common': break else: # Give up, write to working directory. folder = Path() conf.path = folder / CONF_NAME LOGGER.warning('Writing default to "{}"', conf.path) with AtomicWriter(str(conf.path)) as f: conf.save(f) game = Game((folder / conf.get(str, 'gameinfo')).resolve()) fsys_chain = game.get_filesystem() blacklist = set() # type: Set[FileSystem] if not conf.get(bool, 'pack_vpk'): for fsys, prefix in fsys_chain.systems: if isinstance(fsys, VPKFileSystem): blacklist.add(fsys) game_root = game.root for prop in conf.get(Property, 'searchpaths'): # type: Property if prop.has_children(): raise ValueError( 'Config "searchpaths" value cannot have children.') assert isinstance(prop.value, str) if prop.value.endswith('.vpk'): fsys = VPKFileSystem(str((game_root / prop.value).resolve())) else: fsys = RawFileSystem(str((game_root / prop.value).resolve())) if prop.name in ('prefix', 'priority'): fsys_chain.add_sys(fsys, priority=True) elif prop.name == 'nopack': blacklist.add(fsys) elif prop.name in ('path', 'pack'): fsys_chain.add_sys(fsys) else: raise ValueError('Unknown searchpath ' 'key "{}"!'.format(prop.real_name)) sources: Dict[Path, PluginSource] = {} builtin_transforms = (Path(sys.argv[0]).parent / 'transforms').resolve() # find all the plugins and make plugin objects out of them for prop in conf.get(Property, 'plugins'): if prop.has_children(): raise ValueError('Config "plugins" value cannot have children.') assert isinstance(prop.value, str) path = (game_root / Path(prop.value)).resolve() if prop.name in ('path', "recursive", 'folder'): if not path.is_dir(): raise ValueError("'{}' is not a directory".format(path)) is_recursive = prop.name == "recursive" try: source = sources[path] except KeyError: sources[path] = PluginSource(path, is_recursive) else: if is_recursive and not source.recursive: # Upgrade to recursive. source.recursive = True elif prop.name in ('single', 'file'): parent = path.parent try: source = sources[parent] except KeyError: source = sources[parent] = PluginSource(parent, False) source.autoload_files.add(path) elif prop.name == "_builtin_": # For development purposes, redirect builtin folder. builtin_transforms = path else: raise ValueError("Unknown plugins key {}".format(prop.real_name)) for source in sources.values(): LOGGER.debug('Plugin path: "{}", recursive={}, files={}', source.folder, source.recursive, sorted(source.autoload_files)) LOGGER.debug('Builtin plugin path is {}', builtin_transforms) if builtin_transforms not in sources: sources[builtin_transforms] = PluginSource(builtin_transforms, True) plugin_finder = PluginFinder('srctools.bsp_transforms.plugin', sources.values()) sys.meta_path.append(plugin_finder) return conf, game, fsys_chain, blacklist, plugin_finder
def save_load_compile_pane( props: Optional[Property] = None) -> Optional[Property]: """Save/load compiler options from 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. """ if props is None: # Saving 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('use_voice_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 # else: Loading chosen_thumb.set(props['sshot_type', chosen_thumb.get()]) cleanup_screenshot.set( props.bool('sshot_cleanup', cleanup_screenshot.get())) if 'sshot_data' in props: screenshot_parts = b'\n'.join([ prop.value.encode('ascii') for prop in props.find_children('sshot_data') ]) screenshot_data = base64.decodebytes(screenshot_parts) with AtomicWriter(SCREENSHOT_LOC, is_bytes=True) as f: f.write(screenshot_data) # Refresh these. set_screen_type() set_screenshot() start_in_elev.set(props.bool('spawn_elev', start_in_elev.get())) try: player_mdl = props['player_model'] except LookupError: pass else: player_model_var.set(PLAYER_MODELS[player_mdl]) COMPILE_CFG['General']['player_model'] = player_mdl VOICE_PRIORITY_VAR.set( props.bool('use_voice_priority', VOICE_PRIORITY_VAR.get())) corr_prop = props.find_key('corridor', []) for group, win in CORRIDOR.items(): try: sel_id = corr_prop[group] except LookupError: "No config option, ok." else: win.sel_item_id(sel_id) COMPILE_CFG['Corridor'][ group] = '0' if sel_id == '<NONE>' else sel_id COMPILE_CFG.save_check() return None