Esempio n. 1
0
def clear_caches():
    """Wipe the cache times in configs.

     This will force package resources to be extracted again.
     """
    import gameMan
    import packageLoader

    restart_ok = messagebox.askokcancel(
        title='Allow Restart?',
        message='Restart the BEE2 to re-extract packages?',
    )

    if not restart_ok:
        return

    for game in gameMan.all_games:
        game.mod_time = 0
        game.save()
    GEN_OPTS['General']['cache_time'] = '0'

    for pack_id in packageLoader.packages:
        packageLoader.PACK_CONFIG[pack_id]['ModTime'] = '0'

    save()  # Save any option changes..

    gameMan.CONFIG.save_check()
    GEN_OPTS.save_check()
    packageLoader.PACK_CONFIG.save_check()

    utils.restart_app()
Esempio n. 2
0
    def refresh_cache(self):
        """Copy over the resource files into this game."""

        screen_func = export_screen.step
        copy = shutil.copy

        def copy_func(src, dest):
            screen_func("RES")
            copy(src, dest)

        for folder in os.listdir("../cache/resources/"):
            source = os.path.join("../cache/resources/", folder)
            if not os.path.isdir(source):
                continue  # Skip DS_STORE, desktop.ini, etc.

            if folder == "instances":
                dest = self.abs_path(INST_PATH)
            elif folder.casefold() == "bee2":
                continue  # Skip app icons
            else:
                dest = self.abs_path(os.path.join("bee2", folder))
            LOGGER.info('Copying to "{}" ...', dest)
            try:
                shutil.rmtree(dest)
            except (IOError, shutil.Error):
                pass

            # This handles existing folders, without raising in os.makedirs().
            utils.merge_tree(source, dest, copy_function=copy_func)
        LOGGER.info("Cache copied.")
        # Save the new cache modification date.
        self.mod_time = GEN_OPTS.get_int("General", "cache_time", 0)
        self.save()
        CONFIG.save_check()
Esempio n. 3
0
 def load_after_export():
     """Read the 'After Export' radio set."""
     AFTER_EXPORT_ACTION.set(GEN_OPTS.get_int(
         'General',
         'after_export_action',
         AFTER_EXPORT_ACTION.get()
     ))
Esempio n. 4
0
def check_cache(zip_list):
    """Check to see if any zipfiles are invalid, and if so extract the cache."""
    global copy_process
    from BEE2_config import GEN_OPTS

    LOGGER.info('Checking cache...')

    cache_packs = GEN_OPTS.get_int('General', 'cache_pack_count')

    # We need to match the number of packages too, to account for removed ones.
    cache_stale = (len(packageLoader.packages) == cache_packs) or any(
        pack.is_stale() for pack in packageLoader.packages.values())

    if not cache_stale:
        # We've already done the copying..
        LOGGER.info('Cache is still fresh, skipping extraction')
        done_callback()
        return

    copy_process = multiprocessing.Process(
        target=do_copy,
        args=(zip_list, currently_done),
    )
    copy_process.daemon = True
    LOGGER.info('Starting background extraction process!')
    copy_process.start()
    TK_ROOT.after(UPDATE_INTERVAL, update)
Esempio n. 5
0
    def load_conf(self) -> None:
        """Load configuration from our config file."""
        try:
            if self.can_resize_x:
                width = int(GEN_OPTS['win_state'][self.win_name + '_width'])
            else:
                width = self.winfo_reqwidth()
            if self.can_resize_y:
                height = int(GEN_OPTS['win_state'][self.win_name + '_height'])
            else:
                height = self.winfo_reqheight()
            self.deiconify()

            self.geometry('{!s}x{!s}'.format(width, height))
            self.sizefrom('user')

            self.relX = int(GEN_OPTS['win_state'][self.win_name + '_x'])
            self.relY = int(GEN_OPTS['win_state'][self.win_name + '_y'])

            self.follow_main()
            self.positionfrom('user')
        except (ValueError, KeyError):
            pass
        if not GEN_OPTS.get_bool('win_state', self.win_name + '_visible',
                                 True):
            self.after(150, self.hide_win)

        # Prevent this until here, so the <config> event won't erase our
        #  settings
        self.can_save = True
Esempio n. 6
0
    def refresh_cache(self):
        """Copy over the resource files into this game."""

        screen_func = export_screen.step
        copy = shutil.copy

        def copy_func(src, dest):
            screen_func('RES')
            copy(src, dest)

        for folder in os.listdir('../cache/resources/'):
            source = os.path.join('../cache/resources/', folder)
            if not os.path.isdir(source):
                continue  # Skip DS_STORE, desktop.ini, etc.

            if folder == 'instances':
                dest = self.abs_path(INST_PATH)
            elif folder.casefold() == 'bee2':
                continue  # Skip app icons
            else:
                dest = self.abs_path(os.path.join('bee2', folder))
            LOGGER.info('Copying to "{}" ...', dest)
            try:
                shutil.rmtree(dest)
            except (IOError, shutil.Error):
                pass

            # This handles existing folders, without raising in os.makedirs().
            utils.merge_tree(source, dest, copy_function=copy_func)
        LOGGER.info('Cache copied.')
        # Save the new cache modification date.
        self.mod_time = GEN_OPTS.get_int('General', 'cache_time', 0)
        self.save()
        CONFIG.save_check()
Esempio n. 7
0
 def load_after_export():
     """Read the 'After Export' radio set."""
     AFTER_EXPORT_ACTION.set(GEN_OPTS.get_int(
         'General',
         'after_export_action',
         AFTER_EXPORT_ACTION.get()
     ))
Esempio n. 8
0
    def refresh_cache(self):
        """Copy over the resource files into this game."""

        screen_func = export_screen.step
        copy2 = shutil.copy2

        def copy_func(src, dest):
            screen_func('RES')
            copy2(src, dest)

        for folder in os.listdir('../cache/resources/'):
            source = os.path.join('../cache/resources/', folder)
            if folder == 'instances':
                dest = self.abs_path(INST_PATH)
            elif folder.casefold() == 'bee2':
                continue  # Skip app icons
            else:
                dest = self.abs_path(os.path.join('bee2', folder))
            LOGGER.info('Copying to "{}" ...', dest)
            try:
                shutil.rmtree(dest)
            except (IOError, shutil.Error):
                pass

            shutil.copytree(source, dest, copy_function=copy_func)
        LOGGER.info('Cache copied.')
        # Save the new cache modification date.
        self.mod_time = GEN_OPTS.get_int('General', 'cache_time', 0)
        self.save()
        CONFIG.save_check()
Esempio n. 9
0
 def load_opt():
     """Load the checkbox's values."""
     var.set(GEN_OPTS.get_bool(
         section,
         item,
         default,
     ))
Esempio n. 10
0
def check_cache(zip_list):
    """Check to see if any zipfiles are invalid, and if so extract the cache."""
    global copy_process
    from BEE2_config import GEN_OPTS

    LOGGER.info('Checking cache...')

    cache_packs = GEN_OPTS.get_int('General', 'cache_pack_count')

    # We need to match the number of packages too, to account for removed ones.
    cache_stale = (len(packageLoader.packages) == cache_packs) or any(
        pack.is_stale()
        for pack in
        packageLoader.packages.values()
    )

    if not cache_stale:
        # We've already done the copying..
        LOGGER.info('Cache is still fresh, skipping extraction')
        done_callback()
        return

    copy_process = multiprocessing.Process(
        target=do_copy,
        args=(zip_list, currently_done),
    )
    copy_process.daemon = True
    LOGGER.info('Starting background extraction process!')
    copy_process.start()
    TK_ROOT.after(UPDATE_INTERVAL, update)
 def load_opt():
     """Load the checkbox's values."""
     var.set(GEN_OPTS.get_bool(
         section,
         item,
         default,
     ))
def on_error(exc_type, exc_value, exc_tb):
    """Run when the application crashes. Display to the user, log it, and quit."""
    # We don't want this to fail, so import everything here, and wrap in
    # except Exception.
    import traceback

    # Close loading screens if they're visible..
    try:
        import loadScreen
        loadScreen.close_all()
    except Exception:
        pass

    err = ''.join(traceback.format_exception(exc_type, exc_value, exc_tb))

    # Grab and release the grab so nothing else can block the error message.
    try:
        TK_ROOT.grab_set_global()
        TK_ROOT.grab_release()

        # Append traceback to the clipboard.
        TK_ROOT.clipboard_append(err)
    except Exception:
        pass

    # Put it onscreen.
    try:
        from tkinter import messagebox
        messagebox.showinfo(
            title='BEE2 Error!',
            message='An error occurred: \n{}\n\nThis has '
                    'been copied to the clipboard.'.format(err),
            icon=messagebox.ERROR,
        )
    except Exception:
        pass

    try:
        from BEE2_config import GEN_OPTS
        # Try to turn on the logging window for next time..
        GEN_OPTS.load()
        GEN_OPTS['Debug']['show_log_win'] = '1'
        GEN_OPTS['Debug']['window_log_level'] = 'DEBUG'
        GEN_OPTS.save()
    except Exception:
        # Ignore failures...
        pass
Esempio n. 13
0
    def cache_valid(self):
        """Check to see if the cache is valid."""
        cache_time = GEN_OPTS.get_int('General', 'cache_time', 0)

        if cache_time == self.mod_time:
            LOGGER.info("Skipped copying cache!")
            return True
        LOGGER.info("Cache invalid - copying..")
        return False
Esempio n. 14
0
    def cache_valid(self):
        """Check to see if the cache is valid."""
        cache_time = GEN_OPTS.get_int('General', 'cache_time', 0)

        if cache_time == self.mod_time:
            LOGGER.info("Skipped copying cache!")
            return True
        LOGGER.info("Cache invalid - copying..")
        return False
Esempio n. 15
0
def get_package(file: Path) -> RawFileSystem:
    """Get the package desired for a file."""
    global PACKAGE_REPEAT
    last_package = GEN_OPTS.get_val('Last_Selected', 'Package_sync_id', '')
    if last_package:
        if PACKAGE_REPEAT is not None:
            return PACKAGE_REPEAT

        message = ('Choose package ID for "{}", or '
                   'blank to assume {}: '.format(file, last_package))
    else:
        message = 'Choose package ID for "{}": '.format(file)

    error_message = 'Invalid package!\n' + message

    while True:
        pack_id = input(message)

        # After first time, always use the 'invalid package' warning.
        message = error_message

        if pack_id == '*' and last_package:
            try:
                fsys = PACKAGES[last_package].fsys
            except KeyError:
                continue
            if isinstance(fsys, RawFileSystem):
                PACKAGE_REPEAT = fsys
                return PACKAGE_REPEAT
            else:
                print('Packages must be folders, not zips!')
        elif not pack_id and last_package:
            pack_id = last_package

        try:
            fsys = PACKAGES[pack_id.casefold()].fsys
        except KeyError:
            continue
        if isinstance(fsys, RawFileSystem):
            GEN_OPTS['Last_Selected']['Package_sync_id'] = pack_id
            GEN_OPTS.save_check()
            return fsys
        else:
            print('Packages must be folders, not zips!')
Esempio n. 16
0
    def clean_editor_models(self, items: Iterable[editoritems.Item]) -> None:
        """The game is limited to having 1024 models loaded at once.

        Editor models are always being loaded, so we need to keep the number
        small. Go through editoritems, and disable (by renaming to .mdl_dis)
        unused ones.
        """
        # If set, force them all to be present.
        force_on = GEN_OPTS.get_bool('Debug', 'force_all_editor_models')

        used_models = {
            str(mdl.with_suffix('')).casefold()
            for item in items for subtype in item.subtypes
            for mdl in subtype.models
        }

        mdl_count = 0

        for mdl_folder in [
                self.abs_path('bee2/models/props_map_editor/'),
                self.abs_path('bee2_dev/models/props_map_editor/'),
        ]:
            if not os.path.exists(mdl_folder):
                continue
            for file in os.listdir(mdl_folder):
                if not file.endswith(('.mdl', '.mdl_dis')):
                    continue

                mdl_count += 1

                file_no_ext, ext = os.path.splitext(file)
                if force_on or file_no_ext.casefold() in used_models:
                    new_ext = '.mdl'
                else:
                    new_ext = '.mdl_dis'

                if new_ext != ext:
                    try:
                        os.remove(
                            os.path.join(mdl_folder, file_no_ext + new_ext))
                    except FileNotFoundError:
                        pass
                    os.rename(
                        os.path.join(mdl_folder, file_no_ext + ext),
                        os.path.join(mdl_folder, file_no_ext + new_ext),
                    )

        if mdl_count != 0:
            LOGGER.info(
                '{}/{} ({:.0%}) editor models used.',
                len(used_models),
                mdl_count,
                len(used_models) / mdl_count,
            )
        else:
            LOGGER.warning('No custom editor models!')
Esempio n. 17
0
def add_vars(data):
    """
    Add the given stylevars to our list.

    """
    global var_list
    var_list = sorted(data, key=operator.attrgetter('id'))

    for var in var_list:
        var.default = GEN_OPTS.get_bool('StyleVar', var.id, var.default)
Esempio n. 18
0
def add_vars(data):
    """
    Add the given stylevars to our list.

    """
    global var_list
    var_list = sorted(data, key=operator.attrgetter('id'))

    for var in var_list:
        var.default = GEN_OPTS.get_bool('StyleVar', var.id, var.default)
Esempio n. 19
0
    def clean_editor_models(self, editoritems: Property):
        """The game is limited to having 1024 models loaded at once.

        Editor models are always being loaded, so we need to keep the number
        small. Go through editoritems, and disable (by renaming to .mdl_dis)
        unused ones.
        """
        # If set, force them all to be present.
        force_on = GEN_OPTS.get_bool('Debug', 'force_all_editor_models')

        used_models = {
            mdl.value.rsplit('.', 1)[0].casefold()
            for mdl in editoritems.find_all(
                'Item',
                'Editor',
                'Subtype',
                'Model',
                'ModelName',
            )
        }

        mdl_count = 0

        for mdl_folder in [
                self.abs_path('bee2/models/props_map_editor/'),
                self.abs_path('bee2_dev/models/props_map_editor/'),
        ]:
            if not os.path.exists(mdl_folder):
                continue
            for file in os.listdir(mdl_folder):
                if not file.endswith(('.mdl', '.mdl_dis')):
                    continue

                mdl_count += 1

                file_no_ext, ext = os.path.splitext(file)
                if force_on or file_no_ext.casefold() in used_models:
                    new_ext = '.mdl'
                else:
                    new_ext = '.mdl_dis'

                if new_ext != ext:
                    try:
                        os.remove(
                            os.path.join(mdl_folder, file_no_ext + new_ext))
                    except FileNotFoundError:
                        pass
                    os.rename(
                        os.path.join(mdl_folder, file_no_ext + ext),
                        os.path.join(mdl_folder, file_no_ext + new_ext),
                    )

        LOGGER.info('{}/{} editor models used.', len(used_models), mdl_count)
Esempio n. 20
0
def clear_caches() -> None:
    """Wipe the cache times in configs.

     This will force package resources to be extracted again.
     """
    import gameMan
    import packageLoader

    message = _(
        'Package cache times have been reset. '
        'These will now be extracted during the next export.'
    )

    for game in gameMan.all_games:
        game.mod_times.clear()
        game.save()
    GEN_OPTS['General']['cache_time'] = '0'

    for pack_id in packageLoader.packages:
        packageLoader.PACK_CONFIG[pack_id]['ModTime'] = '0'

    # This needs to be disabled, since otherwise we won't actually export
    # anything...
    if PRESERVE_RESOURCES.get():
        PRESERVE_RESOURCES.set(False)
        message += '\n\n' + _('"Preserve Game Resources" has been disabled.')

    save()  # Save any option changes..

    gameMan.CONFIG.save_check()
    GEN_OPTS.save_check()
    packageLoader.PACK_CONFIG.save_check()

    # Since we've saved, dismiss this window.
    win.withdraw()
    
    messagebox.showinfo(
        title=_('Packages Reset'),
        message=message,
    )
Esempio n. 21
0
    def _send_msg(self, command, *args):
        """Send a message to the daemon."""
        _PIPE_MAIN_SEND.send((command, id(self), args))
        # Check the messages coming back as well.
        while _PIPE_MAIN_REC.poll():
            command, arg = _PIPE_MAIN_REC.recv()
            if command == 'main_set_compact':
                # Save the compact state to the config.
                GEN_OPTS['General']['compact_splash'] = '1' if arg else '0'
                GEN_OPTS.save_check()
            elif command == 'cancel':
                # Mark this loadscreen as cancelled.
                _SCREEN_CANCEL_FLAG[arg] = True
            else:
                raise ValueError('Bad command from daemon: ' + repr(command))

        # If the flag was set for us, raise an exception - the loading thing
        # will then stop.
        if _SCREEN_CANCEL_FLAG[id(self)]:
            _SCREEN_CANCEL_FLAG[id(self)] = False
            LOGGER.info('User cancelled loading screen.')
            raise Cancelled
Esempio n. 22
0
def add_vars(style_vars, styles):
    """
    Add the given stylevars to our list.

    """
    VAR_LIST.clear()
    VAR_LIST.extend(sorted(style_vars, key=operator.attrgetter('id')))

    for var in VAR_LIST:  # type: packageLoader.StyleVar
        var.enabled = GEN_OPTS.get_bool('StyleVar', var.id, var.default)

    for style in styles:
        STYLES[style.id] = style
Esempio n. 23
0
def update_modtimes():
    """Update the cache modification times, so next time we don't extract.

    This should only be done if we've copied all the files.
    """
    import time
    from BEE2_config import GEN_OPTS
    LOGGER.info('Setting modtimes..')
    for pack in packageLoader.packages.values():
        # Set modification times for each package.
        pack.set_modtime()

    # Reset package cache times for removed packages. This ensures they'll be
    # detected if re-added.
    for pak_id in packageLoader.PACK_CONFIG:
        if pak_id not in packageLoader.packages:
            packageLoader.PACK_CONFIG[pak_id]['ModTime'] = '0'

    # Set the overall cache time to now.
    GEN_OPTS['General']['cache_time'] = str(int(time.time()))
    GEN_OPTS['General']['cache_pack_count'] = str(len(packageLoader.packages))

    packageLoader.PACK_CONFIG.save()
    GEN_OPTS.save()
Esempio n. 24
0
def add_vars(style_vars, styles):
    """
    Add the given stylevars to our list.

    """
    VAR_LIST.clear()
    VAR_LIST.extend(
        sorted(style_vars, key=operator.attrgetter('id'))
    )

    for var in VAR_LIST:  # type: packageLoader.StyleVar
        var.enabled = GEN_OPTS.get_bool('StyleVar', var.id, var.default)

    for style in styles:
        STYLES[style.id] = style
Esempio n. 25
0
def update_modtimes():
    """Update the cache modification times, so next time we don't extract.

    This should only be done if we've copied all the files.
    """
    import time
    from BEE2_config import GEN_OPTS
    LOGGER.info('Setting modtimes..')
    for pack in packageLoader.packages.values():
        # Set modification times for each package.
        pack.set_modtime()

    # Reset package cache times for removed packages. This ensures they'll be
    # detected if re-added.
    for pak_id in packageLoader.PACK_CONFIG:
        if pak_id not in packageLoader.packages:
            packageLoader.PACK_CONFIG[pak_id]['ModTime'] = '0'

    # Set the overall cache time to now.
    GEN_OPTS['General']['cache_time'] = str(int(time.time()))
    GEN_OPTS['General']['cache_pack_count'] = str(len(packageLoader.packages))

    packageLoader.PACK_CONFIG.save()
    GEN_OPTS.save()
Esempio n. 26
0
    def cache_invalid(self):
        """Check to see if the cache is valid."""
        if GEN_OPTS.get_bool('General', 'preserve_bee2_resource_dir'):
            # Skipped always
            return False

        # Check lengths, to ensure we re-extract if packages were removed.
        if len(packageLoader.packages) != len(self.mod_times):
            LOGGER.info('Need to extract - package counts inconsistent!')
            return True

        if any(
                pack.is_stale(self.mod_times.get(pack_id.casefold(), 0))
                for pack_id, pack in packageLoader.packages.items()):
            return True
Esempio n. 27
0
def auto_backup(game: 'gameMan.Game', loader: LoadScreen):
    """Perform an automatic backup for the given game.

    We do this seperately since we don't need to read the property files.
    """
    from BEE2_config import GEN_OPTS
    if not GEN_OPTS.get_bool('General', 'enable_auto_backup'):
        # Don't backup!
        loader.skip_stage(AUTO_BACKUP_STAGE)
        return

    folder = find_puzzles(game)
    if not folder:
        loader.skip_stage(AUTO_BACKUP_STAGE)
        return

    # Keep this many previous
    extra_back_count = GEN_OPTS.get_int('General', 'auto_backup_count', 0)

    to_backup = os.listdir(folder)
    backup_dir = GEN_OPTS.get_val('Directories', 'backup_loc', 'backups/')

    os.makedirs(backup_dir, exist_ok=True)

    # A version of the name stripped of special characters
    # Allowed: a-z, A-Z, 0-9, '_-.'
    safe_name = utils.whitelist(
        game.name,
        valid_chars=BACKUP_CHARS,
    )

    loader.set_length(AUTO_BACKUP_STAGE, len(to_backup))

    if extra_back_count:
        back_files = [
            AUTO_BACKUP_FILE.format(game=safe_name, ind='')
        ] + [
            AUTO_BACKUP_FILE.format(game=safe_name, ind='_'+str(i+1))
            for i in range(extra_back_count)
        ]
        # Move each file over by 1 index, ignoring missing ones
        # We need to reverse to ensure we don't overwrite any zips
        for old_name, new_name in reversed(
                list(zip(back_files, back_files[1:]))
                ):
            LOGGER.info(
                'Moving: {old} -> {new}',
                old=old_name,
                new=new_name,
            )
            old_name = os.path.join(backup_dir, old_name)
            new_name = os.path.join(backup_dir, new_name)
            try:
                os.remove(new_name)
            except FileNotFoundError:
                pass  # We're overwriting this anyway
            try:
                os.rename(old_name, new_name)
            except FileNotFoundError:
                pass

    final_backup = os.path.join(
        backup_dir,
        AUTO_BACKUP_FILE.format(game=safe_name, ind=''),
    )
    LOGGER.info('Writing backup to "{}"', final_backup)
    with open(final_backup, 'wb') as f:
        with ZipFile(f, mode='w', compression=ZIP_LZMA) as zip_file:
            for file in to_backup:
                zip_file.write(
                    os.path.join(folder, file),
                    file,
                    ZIP_LZMA,
                )
                loader.step(AUTO_BACKUP_STAGE)
Esempio n. 28
0
def make_pane(tool_frame):
    """Create the styleVar pane.

    """
    global window
    window = SubPane(
        TK_ROOT,
        options=GEN_OPTS,
        title=_('Style Properties'),
        name='style',
        resize_y=True,
        tool_frame=tool_frame,
        tool_img=png.png('icons/win_stylevar'),
        tool_col=3,
    )

    UI['style_can'] = Canvas(window, highlightthickness=0)
    # need to use a canvas to allow scrolling
    UI['style_can'].grid(sticky='NSEW')
    window.rowconfigure(0, weight=1)

    UI['style_scroll'] = ttk.Scrollbar(
        window,
        orient=VERTICAL,
        command=UI['style_can'].yview,
    )
    UI['style_scroll'].grid(column=1, row=0, rowspan=2, sticky="NS")
    UI['style_can']['yscrollcommand'] = UI['style_scroll'].set

    utils.add_mousewheel(UI['style_can'], window)

    canvas_frame = ttk.Frame(UI['style_can'])

    frame_all = ttk.Labelframe(canvas_frame, text=_("All:"))
    frame_all.grid(row=0, sticky='EW')

    frm_chosen = ttk.Labelframe(canvas_frame, text=_("Selected Style:"))
    frm_chosen.grid(row=1, sticky='EW')

    ttk.Separator(
        canvas_frame,
        orient=HORIZONTAL,
    ).grid(row=2, sticky='EW', pady=(10, 5))

    frm_other = ttk.Labelframe(canvas_frame, text=_("Other Styles:"))
    frm_other.grid(row=3, sticky='EW')

    UI['stylevar_chosen_none'] = ttk.Label(
        frm_chosen,
        text=_('No Options!'),
        font='TkMenuFont',
        justify='center',
    )
    UI['stylevar_other_none'] = ttk.Label(
        frm_other,
        text=_('None!'),
        font='TkMenuFont',
        justify='center',
    )

    all_pos = 0
    for all_pos, var in enumerate(styleOptions):
        # Add the special stylevars which apply to all styles
        tk_vars[var.id] = IntVar(
            value=GEN_OPTS.get_bool('StyleVar', var.id, var.default))
        checkbox_all[var.id] = ttk.Checkbutton(frame_all,
                                               variable=tk_vars[var.id],
                                               text=var.name,
                                               command=functools.partial(
                                                   set_stylevar, var.id))
        checkbox_all[var.id].grid(row=all_pos, column=0, sticky="W", padx=3)

        tooltip.add_tooltip(
            checkbox_all[var.id],
            make_desc(var, is_hardcoded=True),
        )

    for var in VAR_LIST:
        tk_vars[var.id] = IntVar(value=var.enabled)
        args = {
            'variable': tk_vars[var.id],
            'text': var.name,
            'command': functools.partial(set_stylevar, var.id)
        }
        desc = make_desc(var)
        if var.applies_to_all():
            # Available in all styles - put with the hardcoded variables.
            all_pos += 1

            checkbox_all[var.id] = check = ttk.Checkbutton(frame_all, **args)
            check.grid(row=all_pos, column=0, sticky="W", padx=3)
            tooltip.add_tooltip(check, desc)
        else:
            # Swap between checkboxes depending on style.
            checkbox_chosen[var.id] = ttk.Checkbutton(frm_chosen, **args)
            checkbox_other[var.id] = ttk.Checkbutton(frm_other, **args)
            tooltip.add_tooltip(
                checkbox_chosen[var.id],
                desc,
            )
            tooltip.add_tooltip(
                checkbox_other[var.id],
                desc,
            )

    UI['style_can'].create_window(0, 0, window=canvas_frame, anchor="nw")
    UI['style_can'].update_idletasks()
    UI['style_can'].config(
        scrollregion=UI['style_can'].bbox(ALL),
        width=canvas_frame.winfo_reqwidth(),
    )

    if utils.USE_SIZEGRIP:
        ttk.Sizegrip(
            window,
            cursor=utils.CURSORS['stretch_vert'],
        ).grid(row=1, column=0)

    UI['style_can'].bind('<Configure>', flow_stylevar)
Esempio n. 29
0
def make_pane(tool_frame):
    """Create the styleVar pane.

    """
    global window
    window = SubPane(
        TK_ROOT,
        options=GEN_OPTS,
        title="Style Properties",
        name="style",
        resize_y=True,
        tool_frame=tool_frame,
        tool_img=png.png("icons/win_stylevar"),
        tool_col=3,
    )

    UI["style_can"] = Canvas(window, highlightthickness=0)
    # need to use a canvas to allow scrolling
    UI["style_can"].grid(sticky="NSEW")
    window.rowconfigure(0, weight=1)

    UI["style_scroll"] = ttk.Scrollbar(window, orient=VERTICAL, command=UI["style_can"].yview)
    UI["style_scroll"].grid(column=1, row=0, rowspan=2, sticky="NS")
    UI["style_can"]["yscrollcommand"] = UI["style_scroll"].set
    canvas_frame = ttk.Frame(UI["style_can"])

    frame_all = ttk.Labelframe(canvas_frame, text="All:")
    frame_all.grid(row=0, sticky="EW")

    frm_chosen = ttk.Labelframe(canvas_frame, text="Selected Style:")
    frm_chosen.grid(row=1, sticky="EW")

    ttk.Separator(canvas_frame, orient=HORIZONTAL).grid(row=2, sticky="EW", pady=(10, 5))

    frm_other = ttk.Labelframe(canvas_frame, text="Other Styles:")
    frm_other.grid(row=3, sticky="EW")

    UI["stylevar_chosen_none"] = ttk.Label(frm_chosen, text="No Options!", font="TkMenuFont", justify="center")
    UI["stylevar_other_none"] = ttk.Label(frm_other, text="None!", font="TkMenuFont", justify="center")

    for pos, var in enumerate(styleOptions):
        # Add the special stylevars which apply to all styles
        tk_vars[var.id] = IntVar(value=GEN_OPTS.get_bool("StyleVar", var.id, var.enabled))
        checkbox_special[var.id] = ttk.Checkbutton(
            frame_all, variable=tk_vars[var.id], text=var.name, command=functools.partial(set_stylevar, var.id)
        )
        checkbox_special[var.id].grid(row=pos, column=0, sticky="W", padx=3)

        if var.desc:
            tooltip.add_tooltip(checkbox_special[var.id], var.desc)

    for var in var_list:
        tk_vars[var.id] = IntVar(value=var.default)
        args = {"variable": tk_vars[var.id], "text": var.name, "command": functools.partial(set_stylevar, var.id)}
        checkbox_chosen[var.id] = ttk.Checkbutton(frm_chosen, **args)
        checkbox_other[var.id] = ttk.Checkbutton(frm_other, **args)
        if var.desc:
            tooltip.add_tooltip(checkbox_chosen[var.id], var.desc)
            tooltip.add_tooltip(checkbox_other[var.id], var.desc)

    UI["style_can"].create_window(0, 0, window=canvas_frame, anchor="nw")
    UI["style_can"].update_idletasks()
    UI["style_can"].config(scrollregion=UI["style_can"].bbox(ALL), width=canvas_frame.winfo_reqwidth())
    ttk.Sizegrip(window, cursor=utils.CURSORS["stretch_vert"]).grid(row=1, column=0)

    UI["style_can"].bind("<Configure>", flow_stylevar)

    # Scroll globally even if canvas is not selected.
    if utils.WIN:
        window.bind("<MouseWheel>", lambda e: scroll(int(-1 * (e.delta / 120))))
    elif utils.MAC:
        window.bind("<Button-4>", lambda e: scroll(1))
        window.bind("<Button-5>", lambda e: scroll(-1))
Esempio n. 30
0
def make_pane(tool_frame):
    """Create the styleVar pane.

    """
    global window
    window = SubPane(
        TK_ROOT,
        options=GEN_OPTS,
        title='Style Properties',
        name='style',
        resize_y=True,
        tool_frame=tool_frame,
        tool_img=png.png('icons/win_stylevar'),
        tool_col=2,
    )

    UI['style_can'] = Canvas(window, highlightthickness=0)
    # need to use a canvas to allow scrolling
    UI['style_can'].grid(sticky='NSEW')
    window.rowconfigure(0, weight=1)

    UI['style_scroll'] = ttk.Scrollbar(
        window,
        orient=VERTICAL,
        command=UI['style_can'].yview,
        )
    UI['style_scroll'].grid(column=1, row=0, rowspan=2, sticky="NS")
    UI['style_can']['yscrollcommand'] = UI['style_scroll'].set
    canvas_frame = ttk.Frame(UI['style_can'])

    frame_all = ttk.Labelframe(canvas_frame, text="All:")
    frame_all.grid(row=0, sticky='EW')

    frm_chosen = ttk.Labelframe(canvas_frame, text="Selected Style:")
    frm_chosen.grid(row=1, sticky='EW')

    ttk.Separator(
        canvas_frame,
        orient=HORIZONTAL,
        ).grid(row=2, sticky='EW', pady=(10, 5))

    frm_other = ttk.Labelframe(canvas_frame, text="Other Styles:")
    frm_other.grid(row=3, sticky='EW')

    UI['stylevar_chosen_none'] = ttk.Label(
        frm_chosen,
        text='No Options!',
        font='TkMenuFont',
        justify='center',
        )
    UI['stylevar_other_none'] = ttk.Label(
        frm_other,
        text='None!',
        font='TkMenuFont',
        justify='center',
        )

    for pos, (var_id, name, default) in enumerate(styleOptions):
        # Add the special stylevars which apply to all styles
        tk_vars[var_id] = IntVar(
            value=GEN_OPTS.get_bool('StyleVar', var_id, default)
        )
        checkbox_special[var_id] = ttk.Checkbutton(
            frame_all,
            variable=tk_vars[var_id],
            text=name,
            command=functools.partial(set_stylevar, var_id)
            )
        checkbox_special[var_id].grid(row=pos, column=0, sticky="W", padx=3)

    for var in var_list:
        tk_vars[var.id] = IntVar(value=var.default)
        args = {
            'variable': tk_vars[var.id],
            'text': var.name,
            'command': functools.partial(set_stylevar, var.id)
            }
        checkbox_chosen[var.id] = ttk.Checkbutton(frm_chosen, **args)
        checkbox_other[var.id] = ttk.Checkbutton(frm_other, **args)

    UI['style_can'].create_window(0, 0, window=canvas_frame, anchor="nw")
    UI['style_can'].update_idletasks()
    UI['style_can'].config(
        scrollregion=UI['style_can'].bbox(ALL),
        width=canvas_frame.winfo_reqwidth(),
        )
    ttk.Sizegrip(
        window,
        cursor="sb_v_double_arrow",
        ).grid(row=1, column=0)

    UI['style_can'].bind('<Configure>', flow_stylevar)

    # Scroll globally even if canvas is not selected.
    window.bind(
        "<MouseWheel>",
        lambda e: scroll(int(-1*(e.delta/120))),
        )
    window.bind(
        "<Button-4>",
        lambda e: scroll(1),
        )
    window.bind(
        "<Button-5>",
        lambda e: scroll(-1),
        )
Esempio n. 31
0
def make_pane(tool_frame):
    """Create the styleVar pane.

    """
    global window
    window = SubPane(
        TK_ROOT,
        options=GEN_OPTS,
        title='Style Properties',
        name='style',
        resize_y=True,
        tool_frame=tool_frame,
        tool_img=png.png('icons/win_stylevar'),
        tool_col=3,
    )

    UI['style_can'] = Canvas(window, highlightthickness=0)
    # need to use a canvas to allow scrolling
    UI['style_can'].grid(sticky='NSEW')
    window.rowconfigure(0, weight=1)

    UI['style_scroll'] = ttk.Scrollbar(
        window,
        orient=VERTICAL,
        command=UI['style_can'].yview,
        )
    UI['style_scroll'].grid(column=1, row=0, rowspan=2, sticky="NS")
    UI['style_can']['yscrollcommand'] = UI['style_scroll'].set

    utils.add_mousewheel(UI['style_can'], window)

    canvas_frame = ttk.Frame(UI['style_can'])

    frame_all = ttk.Labelframe(canvas_frame, text="All:")
    frame_all.grid(row=0, sticky='EW')

    frm_chosen = ttk.Labelframe(canvas_frame, text="Selected Style:")
    frm_chosen.grid(row=1, sticky='EW')

    ttk.Separator(
        canvas_frame,
        orient=HORIZONTAL,
        ).grid(row=2, sticky='EW', pady=(10, 5))

    frm_other = ttk.Labelframe(canvas_frame, text="Other Styles:")
    frm_other.grid(row=3, sticky='EW')

    UI['stylevar_chosen_none'] = ttk.Label(
        frm_chosen,
        text='No Options!',
        font='TkMenuFont',
        justify='center',
        )
    UI['stylevar_other_none'] = ttk.Label(
        frm_other,
        text='None!',
        font='TkMenuFont',
        justify='center',
        )

    for pos, var in enumerate(styleOptions):
        # Add the special stylevars which apply to all styles
        tk_vars[var.id] = IntVar(
            value=GEN_OPTS.get_bool('StyleVar', var.id, var.default)
        )
        checkbox_special[var.id] = ttk.Checkbutton(
            frame_all,
            variable=tk_vars[var.id],
            text=var.name,
            command=functools.partial(set_stylevar, var.id)
            )
        checkbox_special[var.id].grid(row=pos, column=0, sticky="W", padx=3)

        tooltip.add_tooltip(
            checkbox_special[var.id],
            make_desc(var, is_hardcoded=True),
        )

    for var in VAR_LIST:
        tk_vars[var.id] = IntVar(value=var.enabled)
        args = {
            'variable': tk_vars[var.id],
            'text': var.name,
            'command': functools.partial(set_stylevar, var.id)
            }
        checkbox_chosen[var.id] = ttk.Checkbutton(frm_chosen, **args)
        checkbox_other[var.id] = ttk.Checkbutton(frm_other, **args)
        desc = make_desc(var)
        tooltip.add_tooltip(
            checkbox_chosen[var.id],
            desc,
        )
        tooltip.add_tooltip(
            checkbox_other[var.id],
            desc,
        )

    UI['style_can'].create_window(0, 0, window=canvas_frame, anchor="nw")
    UI['style_can'].update_idletasks()
    UI['style_can'].config(
        scrollregion=UI['style_can'].bbox(ALL),
        width=canvas_frame.winfo_reqwidth(),
    )

    if utils.USE_SIZEGRIP:
        ttk.Sizegrip(
            window,
            cursor=utils.CURSORS['stretch_vert'],
        ).grid(row=1, column=0)

    UI['style_can'].bind('<Configure>', flow_stylevar)
Esempio n. 32
0
def main(files: List[str]) -> int:
    """Run the transfer."""
    if not files:
        LOGGER.error('No files to copy!')
        LOGGER.error('packages_sync: {}', __doc__)
        return 1

    try:
        portal2_loc = Path(os.environ['PORTAL_2_LOC'])
    except KeyError:
        raise ValueError(
            'Environment Variable $PORTAL_2_LOC not set! '
            'This should be set to Portal 2\'s directory.'
        ) from None

    # Load the general options in to find out where packages are.
    GEN_OPTS.load()

    # Borrow PackageLoader to do the finding and loading for us.
    LOGGER.info('Locating packages...')

    # Disable logging of package info.
    packages_logger.setLevel(logging.ERROR)
    find_packages(GEN_OPTS['Directories']['package'])
    packages_logger.setLevel(logging.INFO)

    LOGGER.info('Done!')

    print_package_ids()

    package_loc = Path('../', GEN_OPTS['Directories']['package']).resolve()

    file_list = []  # type: List[Path]

    for file in files:
        file_path = Path(file)
        if file_path.is_dir():
            for sub_file in file_path.glob('**/*'):  # type: Path
                if sub_file.is_file():
                    file_list.append(sub_file)
        else:
            file_list.append(file_path)

    files_to_check = set()

    for file_path in file_list:
        if file_path.suffix.casefold() in ('.vmx', '.log', '.bsp', '.prt', '.lin'):
            # Ignore these file types.
            continue
        files_to_check.add(file_path)
        if file_path.suffix == '.mdl':
            for suffix in ['.vvd', '.phy', '.dx90.vtx', '.sw.vtx']:
                sub_file = file_path.with_suffix(suffix)
                if sub_file.exists():
                    files_to_check.add(sub_file)

    LOGGER.info('Processing {} files...', len(files_to_check))

    for file_path in files_to_check:
        check_file(file_path, portal2_loc, package_loc)

    return 0
Esempio n. 33
0
def make_widgets(frame: ttk.LabelFrame, pane: SubPane) -> SelectorWin:
    """Generate the UI components, and return the base window."""

    def for_channel(channel: MusicChannel) -> List[SelItem]:
        """Get the items needed for a specific channel."""
        music_list = []
        for music in Music.all():
            if music.provides_channel(channel):
                selitem = SEL_ITEMS[music.id].copy()
                selitem.snd_sample = music.get_sample(channel)
                music_list.append(selitem)
        return music_list

    # This gets overwritten when making windows.
    last_selected = {
        channel: GEN_OPTS.get_val(
            'Last_Selected',
            'music_' + channel.name.casefold(),
            '<NONE>',
        ) for channel in MusicChannel
    }

    base_win = WINDOWS[MusicChannel.BASE] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.BASE),
        title=_('Select Background Music - Base'),
        desc=_('This controls the background music used for a map. Expand '
               'the dropdown to set tracks for specific test elements.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music to the map at all. Testing Element-specific '
                    'music may still be added.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.BASE],
        attributes=[
            SelAttr.bool('SPEED', _('Propulsion Gel SFX')),
            SelAttr.bool('BOUNCE', _('Repulsion Gel SFX')),
            SelAttr.bool('TBEAM', _('Excursion Funnel Music')),
            SelAttr.bool('TBEAM_SYNC', _('Synced Funnel Music')),
        ],
    )

    WINDOWS[MusicChannel.TBEAM] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.TBEAM),
        title=_('Select Excursion Funnel Music'),
        desc=_('Set the music used while inside Excursion Funnels.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Have no music playing when inside funnels.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.TBEAM],
        attributes=[
            SelAttr.bool('TBEAM_SYNC', _('Synced Funnel Music')),
        ],
    )

    WINDOWS[MusicChannel.BOUNCE] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.BOUNCE),
        title=_('Select Repulsion Gel Music'),
        desc=_('Select the music played when players jump on Repulsion Gel.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music when jumping on Repulsion Gel.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.BOUNCE],
    )

    WINDOWS[MusicChannel.SPEED] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.SPEED),
        title=_('Select Propulsion Gel Music'),
        desc=_('Select music played when players have large amounts of horizontal velocity.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music while running fast.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.SPEED],
    )

    assert set(WINDOWS.keys()) == set(MusicChannel), "Extra channels?"

    # Widgets we want to remove when collapsing.
    exp_widgets = []  # type: List[tkinter.Widget]

    def toggle_btn_enter(event=None):
        toggle_btn['text'] = BTN_EXPAND_HOVER if is_collapsed else BTN_CONTRACT_HOVER

    def toggle_btn_exit(event=None):
        toggle_btn['text'] = BTN_EXPAND if is_collapsed else BTN_CONTRACT

    def set_collapsed():
        """Configure for the collapsed state."""
        global is_collapsed
        is_collapsed = True
        GEN_OPTS['Last_Selected']['music_collapsed'] = '1'
        base_lbl['text'] = _('Music: ')
        toggle_btn_exit()

        # Set all music to the children - so those are used.
        set_suggested(WINDOWS[MusicChannel.BASE].chosen_id, sel_item=True)

        for wid in exp_widgets:
            wid.grid_remove()

    def set_expanded():
        """Configure for the expanded state."""
        global is_collapsed
        is_collapsed = False
        GEN_OPTS['Last_Selected']['music_collapsed'] = '0'
        base_lbl['text'] = _('Base: ')
        toggle_btn_exit()
        for wid in exp_widgets:
            wid.grid()
        pane.update_idletasks()
        pane.move()

    def toggle(event=None):
        if is_collapsed:
            set_expanded()
        else:
            set_collapsed()
        pane.update_idletasks()
        pane.move()

    frame.columnconfigure(2, weight=1)

    base_lbl = ttk.Label(frame)
    base_lbl.grid(row=0, column=1)

    toggle_btn = ttk.Label(frame, text=' ')
    toggle_btn.bind('<Enter>', toggle_btn_enter)
    toggle_btn.bind('<Leave>', toggle_btn_exit)
    toggle_btn.bind('<ButtonPress-1>', toggle)
    toggle_btn.grid(row=0, column=0)

    for row, channel in enumerate(MusicChannel):
        btn = WINDOWS[channel].widget(frame)
        if row:
            exp_widgets.append(btn)
        btn.grid(row=row, column=2, sticky='EW')

    for row, text in enumerate([
        _('Funnel:'),
        _('Bounce:'),
        _('Speed:'),
    ], start=1):
        label = ttk.Label(frame, text=text)
        exp_widgets.append(label)
        label.grid(row=row, column=1, sticky='EW')

    if GEN_OPTS.get_bool('Last_Selected', 'music_collapsed', True):
        set_collapsed()
    else:
        set_expanded()

    for channel, win in WINDOWS.items():
        win.sel_item_id(last_selected[channel])

    return base_win
Esempio n. 34
0
def auto_backup(game: 'gameMan.Game', loader: loadScreen.LoadScreen):
    """Perform an automatic backup for the given game.

    We do this seperately since we don't need to read the property files.
    """
    from BEE2_config import GEN_OPTS
    if not GEN_OPTS.get_bool('General', 'enable_auto_backup'):
        # Don't backup!
        loader.skip_stage(AUTO_BACKUP_STAGE)
        return

    folder = find_puzzles(game)
    if not folder:
        loader.skip_stage(AUTO_BACKUP_STAGE)
        return

    # Keep this many previous
    extra_back_count = GEN_OPTS.get_int('General', 'auto_backup_count', 0)

    to_backup = os.listdir(folder)
    backup_dir = GEN_OPTS.get_val('Directories', 'backup_loc', 'backups/')

    os.makedirs(backup_dir, exist_ok=True)

    # A version of the name stripped of special characters
    # Allowed: a-z, A-Z, 0-9, '_-.'
    safe_name = srctools.whitelist(
        game.name,
        valid_chars=BACKUP_CHARS,
    )

    loader.set_length(AUTO_BACKUP_STAGE, len(to_backup))

    if extra_back_count:
        back_files = [AUTO_BACKUP_FILE.format(game=safe_name, ind='')] + [
            AUTO_BACKUP_FILE.format(game=safe_name, ind='_' + str(i + 1))
            for i in range(extra_back_count)
        ]
        # Move each file over by 1 index, ignoring missing ones
        # We need to reverse to ensure we don't overwrite any zips
        for old_name, new_name in reversed(
                list(zip(back_files, back_files[1:]))):
            LOGGER.info(
                'Moving: {old} -> {new}',
                old=old_name,
                new=new_name,
            )
            old_name = os.path.join(backup_dir, old_name)
            new_name = os.path.join(backup_dir, new_name)
            try:
                os.remove(new_name)
            except FileNotFoundError:
                pass  # We're overwriting this anyway
            try:
                os.rename(old_name, new_name)
            except FileNotFoundError:
                pass

    final_backup = os.path.join(
        backup_dir,
        AUTO_BACKUP_FILE.format(game=safe_name, ind=''),
    )
    LOGGER.info('Writing backup to "{}"', final_backup)
    with open(final_backup, 'wb') as f:
        with ZipFile(f, mode='w', compression=ZIP_LZMA) as zip_file:
            for file in to_backup:
                zip_file.write(
                    os.path.join(folder, file),
                    file,
                    ZIP_LZMA,
                )
                loader.step(AUTO_BACKUP_STAGE)
Esempio n. 35
0
async def init_app():
    """Initialise the application."""
    GEN_OPTS.load()
    GEN_OPTS.set_defaults(DEFAULT_SETTINGS)

    # Special case, load in this early so it applies.
    utils.DEV_MODE = GEN_OPTS.get_bool('Debug', 'development_mode')
    DEV_MODE.set(utils.DEV_MODE)

    LOGGER.debug('Starting loading screen...')
    loadScreen.main_loader.set_length('UI', 16)
    loadScreen.set_force_ontop(
        GEN_OPTS.get_bool('General', 'splash_stay_ontop'))
    loadScreen.show_main_loader(GEN_OPTS.get_bool('General', 'compact_splash'))

    # OS X starts behind other windows, fix that.
    if utils.MAC:
        TK_ROOT.lift()

    logWindow.HANDLER.set_visible(GEN_OPTS.get_bool('Debug', 'show_log_win'))
    logWindow.HANDLER.setLevel(GEN_OPTS['Debug']['window_log_level'])

    LOGGER.debug('Loading settings...')

    UI.load_settings()

    gameMan.load()
    gameMan.set_game_by_name(GEN_OPTS.get_val('Last_Selected', 'Game', ''), )
    gameMan.scan_music_locs()

    LOGGER.info('Loading Packages...')
    package_sys = await packages.load_packages(
        list(get_package_locs()),
        loader=loadScreen.main_loader,
        log_item_fallbacks=GEN_OPTS.get_bool('Debug', 'log_item_fallbacks'),
        log_missing_styles=GEN_OPTS.get_bool('Debug', 'log_missing_styles'),
        log_missing_ent_count=GEN_OPTS.get_bool('Debug',
                                                'log_missing_ent_count'),
        log_incorrect_packfile=GEN_OPTS.get_bool('Debug',
                                                 'log_incorrect_packfile'),
        has_tag_music=gameMan.MUSIC_TAG_LOC is not None,
        has_mel_music=gameMan.MUSIC_MEL_VPK is not None,
    )
    loadScreen.main_loader.step('UI', 'pre_ui')
    APP_NURSERY.start_soon(img.init, package_sys)
    APP_NURSERY.start_soon(sound.sound_task)

    # Load filesystems into various modules
    music_conf.load_filesystems(package_sys.values())
    gameMan.load_filesystems(package_sys.values())
    UI.load_packages()
    loadScreen.main_loader.step('UI', 'package_load')
    LOGGER.info('Done!')

    # Check games for Portal 2's basemodui.txt file, so we can translate items.
    LOGGER.info('Loading Item Translations...')
    for game in gameMan.all_games:
        game.init_trans()

    LOGGER.info('Initialising UI...')
    await UI.init_windows()  # create all windows
    LOGGER.info('UI initialised!')

    loadScreen.main_loader.destroy()
    # Delay this until the loop has actually run.
    # Directly run TK_ROOT.lift() in TCL, instead
    # of building a callable.
    TK_ROOT.tk.call('after', 10, 'raise', TK_ROOT)
Esempio n. 36
0
        'log_item_fallbacks': '0',
        # Print message for items that have no match for a style
        'log_missing_styles': '0',
        # Print message for items that are missing ent_count values
        'log_missing_ent_count': '0',
        # Warn if a file is missing that a packfile refers to
        'log_incorrect_packfile': '0',

        # Show the log window on startup
        'show_log_win': '0',
        # The lowest level which will be shown.
        'window_log_level': 'INFO',
    },
}

GEN_OPTS.load()
GEN_OPTS.set_defaults(DEFAULT_SETTINGS)

loadScreen.main_loader.set_length('UI', 14)
loadScreen.show_main_loader(GEN_OPTS.get_bool('General', 'compact_splash'))

# OS X starts behind other windows, fix that.
if utils.MAC:
    TK_ROOT.lift()

logWindow.init(
    GEN_OPTS.get_bool('Debug', 'show_log_win'),
    GEN_OPTS['Debug']['window_log_level']
)

UI.load_settings()
Esempio n. 37
0
def make_widgets(frame: ttk.LabelFrame, pane: SubPane) -> SelectorWin:
    """Generate the UI components, and return the base window."""

    def for_channel(channel: MusicChannel) -> List[SelItem]:
        """Get the items needed for a specific channel."""
        music_list = []
        for music in Music.all():
            if music.provides_channel(channel):
                selitem = SEL_ITEMS[music.id].copy()
                selitem.snd_sample = music.get_sample(channel)
                music_list.append(selitem)
        return music_list

    # This gets overwritten when making windows.
    last_selected = {
        channel: GEN_OPTS.get_val(
            'Last_Selected',
            'music_' + channel.name.casefold(),
            '<NONE>',
        ) for channel in MusicChannel
    }

    base_win = WINDOWS[MusicChannel.BASE] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.BASE),
        title=_('Select Background Music - Base'),
        desc=_('This controls the background music used for a map. Expand '
               'the dropdown to set tracks for specific test elements.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music to the map at all. Testing Element-specific '
                    'music may still be added.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.BASE],
        attributes=[
            SelAttr.bool('SPEED', _('Propulsion Gel SFX')),
            SelAttr.bool('BOUNCE', _('Repulsion Gel SFX')),
            SelAttr.bool('TBEAM', _('Excursion Funnel Music')),
            SelAttr.bool('TBEAM_SYNC', _('Synced Funnel Music')),
        ],
    )

    WINDOWS[MusicChannel.TBEAM] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.TBEAM),
        title=_('Select Excursion Funnel Music'),
        desc=_('Set the music used while inside Excursion Funnels.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Have no music playing when inside funnels.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.TBEAM],
        attributes=[
            SelAttr.bool('TBEAM_SYNC', _('Synced Funnel Music')),
        ],
    )

    WINDOWS[MusicChannel.BOUNCE] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.BOUNCE),
        title=_('Select Repulsion Gel Music'),
        desc=_('Select the music played when players jump on Repulsion Gel.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music when jumping on Repulsion Gel.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.BOUNCE],
    )

    WINDOWS[MusicChannel.SPEED] = SelectorWin(
        TK_ROOT,
        for_channel(MusicChannel.SPEED),
        title=_('Select Propulsion Gel Music'),
        desc=_('Select music played when players have large amounts of horizontal velocity.'),
        has_none=True,
        sound_sys=filesystem,
        none_desc=_('Add no music while running fast.'),
        callback=selwin_callback,
        callback_params=[MusicChannel.SPEED],
    )

    assert set(WINDOWS.keys()) == set(MusicChannel), "Extra channels?"

    # Widgets we want to remove when collapsing.
    exp_widgets = []  # type: List[tkinter.Widget]

    def toggle_btn_enter(event=None):
        toggle_btn['text'] = BTN_EXPAND_HOVER if is_collapsed else BTN_CONTRACT_HOVER

    def toggle_btn_exit(event=None):
        toggle_btn['text'] = BTN_EXPAND if is_collapsed else BTN_CONTRACT

    def set_collapsed():
        """Configure for the collapsed state."""
        global is_collapsed
        is_collapsed = True
        GEN_OPTS['Last_Selected']['music_collapsed'] = '1'
        base_lbl['text'] = _('Music: ')
        toggle_btn_exit()

        # Set all music to the children - so those are used.
        set_suggested(WINDOWS[MusicChannel.BASE].chosen_id, sel_item=True)

        for wid in exp_widgets:
            wid.grid_remove()

    def set_expanded():
        """Configure for the expanded state."""
        global is_collapsed
        is_collapsed = False
        GEN_OPTS['Last_Selected']['music_collapsed'] = '0'
        base_lbl['text'] = _('Base: ')
        toggle_btn_exit()
        for wid in exp_widgets:
            wid.grid()
        pane.update_idletasks()
        pane.move()

    def toggle(event=None):
        if is_collapsed:
            set_expanded()
        else:
            set_collapsed()
        pane.update_idletasks()
        pane.move()

    frame.columnconfigure(2, weight=1)

    base_lbl = ttk.Label(frame)
    base_lbl.grid(row=0, column=1)

    toggle_btn = ttk.Label(frame, text=' ')
    toggle_btn.bind('<Enter>', toggle_btn_enter)
    toggle_btn.bind('<Leave>', toggle_btn_exit)
    toggle_btn.bind('<ButtonPress-1>', toggle)
    toggle_btn.grid(row=0, column=0)

    for row, channel in enumerate(MusicChannel):
        btn = WINDOWS[channel].widget(frame)
        if row:
            exp_widgets.append(btn)
        btn.grid(row=row, column=2, sticky='EW')

    for row, text in enumerate([
        _('Funnel:'),
        _('Bounce:'),
        _('Speed:'),
    ], start=1):
        label = ttk.Label(frame, text=text)
        exp_widgets.append(label)
        label.grid(row=row, column=1, sticky='EW')

    if GEN_OPTS.get_bool('Last_Selected', 'music_collapsed', True):
        set_collapsed()
    else:
        set_expanded()

    for channel, win in WINDOWS.items():
        win.sel_item_id(last_selected[channel])

    return base_win
Esempio n. 38
0
        # Show the log window on startup
        'show_log_win': '0',
        # The lowest level which will be shown.
        'window_log_level': 'INFO',
    },
}

loadScreen.main_loader.set_length('UI', 15)
loadScreen.main_loader.show()

# OS X starts behind other windows, fix that.
if utils.MAC:
    TK_ROOT.lift()

GEN_OPTS.load()
GEN_OPTS.set_defaults(DEFAULT_SETTINGS)

logWindow.init(GEN_OPTS.get_bool('Debug', 'show_log_win'),
               GEN_OPTS['Debug']['window_log_level'])

UI.load_settings()

gameMan.load()
gameMan.set_game_by_name(GEN_OPTS.get_val('Last_Selected', 'Game', ''), )
gameMan.scan_music_locs()

LOGGER.info('Loading Packages...')
pack_data, package_sys = packageLoader.load_packages(
    GEN_OPTS['Directories']['package'],
    log_item_fallbacks=GEN_OPTS.get_bool('Debug', 'log_item_fallbacks'),
Esempio n. 39
0
def _test() -> None:
    """Test the GUI."""
    from srctools.logger import init_logging
    from tk_tools import TK_ROOT
    from BEE2_config import GEN_OPTS
    from packageLoader import find_packages, PACKAGE_SYS

    init_logging()

    # Setup images to read from packages.
    print('Loading packages for images.')
    GEN_OPTS.load()
    find_packages(GEN_OPTS['Directories']['package'])
    img.load_filesystems(PACKAGE_SYS.values())
    print('Done.')

    left_frm = ttk.Frame(TK_ROOT)
    right_canv = tkinter.Canvas(TK_ROOT)

    left_frm.grid(row=0, column=0, sticky='NSEW', padx=8)
    right_canv.grid(row=0, column=1, sticky='NSEW', padx=8)
    TK_ROOT.rowconfigure(0, weight=1)
    TK_ROOT.columnconfigure(1, weight=1)

    slot_dest = []
    slot_src = []

    class TestItem:
        def __init__(
            self,
            name: str,
            icon: str,
            group: str=None,
            group_icon: str=None,
        ) -> None:
            self.name = name
            self.dnd_icon = img.png('items/clean/{}.png'.format(icon))
            self.dnd_group = group
            if group_icon:
                self.dnd_group_icon = img.png('items/clean/{}.png'.format(group_icon))

        def __repr__(self) -> str:
            return '<Item {}>'.format(self.name)

    manager = Manager[TestItem](TK_ROOT, config_icon=True)

    def func(ev):
        def call(slot):
            print('Cback: ', ev, slot)
        return call

    for event in Event:
        manager.reg_callback(event, func(event))

    items = [
        TestItem('Dropper', 'dropper'),
        TestItem('Entry', 'entry_door'),
        TestItem('Exit', 'exit_door'),
        TestItem('Large Obs', 'large_obs_room'),
        TestItem('Faith Plate', 'faithplate'),

        TestItem('Standard Cube', 'cube', 'ITEM_CUBE', 'cubes'),
        TestItem('Companion Cube', 'companion_cube', 'ITEM_CUBE', 'cubes'),
        TestItem('Reflection Cube', 'reflection_cube', 'ITEM_CUBE', 'cubes'),
        TestItem('Edgeless Cube', 'edgeless_safety_cube', 'ITEM_CUBE', 'cubes'),
        TestItem('Franken Cube', 'frankenturret', 'ITEM_CUBE', 'cubes'),

        TestItem('Repulsion Gel', 'paintsplat_bounce', 'ITEM_PAINT_SPLAT', 'paints'),
        TestItem('Propulsion Gel', 'paintsplat_speed', 'ITEM_PAINT_SPLAT', 'paints'),
        TestItem('Reflection Gel', 'paintsplat_reflection', 'ITEM_PAINT_SPLAT', 'paints'),
        TestItem('Conversion Gel', 'paintsplat_portal', 'ITEM_PAINT_SPLAT', 'paints'),
        TestItem('Cleansing Gel', 'paintsplat_water', 'ITEM_PAINT_SPLAT', 'paints'),
    ]

    for y in range(8):
        for x in range(4):
            slot = manager.slot(left_frm, source=False, label=(format(x + 4*y, '02') if y < 3 else ''))
            slot.grid(column=x, row=y, padx=1, pady=1)
            slot_dest.append(slot)

    for i, item in enumerate(items):
            slot = manager.slot(right_canv, source=True, label=format(i+1, '02'))
            slot_src.append(slot)
            slot.contents = item

    def configure(e):
        manager.flow_slots(right_canv, slot_src)

    configure(None)
    right_canv.bind('<Configure>', configure)

    ttk.Button(
        TK_ROOT,
        text='Debug',
        command=lambda: print('Dest:', [slot.contents for slot in slot_dest])
    ).grid(row=2, column=0)
    ttk.Button(
        TK_ROOT,
        text='Debug',
        command=lambda: print('Source:', [slot.contents for slot in slot_src])
    ).grid(row=2, column=1)

    name_lbl = ttk.Label(TK_ROOT, text='')
    name_lbl.grid(row=3, column=0)

    def enter(slot):
        if slot.contents is not None:
            name_lbl['text'] = 'Name: ' + slot.contents.name

    def exit(slot):
        name_lbl['text'] = ''

    manager.reg_callback(Event.HOVER_ENTER, enter)
    manager.reg_callback(Event.HOVER_EXIT, exit)
    manager.reg_callback(Event.CONFIG, lambda slot: messagebox.showinfo('Hello World', str(slot.contents)))

    TK_ROOT.deiconify()
    TK_ROOT.mainloop()
Esempio n. 40
0
def init_backup_settings():
    """Initialise the auto-backup settings widget."""
    from BEE2_config import GEN_OPTS
    check_var = tk.IntVar(
        value=GEN_OPTS.get_bool('General', 'enable_auto_backup')
    )
    count_value = GEN_OPTS.get_int('General', 'auto_backup_count', 0)
    back_dir = GEN_OPTS.get_val('Directories', 'backup_loc', 'backups/')

    def check_callback():
        GEN_OPTS['General']['enable_auto_backup'] = utils.bool_as_int(
            check_var.get()
        )

    def count_callback():
        GEN_OPTS['General']['auto_backup_count'] = str(count.value)

    def directory_callback(path):
        GEN_OPTS['Directories']['backup_loc'] = path

    UI['auto_frame'] = frame = ttk.LabelFrame(
        window,
    )
    UI['auto_enable'] = enable_check = ttk.Checkbutton(
        frame,
        text='Automatic Backup After Export',
        variable=check_var,
        command=check_callback,
    )

    frame['labelwidget'] = enable_check
    frame.grid(row=2, column=0, columnspan=3)

    dir_frame = ttk.Frame(
        frame,
    )
    dir_frame.grid(row=0, column=0)

    ttk.Label(
        dir_frame,
        text='Directory',
    ).grid(row=0, column=0)

    UI['auto_dir'] = tk_tools.FileField(
        dir_frame,
        loc=back_dir,
        is_dir=True,
        callback=directory_callback,
    )
    UI['auto_dir'].grid(row=1, column=0)

    count_frame = ttk.Frame(
        frame,
    )
    count_frame.grid(row=0, column=1)
    ttk.Label(
        count_frame,
        text='Keep (Per Game):'
    ).grid(row=0, column=0)

    count = tk_tools.ttk_Spinbox(
        count_frame,
        range=range(50),
        command=count_callback,
    )
    count.grid(row=1, column=0)
    count.value = count_value
Esempio n. 41
0
 def quit_command():
     from BEE2_config import GEN_OPTS
     window.withdraw()
     GEN_OPTS.save_check()
Esempio n. 42
0
def init_backup_settings() -> None:
    """Initialise the auto-backup settings widget."""
    from BEE2_config import GEN_OPTS
    check_var = tk.IntVar(
        value=GEN_OPTS.get_bool('General', 'enable_auto_backup')
    )
    count_value = GEN_OPTS.get_int('General', 'auto_backup_count', 0)
    back_dir = GEN_OPTS.get_val('Directories', 'backup_loc', 'backups/')

    def check_callback():
        GEN_OPTS['General']['enable_auto_backup'] = srctools.bool_as_int(
            check_var.get()
        )

    def count_callback():
        GEN_OPTS['General']['auto_backup_count'] = str(count.value)

    def directory_callback(path):
        GEN_OPTS['Directories']['backup_loc'] = path

    UI['auto_frame'] = frame = ttk.LabelFrame(
        window,
    )
    UI['auto_enable'] = enable_check = ttk.Checkbutton(
        frame,
        text=gettext('Automatic Backup After Export'),
        variable=check_var,
        command=check_callback,
    )

    frame['labelwidget'] = enable_check
    frame.grid(row=2, column=0, columnspan=3)

    dir_frame = ttk.Frame(
        frame,
    )
    dir_frame.grid(row=0, column=0)

    ttk.Label(
        dir_frame,
        text='Directory',
    ).grid(row=0, column=0)

    UI['auto_dir'] = tk_tools.FileField(
        dir_frame,
        loc=back_dir,
        is_dir=True,
        callback=directory_callback,
    )
    UI['auto_dir'].grid(row=1, column=0)

    count_frame = ttk.Frame(
        frame,
    )
    count_frame.grid(row=0, column=1)
    ttk.Label(
        count_frame,
        text=gettext('Keep (Per Game):'),
    ).grid(row=0, column=0)

    count = tk_tools.ttk_Spinbox(
        count_frame,
        range=range(50),
        command=count_callback,
    )
    count.grid(row=1, column=0)
    count.value = count_value
Esempio n. 43
0
        'log_item_fallbacks': '0',
        # Print message for items that have no match for a style
        'log_missing_styles': '0',
        # Print message for items that are missing ent_count values
        'log_missing_ent_count': '0',
        # Warn if a file is missing that a packfile refers to
        'log_incorrect_packfile': '0',

        # Show the log window on startup
        'show_log_win': '0',
        # The lowest level which will be shown.
        'window_log_level': 'INFO',
    },
}

GEN_OPTS.load()
GEN_OPTS.set_defaults(DEFAULT_SETTINGS)

LOGGER.debug('Starting loading screen...')
loadScreen.main_loader.set_length('UI', 14)
loadScreen.set_force_ontop(GEN_OPTS.get_bool('General', 'splash_stay_ontop'))
loadScreen.show_main_loader(GEN_OPTS.get_bool('General', 'compact_splash'))

# OS X starts behind other windows, fix that.
if utils.MAC:
    TK_ROOT.lift()

logWindow.init(GEN_OPTS.get_bool('Debug', 'show_log_win'),
               GEN_OPTS['Debug']['window_log_level'])

LOGGER.debug('Loading settings...')
Esempio n. 44
0
def make_pane(tool_frame):
    """Create the styleVar pane.

    """
    global window
    window = SubPane(
        TK_ROOT,
        options=GEN_OPTS,
        title='Style Properties',
        name='style',
        resize_y=True,
        tool_frame=tool_frame,
        tool_img=png.png('icons/win_stylevar'),
        tool_col=3,
    )

    UI['style_can'] = Canvas(window, highlightthickness=0)
    # need to use a canvas to allow scrolling
    UI['style_can'].grid(sticky='NSEW')
    window.rowconfigure(0, weight=1)

    UI['style_scroll'] = ttk.Scrollbar(
        window,
        orient=VERTICAL,
        command=UI['style_can'].yview,
    )
    UI['style_scroll'].grid(column=1, row=0, rowspan=2, sticky="NS")
    UI['style_can']['yscrollcommand'] = UI['style_scroll'].set
    canvas_frame = ttk.Frame(UI['style_can'])

    frame_all = ttk.Labelframe(canvas_frame, text="All:")
    frame_all.grid(row=0, sticky='EW')

    frm_chosen = ttk.Labelframe(canvas_frame, text="Selected Style:")
    frm_chosen.grid(row=1, sticky='EW')

    ttk.Separator(
        canvas_frame,
        orient=HORIZONTAL,
    ).grid(row=2, sticky='EW', pady=(10, 5))

    frm_other = ttk.Labelframe(canvas_frame, text="Other Styles:")
    frm_other.grid(row=3, sticky='EW')

    UI['stylevar_chosen_none'] = ttk.Label(
        frm_chosen,
        text='No Options!',
        font='TkMenuFont',
        justify='center',
    )
    UI['stylevar_other_none'] = ttk.Label(
        frm_other,
        text='None!',
        font='TkMenuFont',
        justify='center',
    )

    for pos, var in enumerate(styleOptions):
        # Add the special stylevars which apply to all styles
        tk_vars[var.id] = IntVar(
            value=GEN_OPTS.get_bool('StyleVar', var.id, var.enabled))
        checkbox_special[var.id] = ttk.Checkbutton(frame_all,
                                                   variable=tk_vars[var.id],
                                                   text=var.name,
                                                   command=functools.partial(
                                                       set_stylevar, var.id))
        checkbox_special[var.id].grid(row=pos, column=0, sticky="W", padx=3)

        if var.desc:
            tooltip.add_tooltip(
                checkbox_special[var.id],
                var.desc,
            )

    for var in var_list:
        tk_vars[var.id] = IntVar(value=var.default)
        args = {
            'variable': tk_vars[var.id],
            'text': var.name,
            'command': functools.partial(set_stylevar, var.id)
        }
        checkbox_chosen[var.id] = ttk.Checkbutton(frm_chosen, **args)
        checkbox_other[var.id] = ttk.Checkbutton(frm_other, **args)
        if var.desc:
            tooltip.add_tooltip(
                checkbox_chosen[var.id],
                var.desc,
            )
            tooltip.add_tooltip(
                checkbox_other[var.id],
                var.desc,
            )

    UI['style_can'].create_window(0, 0, window=canvas_frame, anchor="nw")
    UI['style_can'].update_idletasks()
    UI['style_can'].config(
        scrollregion=UI['style_can'].bbox(ALL),
        width=canvas_frame.winfo_reqwidth(),
    )
    ttk.Sizegrip(
        window,
        cursor=utils.CURSORS['stretch_vert'],
    ).grid(row=1, column=0)

    UI['style_can'].bind('<Configure>', flow_stylevar)

    # Scroll globally even if canvas is not selected.
    if utils.WIN:
        window.bind(
            "<MouseWheel>",
            lambda e: scroll(int(-1 * (e.delta / 120))),
        )
    elif utils.MAC:
        window.bind(
            "<Button-4>",
            lambda e: scroll(1),
        )
        window.bind(
            "<Button-5>",
            lambda e: scroll(-1),
        )
Esempio n. 45
0
 def quit_command():
     from BEE2_config import GEN_OPTS
     window.withdraw()
     GEN_OPTS.save_check()
Esempio n. 46
0
    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