Example #1
0
    def export(
        self,
        style: packageLoader.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 lists and dicts etc - too long
            if selected is None or isinstance(selected, str):
                LOGGER.info('{} = {}', obj, selected)

        # VBSP, VRAD, editoritems
        export_screen.set_length('BACK', len(FILES_TO_BACKUP))
        # files in compiler/
        try:
            num_compiler_files = len(os.listdir('../compiler'))
        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!")

        # The items, plus editoritems, vbsp_config and the instance list.
        export_screen.set_length('EXP', len(packageLoader.OBJ_TYPES) + 3)

        # Do this before setting music and resources,
        # those can take time to compute.

        export_screen.show()
        export_screen.grab_set_global()  # Stop interaction with other windows

        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.
        editoritems, vbsp_config = style.export()
        export_screen.step('EXP')

        vpk_success = True

        # Export each object type.
        for obj_name, obj_data in packageLoader.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(packageLoader.ExportData(
                    game=self,
                    selected=selected,
                    editoritems=editoritems,
                    vbsp_conf=vbsp_config,
                    selected_style=style,
                ))
            except packageLoader.NoVPKExport:
                # Raised by StyleVPK to indicate it failed to copy.
                vpk_success = False

            export_screen.step('EXP')

        vbsp_config.set_key(
            ('Options', 'BEE2_loc'),
            os.path.dirname(os.getcwd())  # Go up one dir to our actual location
        )
        vbsp_config.set_key(
            ('Options', 'Game_ID'),
            self.steamID,
        )

        # 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):
                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)

        # This is the connection "heart" and "error" models.
        # These have to come last, so we need to special case it.
        editoritems += style.editor.find_key("Renderables", []).copy()

        # Special-case: implement the UnlockDefault stlylevar here,
        # so all items are modified.
        if selected_objects['StyleVar']['UnlockDefault']:
            LOGGER.info('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'

        LOGGER.info('Editing Gameinfo!')
        self.edit_gameinfo(True)

        LOGGER.info('Writing instance list!')
        with open(self.abs_path('bin/bee2/instances.cfg'), 'w', encoding='utf8') as inst_file:
            for line in self.build_instance_data(editoritems):
                inst_file.write(line)
        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!')
        with srctools.AtomicWriter(self.abs_path(
                'portal2_dlc2/scripts/editoritems.txt')) as editor_file:
            for line in editoritems.export():
                editor_file.write(line)
        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!')
            for file in os.listdir('../compiler'):
                src_path = os.path.join('../compiler', file)
                if not os.path.isfile(src_path):
                    continue

                dest = self.abs_path('bin/' + file)

                LOGGER.info('\t* compiler/{0} -> bin/{0}', file)

                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(
                        src_path,
                        self.abs_path('bin/')
                    )
                except PermissionError:
                    # We might not have permissions, if the compiler is currently
                    # running.
                    export_screen.grab_release()
                    export_screen.reset()
                    messagebox.showerror(
                        title=_('BEE2 - Export Failed!'),
                        message=_('Copying compiler file {file} failed.'
                                  'Ensure the {game} is not running.').format(
                                    file=file,
                                    game=self.name,
                                ),
                        master=TK_ROOT,
                    )
                    return False, vpk_success
                export_screen.step('COMP')


        if should_refresh:
            LOGGER.info('Copying Resources!')
            self.refresh_cache()

            self.copy_mod_music()

        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.grab_release()
        export_screen.reset()  # Hide loading screen, we're done
        return True, vpk_success
Example #2
0
    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.
        -
        """
        LOGGER.info('-' * 20)
        LOGGER.info('Exporting Items and Style for "{}"!', self.name)
        LOGGER.info('Style = {}', style)
        LOGGER.info('Music = {}', music)
        LOGGER.info('Voice = {}', voice)
        LOGGER.info('Skybox = {}', skybox)
        LOGGER.info('Elevator = {}', elevator)
        LOGGER.info('Style Vars:')
        LOGGER.info('  {')
        for key, val in style_vars.items():
            LOGGER.info('  {} = {!s}', key, val)
        LOGGER.info('  }')
        LOGGER.info('{} Pack Lists!', len(pack_list))
        LOGGER.info('{} Editor Sounds!', len(editor_sounds))
        LOGGER.info('-' * 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, template VMF
            7 +
            # 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,
                )
            if music.packfiles:
                vbsp_config.set_key(
                    ('PackTriggers', 'Forced'),
                    [
                        Property('File', file)
                        for file in
                        music.packfiles
                    ],
                )

            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)
            )
            if voice.cave_skin is not None:
                vbsp_config.set_key(
                    ('Options', 'cave_port_skin'),
                    voice.cave_skin,
                )

        vbsp_config.set_key(
            ('Options', 'BEE2_loc'),
            os.path.dirname(os.getcwd())  # Go up one dir to our actual location
        )
        vbsp_config.set_key(
            ('Options', 'Game_ID'),
            self.steamID,
        )

        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):
                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)

        # This is the connections "heart" icon and "error" icon
        editoritems += style.editor.find_key("Renderables", [])


        if style_vars.get('UnlockDefault', False):
            LOGGER.info('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'

        LOGGER.info('Editing Gameinfo!')
        self.edit_gameinfo(True)

        export_screen.step('CONF')

        LOGGER.info('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')

        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') as vbsp_file:
            for line in vbsp_config.export():
                vbsp_file.write(line)
        export_screen.step('CONF')

        LOGGER.info('Writing instance list!')
        with open(self.abs_path('bin/bee2/instances.cfg'), 'w') as inst_file:
            for line in self.build_instance_data(editoritems):
                inst_file.write(line)
        export_screen.step('CONF')

        LOGGER.info('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')

        LOGGER.info('Editing game_sounds!')
        self.add_editor_sounds(editor_sounds.values())
        export_screen.step('CONF')

        LOGGER.info('Exporting {} templates!',
            len(packageLoader.data['BrushTemplate'])
        )
        with open(self.abs_path('bin/bee2/templates.vmf'), 'w') as temp_file:
            packageLoader.TEMPLATE_FILE.export(temp_file)
        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',
                )
                LOGGER.info(path)
                if os.path.isfile(path):
                    shutil.copy(
                        path,
                        self.abs_path('bin/bee2/{}voice.cfg'.format(dest))
                    )
                    LOGGER.info('Written "{}voice.cfg"', dest)
                else:
                    LOGGER.info('No {} voice config!', pretty)
                export_screen.step('CONF')

        LOGGER.info('Copying Custom Compiler!')
        for file in os.listdir('../compiler'):
            LOGGER.info('\t* compiler/{0} -> bin/{0}', file)
            try:
                shutil.copy(
                    os.path.join('../compiler', file),
                    self.abs_path('bin/')
                )
            except PermissionError:
                # We might not have permissions, if the compiler is currently
                # running.
                export_screen.grab_release()
                export_screen.reset()
                messagebox.showerror(
                    title='BEE2 - Export Failed!',
                    message='Copying compiler file {file} failed.'
                            'Ensure the {game} is not running.'.format(
                                file=file,
                                game=self.name,
                            ),
                    master=TK_ROOT,
                )
                return False
            export_screen.step('COMP')

        if should_refresh:
            LOGGER.info('Copying Resources!')
            self.refresh_cache()

        export_screen.grab_release()
        export_screen.reset()  # Hide loading screen, we're done
        return True
Example #3
0
    def export(
            self,
            style: packageLoader.Style,
            selected_objects: dict,
            should_refresh=False,
            ):
        """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 lists and dicts etc - too long
            if selected is None or isinstance(selected, str):
                LOGGER.info('{} = {}', obj, selected)

        # VBSP, VRAD, editoritems
        export_screen.set_length('BACK', len(FILES_TO_BACKUP))
        # 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')

        # The items, plus editoritems, vbsp_config and the instance list.
        export_screen.set_length('EXP', len(packageLoader.OBJ_TYPES) + 3)

        export_screen.show()
        export_screen.grab_set_global()  # Stop interaction with other windows

        # 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.
        editoritems, vbsp_config = style.export()
        export_screen.step('EXP')

        # Export each object type.
        for obj_name, obj_data in packageLoader.OBJ_TYPES.items():
            if obj_name == 'Style':
                continue  # Done above already

            LOGGER.info('Exporting "{}"', obj_name)
            selected = selected_objects.get(obj_name, None)

            LOGGER.debug('Name: {}, selected: {}', obj_name, selected)

            obj_data.cls.export(packageLoader.ExportData(
                game=self,
                selected=selected,
                editoritems=editoritems,
                vbsp_conf=vbsp_config,
                selected_style=style,
            ))
            export_screen.step('EXP')

        vbsp_config.set_key(
            ('Options', 'BEE2_loc'),
            os.path.dirname(os.getcwd())  # Go up one dir to our actual location
        )
        vbsp_config.set_key(
            ('Options', 'Game_ID'),
            self.steamID,
        )

        # 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):
                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)

        # This is the connection "heart" and "error" models.
        # These have to come last, so we need to special case it.
        editoritems += style.editor.find_key("Renderables", [])

        # Special-case: implement the UnlockDefault stlylevar here,
        # so all items are modified.
        if selected_objects['StyleVar']['UnlockDefault']:
            LOGGER.info('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'

        LOGGER.info('Editing Gameinfo!')
        self.edit_gameinfo(True)

        LOGGER.info('Writing instance list!')
        with open(self.abs_path('bin/bee2/instances.cfg'), 'w') as inst_file:
            for line in self.build_instance_data(editoritems):
                inst_file.write(line)
        export_screen.step('EXP')

        LOGGER.info('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('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') as vbsp_file:
            for line in vbsp_config.export():
                vbsp_file.write(line)
        export_screen.step('EXP')

        LOGGER.info('Copying Custom Compiler!')
        for file in os.listdir('../compiler'):
            src_path = os.path.join('../compiler', file)
            if not os.path.isfile(src_path):
                continue

            LOGGER.info('\t* compiler/{0} -> bin/{0}', file)
            try:
                shutil.copy(
                    src_path,
                    self.abs_path('bin/')
                )
            except PermissionError:
                # We might not have permissions, if the compiler is currently
                # running.
                export_screen.grab_release()
                export_screen.reset()
                messagebox.showerror(
                    title='BEE2 - Export Failed!',
                    message='Copying compiler file {file} failed.'
                            'Ensure the {game} is not running.'.format(
                                file=file,
                                game=self.name,
                            ),
                    master=TK_ROOT,
                )
                return False
            export_screen.step('COMP')

        if should_refresh:
            LOGGER.info('Copying Resources!')
            self.refresh_cache()

        export_screen.grab_release()
        export_screen.reset()  # Hide loading screen, we're done
        return True
Example #4
0
    def export(self, style: packageLoader.Style, selected_objects: dict, should_refresh=False):
        """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 lists and dicts etc - too long
            if selected is None or isinstance(selected, str):
                LOGGER.info("{} = {}", obj, selected)

        # VBSP, VRAD, editoritems
        export_screen.set_length("BACK", len(FILES_TO_BACKUP))
        # files in compiler/
        try:
            num_compiler_files = len(os.listdir("../compiler"))
        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 = not self.cache_valid()

        if should_refresh:
            export_screen.set_length("RES", extract_packages.res_count)
        else:
            export_screen.skip_stage("RES")
            export_screen.skip_stage("MUS")

        # The items, plus editoritems, vbsp_config and the instance list.
        export_screen.set_length("EXP", len(packageLoader.OBJ_TYPES) + 3)

        export_screen.show()
        export_screen.grab_set_global()  # Stop interaction with other windows

        # 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.
        editoritems, vbsp_config = style.export()
        export_screen.step("EXP")

        # Export each object type.
        for obj_name, obj_data in packageLoader.OBJ_TYPES.items():
            if obj_name == "Style":
                continue  # Done above already

            LOGGER.info('Exporting "{}"', obj_name)
            selected = selected_objects.get(obj_name, None)

            obj_data.cls.export(
                packageLoader.ExportData(
                    game=self, selected=selected, editoritems=editoritems, vbsp_conf=vbsp_config, selected_style=style
                )
            )
            export_screen.step("EXP")

        vbsp_config.set_key(
            ("Options", "BEE2_loc"), os.path.dirname(os.getcwd())  # Go up one dir to our actual location
        )
        vbsp_config.set_key(("Options", "Game_ID"), self.steamID)

        # 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):
                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)

        # This is the connection "heart" and "error" models.
        # These have to come last, so we need to special case it.
        editoritems += style.editor.find_key("Renderables", []).copy()

        # Special-case: implement the UnlockDefault stlylevar here,
        # so all items are modified.
        if selected_objects["StyleVar"]["UnlockDefault"]:
            LOGGER.info("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"

        LOGGER.info("Editing Gameinfo!")
        self.edit_gameinfo(True)

        LOGGER.info("Writing instance list!")
        with open(self.abs_path("bin/bee2/instances.cfg"), "w") as inst_file:
            for line in self.build_instance_data(editoritems):
                inst_file.write(line)
        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!")
        with srctools.AtomicWriter(self.abs_path("portal2_dlc2/scripts/editoritems.txt")) as editor_file:
            for line in editoritems.export():
                editor_file.write(line)
        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") 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!")
            for file in os.listdir("../compiler"):
                src_path = os.path.join("../compiler", file)
                if not os.path.isfile(src_path):
                    continue

                dest = self.abs_path("bin/" + file)

                LOGGER.info("\t* compiler/{0} -> bin/{0}", file)

                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(src_path, self.abs_path("bin/"))
                except PermissionError:
                    # We might not have permissions, if the compiler is currently
                    # running.
                    export_screen.grab_release()
                    export_screen.reset()
                    messagebox.showerror(
                        title=_("BEE2 - Export Failed!"),
                        message=_("Copying compiler file {file} failed." "Ensure the {game} is not running.").format(
                            file=file, game=self.name
                        ),
                        master=TK_ROOT,
                    )
                    return False
                export_screen.step("COMP")

        if should_refresh:
            LOGGER.info("Copying Resources!")
            self.refresh_cache()

            self.copy_mod_music()

        if self.steamID == utils.STEAM_IDS["APERTURE TAG"]:
            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.grab_release()
        export_screen.reset()  # Hide loading screen, we're done
        return True