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()
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()
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() ))
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_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
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()
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()
def load_opt(): """Load the checkbox's values.""" var.set(GEN_OPTS.get_bool( section, item, default, ))
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 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
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
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!')
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!')
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)
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)
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, )
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
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
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()
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
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
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)
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)
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))
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), )
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)
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
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
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)
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)
'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()
# 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'),
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()
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
def quit_command(): from BEE2_config import GEN_OPTS window.withdraw() GEN_OPTS.save_check()
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
'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...')
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), )
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