def parse_item_folder(folders, zip_file, pak_id): for fold in folders: prop_path = 'items/' + fold + '/properties.txt' editor_path = 'items/' + fold + '/editoritems.txt' config_path = 'items/' + fold + '/vbsp_config.cfg' try: with zip_file.open(prop_path, 'r') as prop_file: props = Property.parse( prop_file, pak_id + ':' + prop_path, ).find_key('Properties') with zip_file.open(editor_path, 'r') as editor_file: editor = Property.parse(editor_file, pak_id + ':' + editor_path) except KeyError as err: # Opening the files failed! raise IOError('"' + pak_id + ':items/' + fold + '" not valid!' 'Folder likely missing! ') from err editor_iter = Property.find_all(editor, 'Item') folders[fold] = { 'auth': sep_values(props['authors', '']), 'tags': sep_values(props['tags', '']), 'desc': list(desc_parse(props)), 'ent': props['ent_count', '??'], 'url': props['infoURL', None], 'icons': {p.name: p.value for p in props['icon', []]}, 'all_name': props['all_name', None], 'all_icon': props['all_icon', None], 'vbsp': Property(None, []), # The first Item block found 'editor': next(editor_iter), # Any extra blocks (offset catchers, extent items) 'editor_extra': list(editor_iter), } if LOG_ENT_COUNT and folders[fold]['ent'] == '??': print('Warning: "{}:{}" has missing entity count!'.format( pak_id, prop_path, )) # If we have at least 1, but not all of the grouping icon # definitions then notify the author. num_group_parts = ((folders[fold]['all_name'] is not None) + (folders[fold]['all_icon'] is not None) + ('all' in folders[fold]['icons'])) if 0 < num_group_parts < 3: print('Warning: "{}:{}" has incomplete grouping icon ' 'definition!'.format(pak_id, prop_path)) try: with zip_file.open(config_path, 'r') as vbsp_config: folders[fold]['vbsp'] = Property.parse( vbsp_config, pak_id + ':' + config_path, ) except KeyError: folders[fold]['vbsp'] = Property(None, [])
def parse_item_folder(folders, zip_file): for fold in folders: prop_path = 'items/' + fold + '/properties.txt' editor_path = 'items/' + fold + '/editoritems.txt' config_path = 'items/' + fold + '/vbsp_config.cfg' try: with zip_file.open(prop_path, 'r') as prop_file: props = Property.parse( prop_file, prop_path, ).find_key('Properties') with zip_file.open(editor_path, 'r') as editor_file: editor = Property.parse(editor_file, editor_path) except KeyError as err: # Opening the files failed! raise IOError( '"items/' + fold + '" not valid!' 'Folder likely missing! ' ) from err editor_iter = Property.find_all(editor, 'Item') folders[fold] = { 'auth': sep_values(props['authors', '']), 'tags': sep_values(props['tags', '']), 'desc': list(desc_parse(props)), 'ent': props['ent_count', '??'], 'url': props['infoURL', None], 'icons': {p.name: p.value for p in props['icon', []]}, 'all_name': props['all_name', None], 'all_icon': props['all_icon', None], 'vbsp': Property(None, []), # The first Item block found 'editor': next(editor_iter), # Any extra blocks (offset catchers, extent items) 'editor_extra': list(editor_iter), } if LOG_ENT_COUNT and folders[fold]['ent'] == '??': print('Warning: "{}" has missing entity count!'.format(prop_path)) # If we have at least 1, but not all of the grouping icon # definitions then notify the author. num_group_parts = ( (folders[fold]['all_name'] is not None) + (folders[fold]['all_icon'] is not None) + ('all' in folders[fold]['icons']) ) if 0 < num_group_parts < 3: print( 'Warning: "{}" has incomplete grouping icon definition!'.format( prop_path) ) try: with zip_file.open(config_path, 'r') as vbsp_config: folders[fold]['vbsp'] = Property.parse(vbsp_config, config_path) except KeyError: folders[fold]['vbsp'] = Property(None, [])
def parse(cls, data): """Parse a style definition.""" info = data.info selitem_data = get_selitem_data(info) base = info['base', ''] has_video = utils.conv_bool(info['has_video', '1']) sugg = info.find_key('suggested', []) sugg = ( sugg['quote', '<NONE>'], sugg['music', '<NONE>'], sugg['skybox', 'SKY_BLACK'], sugg['goo', 'GOO_NORM'], sugg['elev', '<NONE>'], ) corridors = info.find_key('corridors', []) corridors = { 'sp_entry': corridors.find_key('sp_entry', []), 'sp_exit': corridors.find_key('sp_exit', []), 'coop': corridors.find_key('coop', []), } short_name = selitem_data.short_name or None if base == '': base = None folder = 'styles/' + info['folder'] config = folder + '/vbsp_config.cfg' with data.zip_file.open(folder + '/items.txt', 'r') as item_data: items = Property.parse( item_data, data.pak_id+':'+folder+'/items.txt' ) try: with data.zip_file.open(config, 'r') as vbsp_config: vbsp = Property.parse( vbsp_config, data.pak_id+':'+config, ) except KeyError: vbsp = None return cls( style_id=data.id, name=selitem_data.name, author=selitem_data.auth, desc=selitem_data.desc, icon=selitem_data.icon, editor=items, config=vbsp, base_style=base, short_name=short_name, suggested=sugg, has_video=has_video, corridor_names=corridors, )
def parse(cls, data): """Parse a style definition.""" info = data.info selitem_data = get_selitem_data(info) base = info['base', ''] has_video = utils.conv_bool(info['has_video', '1']) sugg = info.find_key('suggested', []) sugg = ( sugg['quote', '<NONE>'], sugg['music', '<NONE>'], sugg['skybox', 'SKY_BLACK'], sugg['goo', 'GOO_NORM'], sugg['elev', '<NONE>'], ) corridors = info.find_key('corridors', []) corridors = { 'sp_entry': corridors.find_key('sp_entry', []), 'sp_exit': corridors.find_key('sp_exit', []), 'coop': corridors.find_key('coop', []), } if base == '': base = None folder = 'styles/' + info['folder'] config = folder + '/vbsp_config.cfg' with data.zip_file.open(folder + '/items.txt', 'r') as item_data: items = Property.parse(item_data, data.pak_id + ':' + folder + '/items.txt') try: with data.zip_file.open(config, 'r') as vbsp_config: vbsp = Property.parse( vbsp_config, data.pak_id + ':' + config, ) except KeyError: vbsp = None return cls( style_id=data.id, selitem_data=selitem_data, editor=items, config=vbsp, base_style=base, suggested=sugg, has_video=has_video, corridor_names=corridors, )
def parse(cls, data): """Parse a skybox definition.""" config_dir = data.info['config', ''] selitem_data = get_selitem_data(data.info) mat = data.info['material', 'sky_black'] if config_dir == '': # No config at all config = Property(None, []) else: path = 'skybox/' + config_dir + '.cfg' try: with data.zip_file.open(path, 'r') as conf: config = Property.parse(conf) except KeyError: print(config_dir + '.cfg not in zip!') config = Property(None, []) return cls( data.id, selitem_data.name, selitem_data.icon, config, mat, selitem_data.auth, selitem_data.desc, selitem_data.short_name, )
def get_config(prop_block, zip_file, folder, pak_id='', prop_name='config'): """Extract a config file refered 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, []) path = os.path.join(folder, prop_block.value) + '.cfg' try: with zip_file.open(path) as f: return Property.parse( f, pak_id + ':' + path, ) except KeyError: print('"{}:{}" not in zip!'.format(pak_id, path)) return Property(None, [])
def parse(posfile, propfile, path): "Parse through the given palette file to get all data." props = Property.parse(propfile, path + ':properties.txt') name = "Unnamed" opts = {} for option in props: if option.name == "name": name = option.value else: opts[option.name.casefold()] = option.value pos = [] for dirty_line in posfile: line = utils.clean_line(dirty_line) if line: # Lines follow the form # "ITEM_BUTTON_FLOOR", 2 # for subtype 3 of the button if line.startswith('"'): val = line.split('",') if len(val) == 2: pos.append(( val[0][1:], # Item ID int(val[1].strip()), # Item subtype )) else: print("Malformed row '"+line+"'!") return None return Palette(name, pos, opts, filename=path)
def get_config(prop_block, zip_file, folder, pak_id='', prop_name='config'): """Extract a config file refered 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, []) path = os.path.join(folder, prop_block.value) + '.cfg' try: with zip_file.open(path) as f: return Property.parse(f, pak_id + ':' + path, ) except KeyError: print('"{}:{}" not in zip!'.format(pak_id, path)) return Property(None, [])
def parse(posfile, propfile, path): "Parse through the given palette file to get all data." props = Property.parse(propfile, path + ':properties.txt') name = "Unnamed" opts = {} for option in props: if option.name == "name": name = option.value else: opts[option.name.casefold()] = option.value pos = [] for dirty_line in posfile: line = utils.clean_line(dirty_line) if line: # Lines follow the form # "ITEM_BUTTON_FLOOR", 2 # for subtype 3 of the button if line.startswith('"'): val = line.split('",') if len(val) == 2: pos.append(( val[0][1:], # Item ID int(val[1].strip()), # Item subtype )) else: LOGGER.warning('Malformed row "{}"!', line) return None return Palette(name, pos, opts, filename=path)
def load_conf(): """Read the config and build our dictionaries.""" global INST_SPECIAL with open('bee2/instances.cfg') as f: prop_block = Property.parse( f, 'bee2/instances.cfg' ).find_key('Allinstances') for prop in prop_block: INSTANCE_FILES[prop.real_name] = [ inst.value.casefold() for inst in prop ] INST_SPECIAL = { key.casefold(): resolve(val_string) for key, val_string in SPECIAL_INST.items() } INST_SPECIAL['indpan'] = ( INST_SPECIAL['indpancheck'] + INST_SPECIAL['indpantimer'] ) INST_SPECIAL['white_frames'] = ( resolve('<ITEM_ENTRY_DOOR:7>') + resolve('<ITEM_EXIT_DOOR:4>') ) INST_SPECIAL['black_frames'] = ( resolve('<ITEM_ENTRY_DOOR:8>') + resolve('<ITEM_EXIT_DOOR:5>') )
def find_packages(pak_dir, zips, zip_name_lst): """Search a folder for packages, recursing if necessary.""" found_pak = False for name in os.listdir(pak_dir): # Both files and dirs name = os.path.join(pak_dir, name) is_dir = os.path.isdir(name) if name.endswith('.zip') and os.path.isfile(name): zip_file = ZipFile(name) elif is_dir: zip_file = FakeZip(name) if 'info.txt' in zip_file.namelist(): # Is it valid? zips.append(zip_file) zip_name_lst.append(os.path.abspath(name)) print('Reading package "' + name + '"') with zip_file.open('info.txt') as info_file: info = Property.parse(info_file, name + ':info.txt') pak_id = info['ID'] disp_name = info['Name', pak_id] packages[pak_id] = PackageData( zip_file, info, name, disp_name, ) found_pak = True else: if is_dir: # This isn't a package, so check the subfolders too... print('Checking subdir "{}" for packages...'.format(name)) find_packages(name, zips, zip_name_lst) else: zip_file.close() print('ERROR: Bad package "{}"!'.format(name)) if not found_pak: print('No packages in folder!')
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 our packed scripts, force the game to load them # (we know they're used). 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 load_config(): global CONF utils.con_log('Loading Settings...') try: with open("bee2/vrad_config.cfg") as config: CONF = Property.parse(config, 'bee2/vrad_config.cfg').find_key( 'Config', []) except FileNotFoundError: pass utils.con_log('Config Loaded!')
def load_config(): global CONF LOGGER.info('Loading Settings...') try: with open("bee2/vrad_config.cfg") as config: CONF = Property.parse(config, 'bee2/vrad_config.cfg').find_key( 'Config', [] ) except FileNotFoundError: pass LOGGER.info('Config Loaded!')
def init_trans(): """Load a copy of basemodui, used to translate item strings. Valve's items use special translation strings which would look ugly if we didn't convert them. """ try: with open('../basemodui.txt') as trans: trans_prop = Property.parse(trans, 'basemodui.txt') for item in trans_prop.find_key("lang", []).find_key("tokens", []): trans_data[item.real_name] = item.value except IOError: pass
def parse(cls, data): """Parse a voice line definition.""" selitem_data = get_selitem_data(data.info) path = 'voice/' + data.info['file'] + '.voice' with data.zip_file.open(path, 'r') as conf: config = Property.parse(conf, path) return cls( data.id, selitem_data.name, config, selitem_data.icon, selitem_data.desc, auth=selitem_data.auth, short_name=selitem_data.short_name )
def init_trans(): """Load a copy of basemodui, used to translate item strings. Valve's items use special translation strings which would look ugly if we didn't convert them. """ global trans_data try: with open('../basemodui.txt') as trans: trans_prop = Property.parse(trans, 'basemodui.txt') trans_data = { item.real_name: item.value for item in trans_prop.find_key("lang", []).find_key("tokens", []) } except IOError: pass
def load_conf(): """Read the config and build our dictionaries.""" global INST_SPECIAL with open("bee2/instances.cfg") as f: prop_block = Property.parse(f, "bee2/instances.cfg").find_key("Allinstances") for prop in prop_block: INSTANCE_FILES[prop.real_name] = [inst.value.casefold() for inst in prop] INST_SPECIAL = {key.casefold(): resolve(val_string) for key, val_string in SPECIAL_INST.items()} INST_SPECIAL["indpan"] = INST_SPECIAL["indpancheck"] + INST_SPECIAL["indpantimer"] INST_SPECIAL["lasercatcher"] = resolve("<ITEM_LASER_CATCHER_CENTER>") + resolve("<ITEM_LASER_CATCHER_OFFSET>") INST_SPECIAL["laseremitter"] = resolve("<ITEM_LASER_EMITTER_CENTER>") + resolve("<ITEM_LASER_EMITTER_OFFSET>") INST_SPECIAL["laserrelay"] = resolve("<ITEM_LASER_RELAY_CENTER>") + resolve("<ITEM_LASER_RELAY_OFFSET>")
def load_conf(): """Read the config and build our dictionaries.""" global INST_SPECIAL with open('bee2/instances.cfg') as f: prop_block = Property.parse( f, 'bee2/instances.cfg' ).find_key('Allinstances') for prop in prop_block: INSTANCE_FILES[prop.real_name] = [ inst.value.casefold() for inst in prop ] INST_SPECIAL = { key.casefold(): resolve(val_string) for key, val_string in SPECIAL_INST.items() } INST_SPECIAL['indpan'] = ( INST_SPECIAL['indpancheck'] + INST_SPECIAL['indpantimer'] ) INST_SPECIAL['transitionents'] = ( resolve('<ITEM_ENTRY_DOOR:11>') + resolve('<ITEM_COOP_ENTRY_DOOR:4>') ) INST_SPECIAL['lasercatcher'] = ( resolve('<ITEM_LASER_CATCHER_CENTER>') + resolve('<ITEM_LASER_CATCHER_OFFSET>') ) INST_SPECIAL['laseremitter'] = ( resolve('<ITEM_LASER_EMITTER_CENTER>') + resolve('<ITEM_LASER_EMITTER_OFFSET>') ) INST_SPECIAL['laserrelay'] = ( resolve('<ITEM_LASER_RELAY_CENTER>') + resolve('<ITEM_LASER_RELAY_OFFSET>') )
def get_config( prop_block, zip_file, folder, pak_id='', prop_name='config', extension='.cfg', ): """Extract a config file refered 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 seperator, even on Windows! path = folder + '/' + prop_block.value if len(path) < 3 or path[-4] != '.': # Add extension path += extension try: with zip_file.open(path) as f: return Property.parse( f, pak_id + ':' + path, ) except KeyError: 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 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 find_packages(pak_dir, zips, zip_name_lst): """Search a folder for packages, recursing if necessary.""" found_pak = False for name in os.listdir(pak_dir): # Both files and dirs name = os.path.join(pak_dir, name) is_dir = os.path.isdir(name) if name.endswith('.zip') and os.path.isfile(name): zip_file = ZipFile(name) elif is_dir: zip_file = FakeZip(name) else: utils.con_log('Extra file: ', name) continue if 'info.txt' in zip_file.namelist(): # Is it valid? zips.append(zip_file) zip_name_lst.append(os.path.abspath(name)) print('Reading package "' + name + '"') with zip_file.open('info.txt') as info_file: info = Property.parse(info_file, name + ':info.txt') pak_id = info['ID'] disp_name = info['Name', pak_id] packages[pak_id] = PackageData( zip_file, info, name, disp_name, ) found_pak = True else: if is_dir: # This isn't a package, so check the subfolders too... print('Checking subdir "{}" for packages...'.format(name)) find_packages(name, zips, zip_name_lst) else: zip_file.close() print('ERROR: Bad package "{}"!'.format(name)) if not found_pak: print('No packages in folder!')
def from_file(cls, path, zip_file): """Initialise from a file. path is the file path for the map inside the zip, without extension. zip_file is either a ZipFile or FakeZip object. """ with zip_file.open(path + '.p2c') as file: props = Property.parse(file, path) props = props.find_key('portal2_puzzle', []) title = props['title', None] if title is None: title = '<' + path.rsplit('/', 1)[-1] + '.p2c>' return cls( path=path, zip_file = zip_file, title=title, desc=props['description', '...'], is_coop=utils.conv_bool(props['coop', '0']), create_time=Date(props['timestamp_created', '']), mod_time=Date(props['timestamp_modified', '']), )
def parse(cls, data): """Parse a music definition.""" selitem_data = get_selitem_data(data.info) inst = data.info['instance', None] sound = data.info['soundscript', None] config_dir = 'music/' + data.info['config', ''] try: with data.zip_file.open(config_dir) as conf: config = Property.parse(conf, config_dir) except KeyError: config = Property(None, []) return cls( data.id, selitem_data.name, selitem_data.icon, selitem_data.auth, selitem_data.desc, short_name=selitem_data.short_name, inst=inst, sound=sound, config=config, )
def gen_sound_manifest(additional, has_music=False): """Generate a new game_sounds_manifest.txt file. This includes all the current scripts defined, plus any custom ones. """ 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) # 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 from_file(cls, path, zip_file): """Initialise from a file. path is the file path for the map inside the zip, without extension. zip_file is either a ZipFile or FakeZip object. """ # Some P2Cs may have non-ASCII characters in descriptions, so we # need to read it as bytes and convert to utf-8 ourselves - zips # don't convert encodings automatically for us. with zip_open_bin(zip_file, path + '.p2c') as file: props = Property.parse( # Decode the P2C as UTF-8, and skip unknown characters. # We're only using it for display purposes, so that should # be sufficent. EncodedFile( file, data_encoding='utf-8', errors='replace', ), path, ) props = props.find_key('portal2_puzzle', []) title = props['title', None] if title is None: title = '<' + path.rsplit('/', 1)[-1] + '.p2c>' return cls( path=path, zip_file=zip_file, title=title, desc=props['description', '...'], is_coop=utils.conv_bool(props['coop', '0']), create_time=Date(props['timestamp_created', '']), mod_time=Date(props['timestamp_modified', '']), )