def build_instance_data(editoritems: Property): """Build a property tree listing all of the instances for each item. as well as another listing the input and output commands. VBSP uses this to reduce duplication in VBSP_config files. This additionally strips custom instance definitions from the original list. """ instance_locs = Property("AllInstances", []) cust_inst = Property("CustInstances", []) commands = Property("Connections", []) item_classes = Property("ItemClasses", []) root_block = Property(None, [instance_locs, item_classes, cust_inst, commands]) for item in editoritems.find_all("Item"): instance_block = Property(item["Type"], []) instance_locs.append(instance_block) comm_block = Property(item["Type"], []) for inst_block in item.find_all("Exporting", "instances"): for inst in inst_block.value[:]: # type: Property if inst.name.isdigit(): # Direct Portal 2 value instance_block.append(Property("Instance", inst["Name"])) else: # It's a custom definition, remove from editoritems inst_block.value.remove(inst) # Allow the name to start with 'bee2_' also to match # the <> definitions - it's ignored though. name = inst.name if name[:5] == "bee2_": name = name[5:] cust_inst.set_key( (item["type"], name), # Allow using either the normal block format, # or just providing the file - we don't use the # other values. inst["name"] if inst.has_children() else inst.value, ) # Look in the Inputs and Outputs blocks to find the io definitions. # Copy them to property names like 'Input_Activate'. for io_type in ("Inputs", "Outputs"): for block in item.find_all("Exporting", io_type, CONN_NORM): for io_prop in block: comm_block[io_type[:-1] + "_" + io_prop.real_name] = io_prop.value # The funnel item type is special, having the additional input type. # Handle that specially. if item["type"] == "item_tbeam": for block in item.find_all("Exporting", "Inputs", CONN_FUNNEL): for io_prop in block: comm_block["TBEAM_" + io_prop.real_name] = io_prop.value # Fizzlers don't work correctly with outputs. This is a signal to # conditions.fizzler, but it must be removed in editoritems. if item["ItemClass", ""].casefold() == "itembarrierhazard": for block in item.find_all("Exporting", "Outputs"): if CONN_NORM in block: del block[CONN_NORM] # Record the itemClass for each item type. item_classes[item["type"]] = item["ItemClass", "ItemBase"] # Only add the block if the item actually has IO. if comm_block.value: commands.append(comm_block) return root_block.export()
def build_instance_data(editoritems: Property): """Build a property tree listing all of the instances for each item. as well as another listing the input and output commands. VBSP uses this to reduce duplication in VBSP_config files. This additionally strips custom instance definitions from the original list. """ instance_locs = Property("AllInstances", []) cust_inst = Property("CustInstances", []) commands = Property("Connections", []) item_classes = Property("ItemClasses", []) root_block = Property(None, [ instance_locs, item_classes, cust_inst, commands, ]) for item in editoritems.find_all("Item"): instance_block = Property(item['Type'], []) instance_locs.append(instance_block) comm_block = Property(item['Type'], []) for inst_block in item.find_all("Exporting", "instances"): for inst in inst_block.value[:]: # type: Property if inst.name.isdigit(): # Direct Portal 2 value instance_block.append( Property('Instance', inst['Name']) ) else: # It's a custom definition, remove from editoritems inst_block.value.remove(inst) # Allow the name to start with 'bee2_' also to match # the <> definitions - it's ignored though. name = inst.name if name[:5] == 'bee2_': name = name[5:] cust_inst.set_key( (item['type'], name), # Allow using either the normal block format, # or just providing the file - we don't use the # other values. inst['name'] if inst.has_children() else inst.value, ) # Look in the Inputs and Outputs blocks to find the io definitions. # Copy them to property names like 'Input_Activate'. for io_type in ('Inputs', 'Outputs'): for block in item.find_all('Exporting', io_type, CONN_NORM): for io_prop in block: comm_block[ io_type[:-1] + '_' + io_prop.real_name ] = io_prop.value # The funnel item type is special, having the additional input type. # Handle that specially. if item['type'].casefold() == 'item_tbeam': for block in item.find_all('Exporting', 'Inputs', CONN_FUNNEL): for io_prop in block: comm_block['TBeam_' + io_prop.real_name] = io_prop.value # Fizzlers don't work correctly with outputs. This is a signal to # conditions.fizzler, but it must be removed in editoritems. if item['ItemClass', ''].casefold() == 'itembarrierhazard': for block in item.find_all('Exporting', 'Outputs'): if CONN_NORM in block: del block[CONN_NORM] # Record the itemClass for each item type. item_classes[item['type']] = item['ItemClass', 'ItemBase'] # Only add the block if the item actually has IO. if comm_block.value: commands.append(comm_block) return root_block.export()
def export( self, style: packages.Style, selected_objects: dict, should_refresh=False, ) -> Tuple[bool, bool]: """Generate the editoritems.txt and vbsp_config. - If no backup is present, the original editoritems is backed up. - For each object type, run its .export() function with the given - item. - Styles are a special case. """ LOGGER.info('-' * 20) LOGGER.info('Exporting Items and Style for "{}"!', self.name) LOGGER.info('Style = {}', style.id) for obj, selected in selected_objects.items(): # Skip the massive dict in items if obj == 'Item': selected = selected[0] LOGGER.info('{} = {}', obj, selected) # VBSP, VRAD, editoritems export_screen.set_length('BACK', len(FILES_TO_BACKUP)) # files in compiler/ try: num_compiler_files = sum( 1 for file in utils.install_path('compiler').rglob('*')) except FileNotFoundError: num_compiler_files = 0 if self.steamID == utils.STEAM_IDS['APERTURE TAG']: # Coop paint gun instance num_compiler_files += 1 if num_compiler_files == 0: LOGGER.warning('No compiler files!') export_screen.skip_stage('COMP') else: export_screen.set_length('COMP', num_compiler_files) LOGGER.info('Should refresh: {}', should_refresh) if should_refresh: # Check to ensure the cache needs to be copied over.. should_refresh = self.cache_invalid() if should_refresh: LOGGER.info("Cache invalid - copying..") else: LOGGER.info("Skipped copying cache!") # Each object type # Editoritems # VBSP_config # Instance list # Editor models. # FGD file # Gameinfo export_screen.set_length('EXP', len(packages.OBJ_TYPES) + 6) # Do this before setting music and resources, # those can take time to compute. export_screen.show() try: if should_refresh: # Count the files. export_screen.set_length( 'RES', sum(1 for file in res_system.walk_folder_repeat()), ) else: export_screen.skip_stage('RES') export_screen.skip_stage('MUS') # Make the folders we need to copy files to, if desired. os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True) # Start off with the style's data. vbsp_config = Property(None, []) vbsp_config += style.config.copy() all_items = style.items.copy() renderables = style.renderables.copy() export_screen.step('EXP') vpk_success = True # Export each object type. for obj_name, obj_data in packages.OBJ_TYPES.items(): if obj_name == 'Style': continue # Done above already LOGGER.info('Exporting "{}"', obj_name) selected = selected_objects.get(obj_name, None) try: obj_data.cls.export( packages.ExportData( game=self, selected=selected, all_items=all_items, renderables=renderables, vbsp_conf=vbsp_config, selected_style=style, )) except packages.NoVPKExport: # Raised by StyleVPK to indicate it failed to copy. vpk_success = False export_screen.step('EXP') vbsp_config.set_key(('Options', 'Game_ID'), self.steamID) vbsp_config.set_key( ('Options', 'dev_mode'), srctools.bool_as_int(optionWindow.DEV_MODE.get())) # If there are multiple of these blocks, merge them together. # They will end up in this order. vbsp_config.merge_children( 'Textures', 'Fizzlers', 'Options', 'StyleVars', 'DropperItems', 'Conditions', 'Quotes', '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 not os.path.isfile(item_path): # We can't backup at all. should_backup = False elif name == 'Editoritems': should_backup = not os.path.isfile(backup_path) else: # Always backup the non-_original file, it'd be newer. # But only if it's Valves - not our own. should_backup = should_backup_app(item_path) backup_is_good = should_backup_app(backup_path) LOGGER.info( '{}{}: normal={}, backup={}', file, ext, 'Valve' if should_backup else 'BEE2', 'Valve' if backup_is_good else 'BEE2', ) if not should_backup and not backup_is_good: # It's a BEE2 application, we have a problem. # Both the real and backup are bad, we need to get a # new one. try: os.remove(backup_path) except FileNotFoundError: pass try: os.remove(item_path) except FileNotFoundError: pass export_screen.reset() if messagebox.askokcancel( title=_('BEE2 - Export Failed!'), message=_( 'Compiler file {file} missing. ' 'Exit Steam applications, then press OK ' 'to verify your game cache. You can then ' 'export again.').format(file=file + ext, ), master=TK_ROOT, ): webbrowser.open('steam://validate/' + str(self.steamID)) return False, vpk_success if should_backup: LOGGER.info('Backing up original {}!', name) shutil.copy(item_path, backup_path) export_screen.step('BACK') # Backup puzzles, if desired backup.auto_backup(selected_game, export_screen) # Special-case: implement the UnlockDefault stlylevar here, # so all items are modified. if selected_objects['StyleVar']['UnlockDefault']: LOGGER.info('Unlocking Items!') for i, item in enumerate(all_items): # 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.id in _UNLOCK_ITEMS: all_items[i] = copy.copy(item) item.deletable = item.copiable = True item.facing = editoritems.DesiredFacing.UP LOGGER.info('Editing Gameinfo...') self.edit_gameinfo(True) export_screen.step('EXP') if not GEN_OPTS.get_bool('General', 'preserve_bee2_resource_dir'): LOGGER.info('Adding ents to FGD.') self.edit_fgd(True) export_screen.step('EXP') # AtomicWriter writes to a temporary file, then renames in one step. # This ensures editoritems won't be half-written. LOGGER.info('Writing Editoritems script...') with srctools.AtomicWriter( self.abs_path('portal2_dlc2/scripts/editoritems.txt') ) as editor_file: editoritems.Item.export(editor_file, all_items, renderables) export_screen.step('EXP') LOGGER.info('Writing Editoritems database...') with open(self.abs_path('bin/bee2/editor.bin'), 'wb') as inst_file: pick = pickletools.optimize(pickle.dumps(all_items)) inst_file.write(pick) export_screen.step('EXP') LOGGER.info('Writing VBSP Config!') os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True) with open(self.abs_path('bin/bee2/vbsp_config.cfg'), 'w', encoding='utf8') as vbsp_file: for line in vbsp_config.export(): vbsp_file.write(line) export_screen.step('EXP') if num_compiler_files > 0: LOGGER.info('Copying Custom Compiler!') compiler_src = utils.install_path('compiler') for comp_file in compiler_src.rglob('*'): # Ignore folders. if comp_file.is_dir(): continue dest = self.abs_path('bin' / comp_file.relative_to(compiler_src)) LOGGER.info('\t* {} -> {}', comp_file, dest) folder = Path(dest).parent if not folder.exists(): folder.mkdir(parents=True, exist_ok=True) try: if os.path.isfile(dest): # First try and give ourselves write-permission, # if it's set read-only. utils.unset_readonly(dest) shutil.copy(comp_file, dest) except PermissionError: # We might not have permissions, if the compiler is currently # running. export_screen.reset() messagebox.showerror( title=_('BEE2 - Export Failed!'), message=_('Copying compiler file {file} failed. ' 'Ensure {game} is not running.').format( file=comp_file, game=self.name, ), master=TK_ROOT, ) return False, vpk_success export_screen.step('COMP') if should_refresh: LOGGER.info('Copying Resources!') music_files = self.copy_mod_music() self.refresh_cache(music_files) LOGGER.info('Optimizing editor models...') self.clean_editor_models(all_items) export_screen.step('EXP') self.generate_fizzler_sides(vbsp_config) if self.steamID == utils.STEAM_IDS['APERTURE TAG']: os.makedirs(self.abs_path('sdk_content/maps/instances/bee2/'), exist_ok=True) with open( self.abs_path( 'sdk_content/maps/instances/bee2/tag_coop_gun.vmf' ), 'w') as f: TAG_COOP_INST_VMF.export(f) export_screen.reset() # Hide loading screen, we're done return True, vpk_success except loadScreen.Cancelled: return False, False