def __init__( self, style_id, selitem_data: 'SelitemData', editor, config=None, base_style=None, suggested=None, has_video=True, corridor_names=utils.EmptyMapping, ): self.id = style_id self.selitem_data = selitem_data self.editor = editor self.base_style = base_style self.bases = [] # Set by setup_style_tree() self.suggested = suggested or {} self.has_video = has_video self.corridor_names = { 'sp_entry': corridor_names.get('sp_entry', Property('', [])), 'sp_exit': corridor_names.get('sp_exit', Property('', [])), 'coop': corridor_names.get('coop', Property('', [])), } if config is None: self.config = Property(None, []) else: self.config = config
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 generate_music_script(data: Property, pack_list): """Generate a soundscript file for music.""" # We also pack the filenames used for the tracks - that way funnel etc # only get packed when needed. Stock sounds are in VPKS or in aperturetag/, # we don't check there. # The voice attrs used in the map - we can skip tracks voice_attr = CONF['VoiceAttr', ''].casefold().split(';') funnel = data.find_key('tbeam', '') bounce = data.find_key('bouncegel', '') speed = data.find_key('speedgel', '') # The sounds must be present, and the items should be in the map. has_funnel = funnel.value and ( 'funnel' in voice_attr or 'excursionfunnel' in voice_attr ) has_bounce = bounce.value and ( 'bouncegel' in voice_attr or 'bluegel' in voice_attr ) # Speed-gel sounds also play when flinging, so keep it always. with open(os.path.join('bee2', 'inject', 'music_script.txt'), 'w') as file: # Write the base music track file.write(MUSIC_START.format(name='', vol='1')) write_sound(file, data.find_key('base'), pack_list, snd_prefix='#*') file.write(MUSIC_BASE) # The 'soundoperators' section is still open now. # Add the operators to play the auxilluary sounds.. if has_funnel: file.write(MUSIC_FUNNEL_MAIN) if has_bounce: file.write(MUSIC_GEL_BOUNCE_MAIN) if speed.value: file.write(MUSIC_GEL_SPEED_MAIN) # End the main sound block file.write(MUSIC_END) if has_funnel: # Write the 'music.BEE2_funnel' sound entry file.write('\n') file.write(MUSIC_START.format(name='_funnel', vol='1')) write_sound(file, funnel, pack_list, snd_prefix='*') file.write(MUSIC_FUNNEL_STACK) if has_bounce: file.write('\n') file.write(MUSIC_START.format(name='_gel_bounce', vol='0.5')) write_sound(file, bounce, pack_list, snd_prefix='*') file.write(MUSIC_GEL_STACK) if speed.value: file.write('\n') file.write(MUSIC_START.format(name='_gel_speed', vol='0.5')) write_sound(file, speed, pack_list, snd_prefix='*') file.write(MUSIC_GEL_STACK)
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 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 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 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 __init__( self, style_id, name, author, desc, icon, editor, config=None, base_style=None, short_name=None, suggested=None, has_video=True, corridor_names=utils.EmptyMapping, ): self.id = style_id self.auth = author self.name = name self.desc = desc self.icon = icon self.short_name = name if short_name is None else short_name self.editor = editor self.base_style = base_style self.bases = [] # Set by setup_style_tree() self.suggested = suggested or {} self.has_video = has_video self.corridor_names = { 'sp_entry': corridor_names.get('sp_entry', Property('', [])), 'sp_exit': corridor_names.get('sp_exit', Property('', [])), 'coop': corridor_names.get('coop', Property('', [])), } if config is None: self.config = Property(None, []) else: self.config = config
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 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 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 load_conf(prop_block: Property): """Read the config and build our dictionaries.""" global INST_SPECIAL for prop in prop_block.find_key("Allinstances"): 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()} # Several special items which use multiple item types! # Checkmark and Timer indicator panels: INST_SPECIAL["indpan"] = INST_SPECIAL["indpancheck"] + INST_SPECIAL["indpantimer"] INST_SPECIAL["door_frame"] = INST_SPECIAL["door_frame_sp"] + INST_SPECIAL["door_frame_coop"] INST_SPECIAL["white_frame"] = INST_SPECIAL["white_frame_sp"] + INST_SPECIAL["white_frame_coop"] INST_SPECIAL["black_frame"] = INST_SPECIAL["black_frame_sp"] + INST_SPECIAL["black_frame_coop"] # Arrival_departure_ents is set in both entry doors - it's usually the same # though. INST_SPECIAL["transitionents"] = resolve("<ITEM_ENTRY_DOOR:11>") + resolve("<ITEM_COOP_ENTRY_DOOR:4>") # Laser items have the offset and centered item versions. 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 __init__( self, style_id, name, author, desc, icon, editor, config=None, base_style=None, short_name=None, suggested=None, has_video=True, ): self.id = style_id self.auth = author self.name = name self.desc = desc self.icon = icon self.short_name = name if short_name is None else short_name self.editor = editor self.base_style = base_style self.bases = [] # Set by setup_style_tree() self.suggested = suggested or {} self.has_video = has_video if config is None: self.config = Property(None, []) else: self.config = config
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 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 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 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 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_package(zip_file, info, pak_id, disp_name): """Parse through the given package to find all the components.""" for pre in Property.find_key(info, 'Prerequisites', []).value: if pre.value not in packages: utils.con_log( 'Package "' + pre.value + '" required for "' + pak_id + '" - ignoring package!' ) return False objects = 0 # First read through all the components we have, so we can match # overrides to the originals for comp_type in OBJ_TYPES: allow_dupes = OBJ_TYPES[comp_type].allow_mult # Look for overrides for obj in info.find_all("Overrides", comp_type): obj_id = obj['id'] obj_override[comp_type][obj_id].append( ParseData(zip_file, obj_id, obj, pak_id) ) for obj in info.find_all(comp_type): obj_id = obj['id'] if obj_id in all_obj[comp_type]: if allow_dupes: # Pretend this is an override obj_override[comp_type][obj_id].append( ParseData(zip_file, obj_id, obj, pak_id) ) else: raise Exception('ERROR! "' + obj_id + '" defined twice!') objects += 1 all_obj[comp_type][obj_id] = ObjData( zip_file, obj, pak_id, disp_name, ) img_count = 0 img_loc = os.path.join('resources', 'bee2') for item in zip_names(zip_file): item = os.path.normcase(item).casefold() if item.startswith("resources"): extract_packages.res_count += 1 if item.startswith(img_loc): img_count += 1 return objects, img_count
def __init__( self, music_id, selitem_data: 'SelitemData', config=None, inst=None, sound=None, ): self.id = music_id self.config = config or Property(None, []) self.inst = inst self.sound = sound self.selitem_data = selitem_data
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 parse_package(pack: 'Package'): """Parse through the given package to find all the components.""" for pre in Property.find_key(pack.info, 'Prerequisites', []): if pre.value not in packages: LOGGER.warning( 'Package "{pre}" required for "{id}" - ' 'ignoring package!', pre=pre.value, id=pack.id, ) return False # First read through all the components we have, so we can match # overrides to the originals for comp_type in OBJ_TYPES: allow_dupes = OBJ_TYPES[comp_type].allow_mult # Look for overrides for obj in pack.info.find_all("Overrides", comp_type): obj_id = obj['id'] obj_override[comp_type][obj_id].append( ParseData(pack.zip, obj_id, obj, pack.id) ) for obj in pack.info.find_all(comp_type): obj_id = obj['id'] if obj_id in all_obj[comp_type]: if allow_dupes: # Pretend this is an override obj_override[comp_type][obj_id].append( ParseData(pack.zip, obj_id, obj, pack.id) ) else: raise Exception('ERROR! "' + obj_id + '" defined twice!') all_obj[comp_type][obj_id] = ObjData( pack.zip, obj, pack.id, pack.disp_name, ) img_count = 0 img_loc = os.path.join('resources', 'bee2') for item in zip_names(pack.zip): item = os.path.normcase(item).casefold() if item.startswith("resources"): extract_packages.res_count += 1 if item.startswith(img_loc): img_count += 1 return img_count
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 __init__(self, item_id, versions, def_version, needs_unlock=False, all_conf=None, unstyled=False, glob_desc=(), desc_last=False): self.id = item_id self.versions = versions self.def_ver = def_version self.def_data = def_version['def_style'] self.needs_unlock = needs_unlock self.all_conf = all_conf or Property(None, []) self.unstyled = unstyled self.glob_desc = glob_desc self.glob_desc_last = desc_last
def parse_package(zip_file, info, pak_id, disp_name): """Parse through the given package to find all the components.""" for pre in Property.find_key(info, 'Prerequisites', []).value: if pre.value not in packages: utils.con_log('Package "' + pre.value + '" required for "' + pak_id + '" - ignoring package!') return False objects = 0 # First read through all the components we have, so we can match # overrides to the originals for comp_type in OBJ_TYPES: allow_dupes = OBJ_TYPES[comp_type].allow_mult # Look for overrides for obj in info.find_all("Overrides", comp_type): obj_id = obj['id'] obj_override[comp_type][obj_id].append( ParseData(zip_file, obj_id, obj, pak_id)) for obj in info.find_all(comp_type): obj_id = obj['id'] if obj_id in all_obj[comp_type]: if allow_dupes: # Pretend this is an override obj_override[comp_type][obj_id].append( ParseData(zip_file, obj_id, obj, pak_id)) else: raise Exception('ERROR! "' + obj_id + '" defined twice!') objects += 1 all_obj[comp_type][obj_id] = ObjData( zip_file, obj, pak_id, disp_name, ) img_count = 0 img_loc = os.path.join('resources', 'bee2') for item in zip_names(zip_file): item = os.path.normcase(item).casefold() if item.startswith("resources"): extract_packages.res_count += 1 if item.startswith(img_loc): img_count += 1 return objects, img_count
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 flag_random(inst, res: Property): """Randomly is either true or false.""" if res.has_children(): chance = res['chance', '100'] seed = res['seed', ''] else: chance = res.value seed = '' # Allow ending with '%' sign chance = utils.conv_int(chance.rstrip('%'), 100) random.seed('random_chance_{}:{}_{}_{}'.format( seed, inst['targetname', ''], inst['origin'], inst['angles'], )) return random.randrange(100) < chance
def parse_package(zip_file, info, pak_id, disp_name): """Parse through the given package to find all the components.""" global res_count for pre in Property.find_key(info, 'Prerequisites', []).value: if pre.value not in packages: utils.con_log( 'Package "' + pre.value + '" required for "' + pak_id + '" - ignoring package!' ) return False objects = 0 # First read through all the components we have, so we can match # overrides to the originals for comp_type in obj_types: # Look for overrides for obj in info.find_all("Overrides", comp_type): obj_id = obj['id'] obj_override[comp_type][obj_id].append( (zip_file, obj) ) for obj in info.find_all(comp_type): obj_id = obj['id'] if obj_id in all_obj[comp_type]: raise Exception('ERROR! "' + obj_id + '" defined twice!') objects += 1 all_obj[comp_type][obj_id] = ObjData( zip_file, obj, pak_id, disp_name, ) if res_count != -1: for item in zip_names(zip_file): if item.startswith("resources"): res_count += 1 loader.set_length("RES", res_count) return objects
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 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 write_sound(file, snds: Property, pack_list, snd_prefix='*'): """Write either a single sound, or multiple rndsound. snd_prefix is the prefix for each filename - *, #, @, etc. """ if snds.has_children(): file.write(' "rndwave"\n {\n') for snd in snds: file.write( ' "wave" "{sndchar}{file}"\n'.format( file=snd.value, sndchar=snd_prefix, ) ) pack_list.add('sound/' + snd.value.casefold()) file.write(' }\n') else: file.write( ' "wave" "{sndchar}{file}"\n'.format( file=snds.value, sndchar=snd_prefix, ) ) pack_list.add('sound/' + snds.value.casefold())
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', '']), )
def load_conf(prop_block: Property): """Read the config and build our dictionaries.""" global INST_SPECIAL for prop in prop_block.find_key('Allinstances', []): INSTANCE_FILES[prop.real_name] = [ inst.value.casefold() for inst in prop ] for prop in prop_block.find_key('CustInstances', []): CUST_INST_FILES[prop.real_name] = { inst.name: inst.value.casefold() for inst in prop } INST_SPECIAL = { key.casefold(): resolve(val_string, silent=True) for key, val_string in SPECIAL_INST.items() } # Several special items which use multiple item types! # Checkmark and Timer indicator panels: INST_SPECIAL['indpan'] = ( INST_SPECIAL['indpancheck'] + INST_SPECIAL['indpantimer'] ) INST_SPECIAL['door_frame'] = ( INST_SPECIAL['door_frame_sp'] + INST_SPECIAL['door_frame_coop'] ) INST_SPECIAL['white_frame'] = ( INST_SPECIAL['white_frame_sp'] + INST_SPECIAL['white_frame_coop'] ) INST_SPECIAL['black_frame'] = ( INST_SPECIAL['black_frame_sp'] + INST_SPECIAL['black_frame_coop'] ) # Arrival_departure_ents is set in both entry doors - it's usually the same # though. INST_SPECIAL['transitionents'] = ( resolve('<ITEM_ENTRY_DOOR:11>') + resolve('<ITEM_COOP_ENTRY_DOOR:4>') ) # Laser items have the offset and centered item versions. INST_SPECIAL['lasercatcher'] = ( resolve('<ITEM_LASER_CATCHER_CENTER>', silent=True) + resolve('<ITEM_LASER_CATCHER_OFFSET>', silent=True) ) INST_SPECIAL['laseremitter'] = ( resolve('<ITEM_LASER_EMITTER_CENTER>', silent=True) + resolve('<ITEM_LASER_EMITTER_OFFSET>', silent=True) ) INST_SPECIAL['laserrelay'] = ( resolve('<ITEM_LASER_RELAY_CENTER>', silent=True) + resolve('<ITEM_LASER_RELAY_OFFSET>', silent=True) ) LOGGER.warning('None in vals: {}', None in INST_SPECIAL.values())
def export( self, style, all_items, music, skybox, voice, style_vars, elevator, pack_list, editor_sounds, should_refresh=False, ): """Generate the editoritems.txt and vbsp_config. - If no backup is present, the original editoritems is backed up - We unlock the mandatory items if specified - """ print('-' * 20) print('Exporting Items and Style for "' + self.name + '"!') print('Style =', style) print('Music =', music) print('Voice =', voice) print('Skybox =', skybox) print('Elevator = ', elevator) print('Style Vars:\n {') for key, val in style_vars.items(): print(' {} = {!s}'.format(key, val)) print(' }') print(len(pack_list), 'Pack Lists!') print(len(editor_sounds), 'Editor Sounds!') print('-' * 20) # VBSP, VRAD, editoritems export_screen.set_length('BACK', len(FILES_TO_BACKUP)) export_screen.set_length( 'CONF', # VBSP_conf, Editoritems, instances, gameinfo, pack_lists, # editor_sounds 6 + # Don't add the voicelines to the progress bar if not selected (0 if voice is None else len(VOICE_PATHS)), ) # files in compiler/ export_screen.set_length('COMP', len(os.listdir('../compiler'))) if should_refresh: export_screen.set_length('RES', extract_packages.res_count) else: export_screen.skip_stage('RES') export_screen.show() export_screen.grab_set_global() # Stop interaction with other windows vbsp_config = style.config.copy() # Editoritems.txt is composed of a "ItemData" block, holding "Item" and # "Renderables" sections. editoritems = Property("ItemData", list(style.editor.find_all('Item'))) for item in sorted(all_items): item_block, editor_parts, config_part = all_items[item].export() editoritems += item_block editoritems += editor_parts vbsp_config += config_part if voice is not None: vbsp_config += voice.config if skybox is not None: vbsp_config.set_key( ('Textures', 'Special', 'Sky'), skybox.material, ) vbsp_config += skybox.config if style.has_video: if elevator is None: # Use a randomised video vbsp_config.set_key( ('Elevator', 'type'), 'RAND', ) elif elevator.id == 'VALVE_BLUESCREEN': # This video gets a special script and handling vbsp_config.set_key( ('Elevator', 'type'), 'BSOD', ) else: # Use the particular selected video vbsp_config.set_key( ('Elevator', 'type'), 'FORCE', ) vbsp_config.set_key( ('Elevator', 'horiz'), elevator.horiz_video, ) vbsp_config.set_key( ('Elevator', 'vert'), elevator.vert_video, ) else: # No elevator video for this style vbsp_config.set_key( ('Elevator', 'type'), 'NONE', ) if music is not None: if music.sound is not None: vbsp_config.set_key( ('Options', 'music_SoundScript'), music.sound, ) if music.inst is not None: vbsp_config.set_key( ('Options', 'music_instance'), music.inst, ) vbsp_config.set_key(('Options', 'music_ID'), music.id) vbsp_config += music.config if voice is not None: vbsp_config.set_key( ('Options', 'voice_pack'), voice.id, ) vbsp_config.set_key(('Options', 'voice_char'), ','.join(voice.chars)) vbsp_config.set_key( ('Options', 'BEE2_loc'), os.path.dirname( os.getcwd()) # Go up one dir to our actual location ) vbsp_config.ensure_exists('StyleVars') vbsp_config['StyleVars'] += [ Property(key, utils.bool_as_int(val)) for key, val in style_vars.items() ] pack_block = Property('PackList', []) # A list of materials which will casue a specific packlist to be used. pack_triggers = Property('PackTriggers', []) for key, pack in pack_list.items(): pack_block.append( Property(key, [Property('File', file) for file in pack.files])) for trigger_mat in pack.trigger_mats: pack_triggers.append( Property('Material', [ Property('Texture', trigger_mat), Property('PackList', pack.id), ])) if pack_triggers.value: vbsp_config.append(pack_triggers) # If there are multiple of these blocks, merge them together # They will end up in this order. vbsp_config.merge_children( 'Textures', 'Fizzler', 'Options', 'StyleVars', 'Conditions', 'Voice', 'PackTriggers', ) for name, file, ext in FILES_TO_BACKUP: item_path = self.abs_path(file + ext) backup_path = self.abs_path(file + '_original' + ext) if os.path.isfile(item_path) and not os.path.isfile(backup_path): print('Backing up original ' + name + '!') shutil.copy(item_path, backup_path) export_screen.step('BACK') # This is the connections "heart" icon and "error" icon editoritems += style.editor.find_key("Renderables", []) # Build a property tree listing all of the instances for each item all_instances = Property("AllInstances", []) for item in editoritems.find_all("Item"): item_prop = Property(item['Type'], []) all_instances.append(item_prop) for inst_block in item.find_all("Exporting", "instances"): for inst in inst_block: item_prop.append(Property('Instance', inst['Name'])) if style_vars.get('UnlockDefault', False): print('Unlocking Items!') for item in editoritems.find_all('Item'): # If the Unlock Default Items stylevar is enabled, we # want to force the corridors and obs room to be # deletable and copyable # Also add DESIRES_UP, so they place in the correct orientation if item['type', ''] in _UNLOCK_ITEMS: editor_section = item.find_key("Editor", []) editor_section['deletable'] = '1' editor_section['copyable'] = '1' editor_section['DesiredFacing'] = 'DESIRES_UP' print('Editing Gameinfo!') self.edit_gameinfo(True) export_screen.step('CONF') print('Writing Editoritems!') os.makedirs(self.abs_path('portal2_dlc2/scripts/'), exist_ok=True) with open(self.abs_path('portal2_dlc2/scripts/editoritems.txt'), 'w') as editor_file: for line in editoritems.export(): editor_file.write(line) export_screen.step('CONF') print('Writing VBSP Config!') os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True) with open(self.abs_path('bin/bee2/vbsp_config.cfg'), 'w') as vbsp_file: for line in vbsp_config.export(): vbsp_file.write(line) export_screen.step('CONF') print('Writing instance list!') with open(self.abs_path('bin/bee2/instances.cfg'), 'w') as inst_file: for line in all_instances.export(): inst_file.write(line) export_screen.step('CONF') print('Writing packing list!') with open(self.abs_path('bin/bee2/pack_list.cfg'), 'w') as pack_file: for line in pack_block.export(): pack_file.write(line) export_screen.step('CONF') print('Editing game_sounds!') self.add_editor_sounds(editor_sounds.values()) export_screen.step('CONF') if voice is not None: for prefix, dest, pretty in VOICE_PATHS: path = os.path.join( os.getcwd(), '..', 'config', 'voice', prefix + voice.id + '.cfg', ) print(path) if os.path.isfile(path): shutil.copy( path, self.abs_path('bin/bee2/{}voice.cfg'.format(dest))) print('Written "{}voice.cfg"'.format(dest)) else: print('No ' + pretty + ' voice config!') export_screen.step('CONF') print('Copying Custom Compiler!') for file in os.listdir('../compiler'): print('\t* compiler/{0} -> bin/{0}'.format(file)) shutil.copy(os.path.join('../compiler', file), self.abs_path('bin/')) export_screen.step('COMP') if should_refresh: print('Copying Resources!') self.refresh_cache() export_screen.grab_release() export_screen.reset() # Hide loading screen, we're done
from datetime import datetime from zipfile import ZipFile from io import BytesIO import os import os.path import stat import shutil import sys import subprocess from property_parser import Property from BSP import BSP, BSP_LUMPS import utils CONF = Property('Config') SCREENSHOT_DIR = os.path.join( '..', 'portal2', # This is hardcoded into P2, it won't change for mods. 'puzzles', # Then the <random numbers> folder ) # Locations of resources we need to pack RES_ROOT = [ os.path.join('..', loc) for loc in ('bee2', 'bee2_dev', 'portal2_dlc2') ] def quote(txt): return '"' + txt + '"'
class Style: def __init__( self, style_id, selitem_data: 'SelitemData', editor, config=None, base_style=None, suggested=None, has_video=True, corridor_names=utils.EmptyMapping, ): self.id = style_id self.selitem_data = selitem_data self.editor = editor self.base_style = base_style self.bases = [] # Set by setup_style_tree() self.suggested = suggested or {} self.has_video = has_video self.corridor_names = { 'sp_entry': corridor_names.get('sp_entry', Property('', [])), 'sp_exit': corridor_names.get('sp_exit', Property('', [])), 'coop': corridor_names.get('coop', Property('', [])), } if config is None: self.config = Property(None, []) else: self.config = config @classmethod 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 add_over(self, override: 'Style'): """Add the additional commands to ourselves.""" self.editor.extend(override.editor) self.config.extend(override.config) self.selitem_data.auth.extend(override.selitem_data.auth) def __repr__(self): return '<Style:' + self.id + '>'