def generate_resp_script(file, allow_dings): """Write the responses section into a file.""" use_dings = allow_dings config = ConfigFile('resp_voice.cfg', root='bee2') file.write("BEE2_RESPONSES <- {\n") for section in QUOTE_DATA.find_key('CoopResponses', []): if not section.has_children() and section.name == 'use_dings': # Allow overriding specifically for the response script use_dings = utils.conv_bool(section.value, allow_dings) continue voice_attr = RESP_HAS_NAMES.get(section.name, '') if voice_attr and not map_attr[voice_attr]: continue # This response catagory isn't present section_data = ['\t{} = [\n'.format(section.name)] for index, line in enumerate(section): if not config.getboolean(section.name, "line_" + str(index), True): # It's disabled! continue section_data.append( '\t\tCreateSceneEntity("{}"),\n'.format(line['choreo']) ) if len(section_data) != 1: for line in section_data: file.write(line) file.write('\t],\n') file.write('}\n') file.write('BEE2_PLAY_DING = {};\n'.format( 'true' if use_dings else 'false' ))
def generate_resp_script(file, allow_dings): """Write the responses section into a file.""" use_dings = allow_dings config = ConfigFile('resp_voice.cfg', root='bee2') file.write("BEE2_RESPONSES <- {\n") for section in QUOTE_DATA.find_key('CoopResponses', []): if not section.has_children() and section.name == 'use_dings': # Allow overriding specifically for the response script use_dings = srctools.conv_bool(section.value, allow_dings) continue voice_attr = RESP_HAS_NAMES.get(section.name, '') if voice_attr and not map_attr[voice_attr]: continue # This response catagory isn't present section_data = ['\t{} = [\n'.format(section.name)] for index, line in enumerate(section): if not config.getboolean(section.name, "line_" + str(index), True): # It's disabled! continue section_data.append('\t\tCreateSceneEntity("{}"),\n'.format( line['choreo'])) if len(section_data) != 1: for line in section_data: file.write(line) file.write('\t],\n') file.write('}\n') file.write( 'BEE2_PLAY_DING = {};\n'.format('true' if use_dings else 'false'))
def parse(cls, gm_id, config: ConfigFile): steam_id = config.get_val(gm_id, "SteamID", "<none>") if not steam_id.isdigit(): raise ValueError("Game {} has invalid Steam ID: {}".format(gm_id, steam_id)) folder = config.get_val(gm_id, "Dir", "") if not folder: raise ValueError("Game {} has no folder!".format(gm_id)) mod_time = config.get_int(gm_id, "ModTime", 0) return cls(gm_id, steam_id, folder, mod_time)
def parse(cls, gm_id, config: ConfigFile): steam_id = config.get_val(gm_id, 'SteamID', '<none>') if not steam_id.isdigit(): raise ValueError('Game {} has invalid Steam ID: {}'.format( gm_id, steam_id)) folder = config.get_val(gm_id, 'Dir', '') if not folder: raise ValueError('Game {} has no folder!'.format(gm_id)) mod_time = config.get_int(gm_id, 'ModTime', 0) return cls(gm_id, steam_id, folder, mod_time)
def parse(cls, gm_id, config: ConfigFile): steam_id = config.get_val(gm_id, 'SteamID', '<none>') if not steam_id.isdigit(): raise ValueError( 'Game {} has invalid Steam ID: {}'.format(gm_id, steam_id) ) folder = config.get_val(gm_id, 'Dir', '') if not folder: raise ValueError( 'Game {} has no folder!'.format(gm_id) ) mod_time = config.get_int(gm_id, 'ModTime', 0) return cls(gm_id, steam_id, folder, mod_time)
def parse(cls, gm_id, config: ConfigFile): steam_id = config.get_val(gm_id, 'SteamID', '<none>') if not steam_id.isdigit(): raise ValueError('Game {} has invalid Steam ID: {}'.format( gm_id, steam_id)) folder = config.get_val(gm_id, 'Dir', '') if not folder: raise ValueError('Game {} has no folder!'.format(gm_id)) mod_times = {} for name, value in config.items(gm_id): if name.startswith('pack_mod_'): mod_times[name[9:].casefold()] = srctools.conv_int(value) return cls(gm_id, steam_id, folder, mod_times)
def generate_resp_script(file): """Write the responses section into a file.""" config = ConfigFile('resp_voice.cfg', root='bee2') file.write("BEE2_RESPONSES <- {\n") for section in QUOTE_DATA.find_key('CoopResponses', []): section_data = ['\t{} = [\n'.format(section.name)] for index, line in enumerate(section): if not config.getboolean(section.name, "line_" + str(index), True): # It's disabled! continue section_data.append( '\t\tCreateSceneEntity("{}"),\n'.format(line['choreo']) ) if len(section_data) != 1: for line in section_data: file.write(line) file.write('\t],\n') file.write('}\n')
def show(quote_pack): """Display the editing window.""" global voice_item, config, config_mid voice_item = quote_pack win.title('BEE2 - Configure "' + voice_item.selitem_data.name + '"') notebook = UI['tabs'] quote_data = quote_pack.config os.makedirs('config/voice', exist_ok=True) config = ConfigFile('voice/' + quote_pack.id + '.cfg') config_mid = ConfigFile('voice/MID_' + quote_pack.id + '.cfg') # Clear the transcript textbox text = UI['trans'] text['state'] = 'normal' text.delete(1.0, END) text['state'] = 'disabled' # Destroy all the old tabs for tab in TABS.values(): try: notebook.forget(tab) except TclError as e: pass tab.destroy() TABS.clear() for group in quote_data.find_all('quotes', 'group'): make_tab( group, config, is_mid=False, ) mid_quotes = list(quote_data.find_all('quotes', 'midchamber')) if len(mid_quotes) > 0: frame = make_tab( mid_quotes[0], config_mid, is_mid=True, ) frame.nb_text = '' config.save() config_mid.save() add_tabs() win.deiconify() win.lift(win.winfo_parent()) utils.center_win(win) # Center inside the parent
def parse(cls, gm_id: str, config: ConfigFile) -> 'Game': """Parse out the given game ID from the config file.""" steam_id = config.get_val(gm_id, 'SteamID', '<none>') if not steam_id.isdigit(): raise ValueError(f'Game {gm_id} has invalid Steam ID: {steam_id}') folder = config.get_val(gm_id, 'Dir', '') if not folder: raise ValueError(f'Game {gm_id} has no folder!') if not os.path.exists(folder): raise ValueError( f'Folder {folder} does not exist for game {gm_id}!') mod_times = {} for name, value in config.items(gm_id): if name.startswith('pack_mod_'): mod_times[name[9:].casefold()] = srctools.conv_int(value) return cls(gm_id, steam_id, folder, mod_times)
def encode_coop_responses(vmf: VMF, pos: Vec, allow_dings: bool, voice_attrs: dict) -> None: """Write the coop responses information into the map.""" config = ConfigFile('bee2/resp_voice.cfg', in_conf_folder=False) response_block = QUOTE_DATA.find_key('CoopResponses', []) # Pass in whether to include dings or not. vmf.create_ent( 'comp_scriptvar_setter', origin=pos, target='@glados', variable='BEE2_PLAY_DING', mode='const', # Allow overriding specifically for the response script. const=response_block.bool('use_dings', allow_dings), ) for section in response_block: if not section.has_children(): continue voice_attr = RESP_HAS_NAMES.get(section.name, '') if voice_attr and not voice_attrs[voice_attr]: # This response category isn't present. continue # Use a custom entity to encode our information. ent = vmf.create_ent( 'bee2_coop_response', origin=pos, type=section.name, ) # section_data = [] for index, line in enumerate(section): if not config.getboolean(section.name, "line_" + str(index), True): # It's disabled! continue ent[f'choreo{index:02}'] = line['choreo']
def main(argv: List[str]) -> None: """Main VRAD script.""" LOGGER.info( "BEE{} VRAD hook initiallised, srctools v{}, Hammer Addons v{}", utils.BEE_VERSION, srctools.__version__, version_haddons, ) # Warn if srctools Cython code isn't installed. utils.check_cython(LOGGER.warning) args = " ".join(argv) fast_args = argv[1:] full_args = argv[1:] if not fast_args: # No arguments! LOGGER.info( 'No arguments!\n' "The BEE2 VRAD takes all the regular VRAD's " 'arguments, with some extra arguments:\n' '-force_peti: Force enabling map conversion. \n' "-force_hammer: Don't convert the map at all.\n" "If not specified, the map name must be \"preview.bsp\" to be " "treated as PeTI." ) sys.exit() # The path is the last argument to vrad # P2 adds wrong slashes sometimes, so fix that. fast_args[-1] = path = os.path.normpath(argv[-1]) LOGGER.info("Map path is " + path) LOGGER.info('Loading Settings...') config = ConfigFile('compile.cfg') for a in fast_args[:]: folded_a = a.casefold() if folded_a.casefold() in ( "-final", "-staticproplighting", "-staticproppolys", "-textureshadows", ): # remove final parameters from the modified arguments fast_args.remove(a) elif folded_a == '-both': # LDR Portal 2 isn't actually usable, so there's not much # point compiling for it. pos = fast_args.index(a) fast_args[pos] = full_args[pos] = '-hdr' elif a in ('-force_peti', '-force_hammer', '-no_pack'): # we need to strip these out, otherwise VRAD will get confused fast_args.remove(a) full_args.remove(a) fast_args = ['-bounce', '2', '-noextra'] + fast_args # Fast args: -bounce 2 -noextra -game $gamedir $path\$file # Final args: -both -final -staticproplighting -StaticPropPolys # -textureshadows -game $gamedir $path\$file if not path.endswith(".bsp"): path += ".bsp" if not os.path.exists(path): raise ValueError('"{}" does not exist!'.format(path)) if not os.path.isfile(path): raise ValueError('"{}" is not a file!'.format(path)) LOGGER.info('Reading BSP') bsp_file = BSP(path) # If VBSP marked it as Hammer, trust that. if srctools.conv_bool(bsp_file.ents.spawn['BEE2_is_peti']): is_peti = True # Detect preview via knowing the bsp name. If we are in preview, # check the config file to see what was specified there. if os.path.basename(path) == "preview.bsp": edit_args = not config.get_bool('General', 'vrad_force_full') # If shift is held, reverse. if utils.check_shift(): LOGGER.info('Shift held, inverting configured lighting option!') edit_args = not edit_args else: # publishing - always force full lighting. edit_args = False else: is_peti = edit_args = False if '-force_peti' in args or '-force_hammer' in args: # we have override commands! if '-force_peti' in args: LOGGER.warning('OVERRIDE: Applying cheap lighting!') is_peti = edit_args = True else: LOGGER.warning('OVERRIDE: Preserving args!') is_peti = edit_args = False LOGGER.info('Final status: is_peti={}, edit_args={}', is_peti, edit_args) if not is_peti: # Skip everything, if the user wants these features install the Hammer Addons postcompiler. LOGGER.info("Hammer map detected! Skipping all transforms.") run_vrad(full_args) return # Grab the currently mounted filesystems in P2. game = find_gameinfo(argv) root_folder = game.path.parent fsys = game.get_filesystem() # Special case - move the BEE2 filesystem FIRST, so we always pack files found there. for child_sys in fsys.systems[:]: if 'bee2' in child_sys[0].path.casefold(): fsys.systems.remove(child_sys) fsys.systems.insert(0, child_sys) zip_data = BytesIO() zip_data.write(bsp_file.get_lump(BSP_LUMPS.PAKFILE)) zipfile = ZipFile(zip_data) # Mount the existing packfile, so the cubemap files are recognised. fsys.add_sys(ZipFileSystem('<BSP pakfile>', zipfile)) LOGGER.info('Done!') LOGGER.debug('Filesystems:') for child_sys in fsys.systems[:]: LOGGER.debug('- {}: {!r}', child_sys[1], child_sys[0]) LOGGER.info('Reading our FGD files...') fgd = FGD.engine_dbase() packlist = PackList(fsys) LOGGER.info('Reading soundscripts...') packlist.load_soundscript_manifest( str(root_folder / 'bin/bee2/sndscript_cache.vdf') ) # We need to add all soundscripts in scripts/bee2_snd/ # This way we can pack those, if required. for soundscript in fsys.walk_folder('scripts/bee2_snd/'): if soundscript.path.endswith('.txt'): packlist.load_soundscript(soundscript, always_include=False) LOGGER.info('Reading particles....') packlist.load_particle_manifest() LOGGER.info('Loading transforms...') load_transforms() LOGGER.info('Checking for music:') music.generate(bsp_file.ents, packlist) LOGGER.info('Run transformations...') run_transformations(bsp_file.ents, fsys, packlist, bsp_file, game) LOGGER.info('Scanning map for files to pack:') packlist.pack_from_bsp(bsp_file) packlist.pack_fgd(bsp_file.ents, fgd) packlist.eval_dependencies() LOGGER.info('Done!') packlist.write_soundscript_manifest() packlist.write_particles_manifest(f'maps/{Path(path).stem}_particles.txt') # We need to disallow Valve folders. pack_whitelist: set[FileSystem] = set() pack_blacklist: set[FileSystem] = set() # Exclude absolutely everything except our folder. for child_sys, _ in fsys.systems: # Add 'bee2/' and 'bee2_dev/' only. if ( isinstance(child_sys, RawFileSystem) and 'bee2' in os.path.basename(child_sys.path).casefold() ): pack_whitelist.add(child_sys) else: pack_blacklist.add(child_sys) if config.get_bool('General', 'packfile_dump_enable'): dump_loc = Path(config.get_val( 'General', 'packfile_dump_dir', '../dump/' )).absolute() else: dump_loc = None if '-no_pack' not in args: # Cubemap files packed into the map already. existing = set(bsp_file.pakfile.namelist()) LOGGER.info('Writing to BSP...') packlist.pack_into_zip( bsp_file, ignore_vpk=True, whitelist=pack_whitelist, blacklist=pack_blacklist, dump_loc=dump_loc, ) LOGGER.info('Packed files:\n{}', '\n'.join( set(bsp_file.pakfile.namelist()) - existing )) LOGGER.info('Writing BSP...') bsp_file.save() LOGGER.info(' - BSP written!') screenshot.modify(config, game.path) if edit_args: LOGGER.info("Forcing Cheap Lighting!") run_vrad(fast_args) else: LOGGER.info("Publishing - Full lighting enabled! (or forced to do so)") run_vrad(full_args) LOGGER.info("BEE2 VRAD hook finished!")
import utils import srctools from typing import List, Tuple LOGGER = utils.getLogger(__name__) all_games = [] # type: List[Game] selected_game = None # type: Game selectedGame_radio = IntVar(value=0) game_menu = None # type: Menu # Translated text from basemodui.txt. TRANS_DATA = {} CONFIG = ConfigFile('games.cfg') FILES_TO_BACKUP = [ ('Editoritems', 'portal2_dlc2/scripts/editoritems', '.txt'), ('VBSP', 'bin/vbsp', '.exe'), ('VRAD', 'bin/vrad', '.exe'), ('VBSP', 'bin/vbsp_osx', ''), ('VRAD', 'bin/vrad_osx', ''), ('VBSP', 'bin/vbsp_linux', ''), ('VRAD', 'bin/vrad_linux', ''), ] _UNLOCK_ITEMS = [ 'ITEM_EXIT_DOOR', 'ITEM_COOP_EXIT_DOOR', 'ITEM_ENTRY_DOOR',
'brush': '0', 'ent': '0', 'overlay': '0', }, } PLAYER_MODELS = { 'ATLAS': 'ATLAS', 'PBODY': 'P-Body', 'SP': 'Chell', 'PETI': 'Bendy', } PLAYER_MODEL_ORDER = ['Bendy', 'Chell', 'ATLAS', 'P-Body'] PLAYER_MODELS_REV = {value: key for key, value in PLAYER_MODELS.items()} COMPILE_CFG = ConfigFile('compile.cfg') COMPILE_CFG.set_defaults(COMPILE_DEFAULTS) window = None UI = {} chosen_thumb = StringVar( value=COMPILE_CFG.get_val('Screenshot', 'Type', 'AUTO')) tk_screenshot = None # The preview image shown # Location we copy custom screenshots to SCREENSHOT_LOC = os.path.abspath( os.path.join(os.getcwd(), '..', 'config', 'screenshot.jpg')) player_model_var = StringVar(value=PLAYER_MODELS.get( COMPILE_CFG.get_val('General', 'player_model', 'PETI'), PLAYER_MODELS['PETI'],
def make_tab(group, config: ConfigFile, tab_type): """Create all the widgets for a tab.""" if tab_type is TabTypes.MIDCHAMBER: # Mid-chamber voice lines have predefined values. group_name = 'Mid - Chamber' group_id = 'MIDCHAMBER' group_desc = ( 'Lines played during the actual chamber, ' 'after specific events have occurred.' ) elif tab_type is TabTypes.RESPONSE: group_name = 'Responses' group_id = None group_desc = ( 'Lines played in response to certain events in Coop.' ) elif tab_type is TabTypes.NORM: group_name = group['name', 'No Name!'] group_id = group_name.upper() group_desc = group['desc', ''] + ':' else: raise ValueError('Invalid tab type!') # This is just to hold the canvas and scrollbar outer_frame = ttk.Frame(UI['tabs']) outer_frame.columnconfigure(0, weight=1) outer_frame.rowconfigure(0, weight=1) TABS[group_name] = outer_frame # We add this attribute so the refresh() method knows all the # tab names outer_frame.nb_text = group_name outer_frame.nb_type = tab_type # We need a canvas to make the list scrollable. canv = Canvas( outer_frame, highlightthickness=0, ) scroll = tk_tools.HidingScroll( outer_frame, orient=VERTICAL, command=canv.yview, ) canv['yscrollcommand'] = scroll.set canv.grid(row=0, column=0, sticky='NSEW') scroll.grid(row=0, column=1, sticky='NS') UI['tabs'].add(outer_frame) # This holds the actual elements frame = ttk.Frame( canv, ) frame.columnconfigure(0, weight=1) canv.create_window(0, 0, window=frame, anchor="nw") # We do this so we can adjust the scrollregion later in # <Configure>. canv.frame = frame ttk.Label( frame, text=group_name, anchor='center', font='tkHeadingFont', ).grid( row=0, column=0, sticky='EW', ) ttk.Label( frame, text=group_desc, ).grid( row=1, column=0, sticky='EW', ) ttk.Separator(frame, orient=HORIZONTAL).grid( row=2, column=0, sticky='EW', ) if tab_type is TabTypes.RESPONSE: sorted_quotes = sorted( group, key=lambda prop: prop.real_name ) else: sorted_quotes = sorted( group.find_all('Quote'), key=quote_sort_func, reverse=True, ) for quote in sorted_quotes: # type: Property if not quote.has_children(): continue # Skip over config commands.. if tab_type is TabTypes.RESPONSE: try: name = RESPONSE_NAMES[quote.name] except KeyError: # Convert channels of the form 'death_goo' into 'Death - Goo'. channel, ch_arg = quote.name.split('_', 1) name = channel.title() + ' - ' + ch_arg.title() del channel, ch_arg group_id = quote.name else: name = quote['name', 'No Name!'] ttk.Label( frame, text=name, font=QUOTE_FONT, ).grid( column=0, sticky=W, ) if tab_type is TabTypes.RESPONSE: line_iter = find_resp_lines(quote) else: line_iter = find_lines(quote) for badge, line, line_id in line_iter: if line_id is None: line_id = line['id', line['name']] check = ttk.Checkbutton( frame, text=line['name', 'No Name?'], compound=LEFT, image=badge, ) check.quote_var = IntVar( value=config.get_bool(group_id, line_id, True), ) check['variable'] = check.quote_var check['command'] = functools.partial( check_toggled, var=check.quote_var, config_section=config[group_id], quote_id=line_id, ) check.transcript = list(get_trans_lines(line)) check.grid( column=0, padx=(10, 0), sticky=W, ) check.bind("<Enter>", show_trans) canv.bind('<Configure>', configure_canv) return outer_frame
from query_dialogs import ask_string from BEE2_config import ConfigFile from property_parser import Property import utils import UI import loadScreen import extract_packages all_games = [] selected_game = None selectedGame_radio = IntVar(value=0) game_menu = None trans_data = {} config = ConfigFile('games.cfg') FILES_TO_BACKUP = [ ('Editoritems', 'portal2_dlc2/scripts/editoritems', '.txt'), ('VBSP', 'bin/vbsp', '.exe'), ('VRAD', 'bin/vrad', '.exe'), ('VBSP', 'bin/vbsp_osx', ''), ('VRAD', 'bin/vrad_osx', ''), ('VBSP', 'bin/vbsp_linux', ''), ('VRAD', 'bin/vrad_linux', ''), ] VOICE_PATHS = [ ('', '', 'normal'), ('MID_', 'mid_', 'MidChamber'), ]
def add_voice( voice_data, has_items, style_vars_, vmf_file, map_seed, mode='SP', ): """Add a voice line to the map.""" global ALLOW_MID_VOICES, VMF, map_attr, style_vars, GAME_MODE utils.con_log('Adding Voice Lines!') if len(voice_data.value) == 0: utils.con_log('Error - No Voice Line Data!') return VMF = vmf_file map_attr = has_items style_vars = style_vars_ GAME_MODE = mode norm_config = ConfigFile('voice.cfg', root='bee2') mid_config = ConfigFile('mid_voice.cfg', root='bee2') quote_base = voice_data['base', False] quote_loc = voice_data['quote_loc', '-10000 0 0'] if quote_base: print('Adding Base instance!') VMF.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) ALLOW_MID_VOICES = not style_vars.get('NoMidVoices', False) mid_quotes = [] for group in itertools.chain( voice_data.find_all('group'), voice_data.find_all('midinst'), ): quote_targetname = group['Choreo_Name', '@choreo'] possible_quotes = sorted( find_group_quotes( group, mid_quotes, conf=mid_config if group.name == 'midinst' else norm_config, ), key=sort_func, reverse=True, ) if possible_quotes: choreo_loc = group['choreo_loc', quote_loc] chosen = possible_quotes[0][1] utils.con_log('Chosen:', '\n'.join(map(repr, chosen))) # Join the IDs for the voice lines to the map seed, # so each quote block will chose different lines. random.seed(map_seed + '-VOICE_' + '|'.join(prop['id', 'ID'] for prop in chosen)) # Add one of the associated quotes add_quote(random.choice(chosen), quote_targetname, choreo_loc) print('Mid quotes: ', mid_quotes) for mid_item in mid_quotes: # Add all the mid quotes target = mid_item['target', ''] for prop in mid_item: add_quote(prop, target, quote_loc) utils.con_log('Done!')
def modify(conf: ConfigFile, game_folder: Path) -> None: """Modify the map's screenshot.""" mod_type = conf.get_val('Screenshot', 'type', 'PETI').lower() if mod_type == 'cust': LOGGER.info('Using custom screenshot!') scr_loc = str(utils.conf_location('screenshot.jpg')) elif mod_type == 'auto': LOGGER.info('Using automatic screenshot!') scr_loc = None # The automatic screenshots are found at this location: auto_path = os.path.join(game_folder, 'screenshots') # We need to find the most recent one. If it's named # "previewcomplete", we want to ignore it - it's a flag # to indicate the map was playtested correctly. try: screens = [ os.path.join(auto_path, path) for path in os.listdir(auto_path) ] except FileNotFoundError: # The screenshot folder doesn't exist! screens = [] screens.sort( key=os.path.getmtime, reverse=True, # Go from most recent to least ) playtested = False for scr_shot in screens: filename = os.path.basename(scr_shot) if filename.startswith('bee2_playtest_flag'): # Previewcomplete is a flag to indicate the map's # been playtested. It must be newer than the screenshot playtested = True continue elif filename.startswith('bee2_screenshot'): continue # Ignore other screenshots # We have a screenshot. Check to see if it's # not too old. (Old is > 2 hours) date = datetime.fromtimestamp(os.path.getmtime(scr_shot)) diff = datetime.now() - date if diff.total_seconds() > 2 * 3600: LOGGER.info( 'Screenshot "{scr}" too old ({diff!s})', scr=scr_shot, diff=diff, ) continue # If we got here, it's a good screenshot! LOGGER.info('Chosen "{}"', scr_shot) LOGGER.info('Map Playtested: {}', playtested) scr_loc = scr_shot break else: # If we get to the end, we failed to find an automatic # screenshot! LOGGER.info('No Auto Screenshot found!') mod_type = 'peti' # Suppress the "None not found" error if conf.get_bool('Screenshot', 'del_old'): LOGGER.info('Cleaning up screenshots...') # Clean up this folder - otherwise users will get thousands of # pics in there! for screen in screens: if screen != scr_loc and os.path.isfile(screen): os.remove(screen) LOGGER.info('Done!') else: # PeTI type, or something else scr_loc = None if scr_loc is not None and os.path.isfile(scr_loc): # We should use a screenshot! for screen in find(): LOGGER.info('Replacing "{}"...', screen) # Allow us to edit the file... utils.unset_readonly(screen) shutil.copy(scr_loc, screen) # Make the screenshot readonly, so P2 can't replace it. # Then it'll use our own utils.set_readonly(screen) else: if mod_type != 'peti': # Error if we were looking for a screenshot LOGGER.warning('"{}" not found!', scr_loc) LOGGER.info('Using PeTI screenshot!') for screen in find(): # Make the screenshot writeable, so P2 will replace it LOGGER.info('Making "{}" replaceable...', screen) utils.unset_readonly(screen)
from tkinter import ttk from tkinter import messagebox import tkinter as tk from app import TK_ROOT from app.CheckDetails import CheckDetails, Item as CheckItem from BEE2_config import ConfigFile import packages import utils window = tk.Toplevel(TK_ROOT) window.withdraw() UI = {} PACK_CONFIG = ConfigFile('packages.cfg') pack_items = {} HEADERS = ['Name'] def show(): """Show the manager window.""" window.deiconify() window.lift(TK_ROOT) window.grab_set() utils.center_win(window, TK_ROOT) window.update() UI['details'].refresh()
def show(quote_pack): """Display the editing window.""" global voice_item, config_sp, config_coop, config_mid_sp, config_mid_coop voice_item = quote_pack win.title('BEE2 - Configure "' + voice_item.name + '"') notebook = UI['tabs'] quote_data = quote_pack.config os.makedirs('config/voice', exist_ok=True) config_sp = ConfigFile('voice/SP_' + quote_pack.id + '.cfg') config_coop = ConfigFile('voice/COOP_' + quote_pack.id + '.cfg') config_mid_sp = ConfigFile('voice/MID_SP_' + quote_pack.id + '.cfg') config_mid_coop = ConfigFile('voice/MID_COOP_' + quote_pack.id + '.cfg') # Clear the transcript textbox text = UI['trans'] text['state'] = 'normal' text.delete(1.0, END) text['state'] = 'disabled' # Destroy all the old tabs for tab in itertools.chain( TABS_SP.values(), TABS_COOP.values(), ): try: notebook.forget(tab) except TclError: pass tab.destroy() TABS_SP.clear() TABS_COOP.clear() add_tabs(quote_data, 'quotes_sp', TABS_SP, config_sp) add_tabs(quote_data, 'quotes_coop', TABS_COOP, config_coop) config_sp.save() config_coop.save() config_mid_sp.save() config_mid_coop.save() refresh() win.deiconify() win.lift(win.winfo_parent()) utils.center_win(win) # Center inside the parent
def make_tab(group, config: ConfigFile, tab_type): """Create all the widgets for a tab.""" if tab_type is TabTypes.MIDCHAMBER: # Mid-chamber voice lines have predefined values. group_name = _('Mid - Chamber') group_id = 'MIDCHAMBER' group_desc = _('Lines played during the actual chamber, ' 'after specific events have occurred.') elif tab_type is TabTypes.RESPONSE: # Note: 'Response' tab header, and description group_name = _('Responses') group_id = None group_desc = _('Lines played in response to certain events in Coop.') elif tab_type is TabTypes.NORM: group_name = group['name', 'No Name!'] group_id = group_name.upper() group_desc = group['desc', ''] + ':' else: raise ValueError('Invalid tab type!') # This is just to hold the canvas and scrollbar outer_frame = ttk.Frame(UI['tabs']) outer_frame.columnconfigure(0, weight=1) outer_frame.rowconfigure(0, weight=1) TABS[group_name] = outer_frame # We add this attribute so the refresh() method knows all the # tab names outer_frame.nb_text = group_name outer_frame.nb_type = tab_type # We need a canvas to make the list scrollable. canv = Canvas( outer_frame, highlightthickness=0, ) scroll = tk_tools.HidingScroll( outer_frame, orient=VERTICAL, command=canv.yview, ) canv['yscrollcommand'] = scroll.set canv.grid(row=0, column=0, sticky='NSEW') scroll.grid(row=0, column=1, sticky='NS') UI['tabs'].add(outer_frame) # This holds the actual elements frame = ttk.Frame(canv, ) frame.columnconfigure(0, weight=1) canv.create_window(0, 0, window=frame, anchor="nw") # We do this so we can adjust the scrollregion later in # <Configure>. canv.frame = frame ttk.Label( frame, text=group_name, anchor='center', font='tkHeadingFont', ).grid( row=0, column=0, sticky='EW', ) ttk.Label( frame, text=group_desc, ).grid( row=1, column=0, sticky='EW', ) ttk.Separator(frame, orient=HORIZONTAL).grid( row=2, column=0, sticky='EW', ) if tab_type is TabTypes.RESPONSE: sorted_quotes = sorted(group, key=lambda prop: prop.real_name) else: sorted_quotes = sorted( group.find_all('Quote'), key=quote_sort_func, reverse=True, ) for quote in sorted_quotes: # type: Property if not quote.has_children(): continue # Skip over config commands.. if tab_type is TabTypes.RESPONSE: try: name = RESPONSE_NAMES[quote.name] except KeyError: # Convert channels of the form 'death_goo' into 'Death - Goo'. channel, ch_arg = quote.name.split('_', 1) name = channel.title() + ' - ' + ch_arg.title() del channel, ch_arg group_id = quote.name else: # note: default for quote names name = quote['name', _('No Name!')] ttk.Label( frame, text=name, font=QUOTE_FONT, ).grid( column=0, sticky=W, ) if tab_type is TabTypes.RESPONSE: line_iter = find_resp_lines(quote) else: line_iter = find_lines(quote) for badge, line, line_id in line_iter: if line_id is None: line_id = line['id', line['name']] check = ttk.Checkbutton( frame, # note: default voice line name next to checkbox. text=line['name', _('No Name?')], compound=LEFT, image=badge, ) check.quote_var = IntVar(value=config.get_bool( group_id, line_id, True), ) check['variable'] = check.quote_var check['command'] = functools.partial( check_toggled, var=check.quote_var, config_section=config[group_id], quote_id=line_id, ) check.transcript = list(get_trans_lines(line)) check.grid( column=0, padx=(10, 0), sticky=W, ) check.bind("<Enter>", show_trans) canv.bind('<Configure>', configure_canv) return outer_frame
def show(quote_pack): """Display the editing window.""" global voice_item, config, config_mid, config_resp if voice_item is not None: return voice_item = quote_pack win.title(_('BEE2 - Configure "{}"').format(voice_item.selitem_data.name)) notebook = UI['tabs'] quote_data = quote_pack.config os.makedirs('config/voice', exist_ok=True) config = ConfigFile('voice/' + quote_pack.id + '.cfg') config_mid = ConfigFile('voice/MID_' + quote_pack.id + '.cfg') config_resp = ConfigFile('voice/RESP_' + quote_pack.id + '.cfg') # Clear the transcript textbox text = UI['trans'] text['state'] = 'normal' text.delete(1.0, END) text['state'] = 'disabled' # Destroy all the old tabs for tab in TABS.values(): try: notebook.forget(tab) except TclError: pass tab.destroy() TABS.clear() for group in quote_data.find_all('quotes', 'group'): make_tab(group, config, TabTypes.NORM) # Merge all blocks into one mid_quotes = Property( 'midChamber', list( itertools.chain.from_iterable( quote_data.find_all('quotes', 'midChamber')))) if len(mid_quotes): make_tab( mid_quotes, config_mid, TabTypes.MIDCHAMBER, ) responses = Property( 'CoopResponses', list( itertools.chain.from_iterable( quote_data.find_all('quotes', 'CoopResponses'))), ) if len(responses): make_tab( responses, config_resp, TabTypes.RESPONSE, ) config.save() config_mid.save() config_resp.save() add_tabs() win.deiconify() utils.center_win(win) # Center inside the parent win.lift()
def add_voice( has_items: dict, style_vars_: dict, vmf_file_: VMF, map_seed: str, use_priority=True, ): """Add a voice line to the map.""" global ALLOW_MID_VOICES, vmf_file, map_attr, style_vars LOGGER.info('Adding Voice Lines!') vmf_file = vmf_file_ map_attr = has_items style_vars = style_vars_ norm_config = ConfigFile('voice.cfg', root='bee2') mid_config = ConfigFile('mid_voice.cfg', root='bee2') quote_base = QUOTE_DATA['base', False] quote_loc = Vec.from_str(QUOTE_DATA['quote_loc', '-10000 0 0'], x=-10000) if quote_base: LOGGER.info('Adding Base instance!') vmf_file.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Either box in with nodraw, or place the voiceline studio. has_studio = conditions.monitor.make_voice_studio(vmf_file, quote_loc) bullsye_actor = vbsp_options.get(str, 'voice_studio_actor') if bullsye_actor and has_studio: ADDED_BULLSEYES.add(bullsye_actor) global_bullseye = QUOTE_DATA['bullseye', ''] if global_bullseye: add_bullseye(quote_loc, global_bullseye) ALLOW_MID_VOICES = not style_vars.get('nomidvoices', False) mid_quotes = [] # Enable using the beep before and after choreo lines. allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0']) if allow_dings: vmf_file.create_ent( classname='logic_choreographed_scene', targetname='@ding_on', origin=quote_loc + (-8, -16, 0), scenefile='scenes/npc/glados_manual/ding_on.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) vmf_file.create_ent( classname='logic_choreographed_scene', targetname='@ding_off', origin=quote_loc + (8, -16, 0), scenefile='scenes/npc/glados_manual/ding_off.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) # QuoteEvents allows specifiying an instance for particular items, # so a voice line can be played at a certain time. It's only active # in certain styles, but uses the default if not set. for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'): event_id = event['id', ''].casefold() # We ignore the config if no result was executed. if event_id and event_id in QUOTE_EVENTS: # Instances from the voiceline config are in this subfolder, # but not the default item - that's set from the conditions QUOTE_EVENTS[event_id] = INST_PREFIX + event['file'] LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys())) if has_responses(): LOGGER.info('Generating responses data..') with open(RESP_LOC, 'w') as f: generate_resp_script(f, allow_dings) else: LOGGER.info('No responses data..') try: os.remove(RESP_LOC) except FileNotFoundError: pass for ind, file in enumerate(QUOTE_EVENTS.values()): if not file: continue vmf_file.create_ent( classname='func_instance', targetname='voice_event_' + str(ind), file=file, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): quote_targetname = group['Choreo_Name', '@choreo'] use_dings = srctools.conv_bool(group['use_dings', ''], allow_dings) possible_quotes = sorted( find_group_quotes( group, mid_quotes, use_dings, conf=mid_config if group.name == 'midchamber' else norm_config, mid_name=quote_targetname, ), key=sort_func, reverse=True, ) if possible_quotes: choreo_loc = Vec.from_str(group['choreo_loc', quote_loc]) if use_priority: chosen = possible_quotes[0].lines else: # Chose one of the quote blocks.. random.seed('{}-VOICE_QUOTE_{}'.format( map_seed, len(possible_quotes), )) chosen = random.choice(possible_quotes).lines # Join the IDs for # the voice lines to the map seed, # so each quote block will chose different lines. random.seed(map_seed + '-VOICE_LINE_' + '|'.join(prop['id', 'ID'] for prop in chosen)) # Add one of the associated quotes add_quote( random.choice(chosen), quote_targetname, choreo_loc, use_dings, ) if ADDED_BULLSEYES or srctools.conv_bool(QUOTE_DATA['UseMicrophones', '']): # Add microphones that broadcast audio directly at players. # This ensures it is heard regardless of location. # This is used for Cave and core Wheatley. LOGGER.info('Using microphones...') if vbsp.GAME_MODE == 'SP': vmf_file.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='386', origin=quote_loc, ) else: vmf_file.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='386', origin=quote_loc, ) vmf_file.create_ent( classname='env_microphone', targetname='player_speaker_orange', speakername='!player_orange', maxRange='386', origin=quote_loc, ) LOGGER.info('{} Mid quotes', len(mid_quotes)) for mid_lines in mid_quotes: line = random.choice(mid_lines) mid_item, use_ding, mid_name = line add_quote(mid_item, mid_name, quote_loc, use_ding) LOGGER.info('Done!')
from enum import Enum, EnumMeta import inspect from srctools import Property, Vec, parse_vec_str from BEE2_config import ConfigFile import srctools.logger from typing import Union, Tuple, TypeVar, Type, Optional, Iterator, Any, TextIO LOGGER = srctools.logger.get_logger(__name__) SETTINGS = {} # Overwritten by VBSP to get the actual values. ITEM_CONFIG = ConfigFile(None) class TYPE(Enum): """The types arguments can have.""" STR = str INT = int FLOAT = float BOOL = bool VEC = Vec def convert(self, value: str) -> Any: """Convert a string to the desired argument type.""" return self.value(value)
'max_overlay': '512', 'max_entity': '2048', }, 'CorridorNames': {}, } PLAYER_MODELS = { 'ATLAS': _('ATLAS'), 'PBODY': _('P-Body'), 'SP': _('Chell'), 'PETI': _('Bendy'), } PLAYER_MODEL_ORDER = ['PETI', 'SP', 'ATLAS', 'PBODY'] PLAYER_MODELS_REV = {value: key for key, value in PLAYER_MODELS.items()} COMPILE_CFG = ConfigFile('compile.cfg') COMPILE_CFG.set_defaults(COMPILE_DEFAULTS) window = None UI = {} chosen_thumb = StringVar( value=COMPILE_CFG.get_val('Screenshot', 'Type', 'AUTO') ) tk_screenshot = None # The preview image shown # Location we copy custom screenshots to SCREENSHOT_LOC = os.path.abspath(os.path.join( os.getcwd(), '..', 'config', 'screenshot.jpg'
import inspect import io from srctools import Property, Vec, parse_vec_str from BEE2_config import ConfigFile import srctools import utils from typing import Union, Tuple, TypeVar, Type, Optional, Iterator LOGGER = utils.getLogger(__name__) SETTINGS = {} # Overwritten by VBSP to get the actual values. ITEM_CONFIG = ConfigFile('', root='', auto_load=False) class TYPE(Enum): """The types arguments can have.""" STR = str INT = int FLOAT = float BOOL = bool VEC = Vec TYPE_NAMES = { TYPE.STR: 'Text', TYPE.INT: 'Whole Number', TYPE.FLOAT: 'Decimal Number',
def add_voice( voice_attrs: dict, style_vars: dict, vmf: VMF, map_seed: str, use_priority=True, ) -> None: """Add a voice line to the map.""" from precomp.conditions.monitor import make_voice_studio LOGGER.info('Adding Voice Lines!') norm_config = ConfigFile('bee2/voice.cfg', in_conf_folder=False) mid_config = ConfigFile('bee2/mid_voice.cfg', in_conf_folder=False) quote_base = QUOTE_DATA['base', False] quote_loc = get_studio_loc() if quote_base: LOGGER.info('Adding Base instance!') vmf.create_ent( classname='func_instance', targetname='voice', file=INST_PREFIX + quote_base, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Either box in with nodraw, or place the voiceline studio. has_studio = make_voice_studio(vmf) bullsye_actor = vbsp_options.get(str, 'voice_studio_actor') if bullsye_actor and has_studio: ADDED_BULLSEYES.add(bullsye_actor) global_bullseye = QUOTE_DATA['bullseye', ''] if global_bullseye: add_bullseye(vmf, quote_loc, global_bullseye) allow_mid_voices = not style_vars.get('nomidvoices', False) mid_quotes = [] # Enable using the beep before and after choreo lines. allow_dings = srctools.conv_bool(QUOTE_DATA['use_dings', '0']) if allow_dings: vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_on', origin=quote_loc + (-8, -16, 0), scenefile='scenes/npc/glados_manual/ding_on.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) vmf.create_ent( classname='logic_choreographed_scene', targetname='@ding_off', origin=quote_loc + (8, -16, 0), scenefile='scenes/npc/glados_manual/ding_off.vcd', busyactor="1", # Wait for actor to stop talking onplayerdeath='0', ) # QuoteEvents allows specifying an instance for particular items, # so a voice line can be played at a certain time. It's only active # in certain styles, but uses the default if not set. for event in QUOTE_DATA.find_all('QuoteEvents', 'Event'): event_id = event['id', ''].casefold() # We ignore the config if no result was executed. if event_id and event_id in QUOTE_EVENTS: # Instances from the voiceline config are in this subfolder, # but not the default item - that's set from the conditions QUOTE_EVENTS[event_id] = INST_PREFIX + event['file'] LOGGER.info('Quote events: {}', list(QUOTE_EVENTS.keys())) if has_responses(): LOGGER.info('Generating responses data..') encode_coop_responses(vmf, quote_loc, allow_dings, voice_attrs) for ind, file in enumerate(QUOTE_EVENTS.values()): if not file: continue vmf.create_ent( classname='func_instance', targetname='voice_event_' + str(ind), file=file, angles='0 0 0', origin=quote_loc, fixup_style='0', ) # Determine the flags that enable/disable specific lines based on which # players are used. player_model = vbsp.BEE2_config.get_val( 'General', 'player_model', 'PETI', ).casefold() is_coop = (vbsp.GAME_MODE == 'COOP') is_sp = (vbsp.GAME_MODE == 'SP') player_flags = { 'sp': is_sp, 'coop': is_coop, 'atlas': is_coop or player_model == 'atlas', 'pbody': is_coop or player_model == 'pbody', 'bendy': is_sp and player_model == 'peti', 'chell': is_sp and player_model == 'sp', 'human': is_sp and player_model in ('peti', 'sp'), 'robot': is_coop or player_model in ('atlas', 'pbody'), } # All which are True. player_flag_set = {val for val, flag in player_flags.items() if flag} # For each group, locate the voice lines. for group in itertools.chain( QUOTE_DATA.find_all('group'), QUOTE_DATA.find_all('midchamber'), ): # type: Property quote_targetname = group['Choreo_Name', '@choreo'] use_dings = group.bool('use_dings', allow_dings) possible_quotes = sorted( find_group_quotes( vmf, group, mid_quotes, use_dings=use_dings, allow_mid_voices=allow_mid_voices, conf=mid_config if group.name == 'midchamber' else norm_config, mid_name=quote_targetname, player_flag_set=player_flag_set, ), key=sort_func, reverse=True, ) LOGGER.debug('Possible {}quotes:', 'mid ' if group.name == 'midchamber' else '') for quot in possible_quotes: LOGGER.debug('- {}', quot) if possible_quotes: choreo_loc = group.vec('choreo_loc', *quote_loc) if use_priority: chosen = possible_quotes[0].lines else: # Chose one of the quote blocks.. random.seed('{}-VOICE_QUOTE_{}'.format( map_seed, len(possible_quotes), )) chosen = random.choice(possible_quotes).lines # Join the IDs for # the voice lines to the map seed, # so each quote block will chose different lines. random.seed(map_seed + '-VOICE_LINE_' + '|'.join(prop['id', 'ID'] for prop in chosen)) # Add one of the associated quotes add_quote( vmf, random.choice(chosen), quote_targetname, choreo_loc, style_vars, use_dings, ) if ADDED_BULLSEYES or QUOTE_DATA.bool('UseMicrophones'): # Add microphones that broadcast audio directly at players. # This ensures it is heard regardless of location. # This is used for Cave and core Wheatley. LOGGER.info('Using microphones...') if vbsp.GAME_MODE == 'SP': vmf.create_ent( classname='env_microphone', targetname='player_speaker_sp', speakername='!player', maxRange='386', origin=quote_loc, ) else: vmf.create_ent( classname='env_microphone', targetname='player_speaker_blue', speakername='!player_blue', maxRange='386', origin=quote_loc, ) vmf.create_ent( classname='env_microphone', targetname='player_speaker_orange', speakername='!player_orange', maxRange='386', origin=quote_loc, ) LOGGER.info('{} Mid quotes', len(mid_quotes)) for mid_lines in mid_quotes: line = random.choice(mid_lines) mid_item, use_ding, mid_name = line add_quote(vmf, mid_item, mid_name, quote_loc, style_vars, use_ding) LOGGER.info('Done!')
import math from enum import Enum, EnumMeta import inspect from srctools import Property, Vec, parse_vec_str from BEE2_config import ConfigFile import srctools.logger from typing import Union, Tuple, TypeVar, Type, Optional, Iterator, Any, TextIO LOGGER = srctools.logger.get_logger(__name__) SETTINGS = {} ITEM_CONFIG = ConfigFile('item_cust_configs.cfg') class TYPE(Enum): """The types arguments can have.""" STR = str INT = int FLOAT = float BOOL = bool VEC = Vec def convert(self, value: str) -> Any: """Convert a string to the desired argument type.""" return self.value(value)
def show(quote_pack): """Display the editing window.""" global voice_item, config, config_mid, config_resp voice_item = quote_pack win.title('BEE2 - Configure "' + voice_item.selitem_data.name + '"') notebook = UI['tabs'] quote_data = quote_pack.config os.makedirs('config/voice', exist_ok=True) config = ConfigFile('voice/' + quote_pack.id + '.cfg') config_mid = ConfigFile('voice/MID_' + quote_pack.id + '.cfg') config_resp = ConfigFile('voice/RESP_' + quote_pack.id + '.cfg') # Clear the transcript textbox text = UI['trans'] text['state'] = 'normal' text.delete(1.0, END) text['state'] = 'disabled' # Destroy all the old tabs for tab in TABS.values(): try: notebook.forget(tab) except TclError: pass tab.destroy() TABS.clear() for group in quote_data.find_all('quotes', 'group'): make_tab( group, config, TabTypes.NORM ) # Merge all blocks into one mid_quotes = Property( 'midChamber', list(itertools.chain.from_iterable( quote_data.find_all('quotes', 'midChamber') )) ) if len(mid_quotes): make_tab( mid_quotes, config_mid, TabTypes.MIDCHAMBER, ) responses = Property( 'CoopResponses', list(itertools.chain.from_iterable( quote_data.find_all('quotes', 'CoopResponses') )), ) if len(responses): make_tab( responses, config_resp, TabTypes.RESPONSE, ) config.save() config_mid.save() config_resp.save() add_tabs() win.deiconify() win.lift(win.winfo_parent()) utils.center_win(win) # Center inside the parent
'brush': '0', 'ent': '0', 'overlay': '0', }, } PLAYER_MODELS = { 'ATLAS': 'ATLAS', 'PBODY': 'P-Body', 'SP': 'Chell', 'PETI': 'Bendy', } PLAYER_MODEL_ORDER = ['Bendy', 'Chell', 'ATLAS', 'P-Body'] PLAYER_MODELS_REV = {value: key for key, value in PLAYER_MODELS.items()} COMPILE_CFG = ConfigFile('compile.cfg') COMPILE_CFG.set_defaults(COMPILE_DEFAULTS) window = None UI = {} chosen_thumb = StringVar( value=COMPILE_CFG.get_val('Screenshot', 'Type', 'AUTO') ) player_model_var = StringVar( value=PLAYER_MODELS.get( COMPILE_CFG.get_val('General', 'player_model', 'PETI'), PLAYER_MODELS['PETI'], ) ) start_in_elev = IntVar(value=0) cust_file_loc = COMPILE_CFG.get_val('Screenshot', 'Loc', '')
'brush': '0', 'ent': '0', 'overlay': '0', }, } PLAYER_MODELS = { 'ATLAS': 'ATLAS', 'PBODY': 'P-Body', 'SP': 'Chell', 'PETI': 'Bendy', } PLAYER_MODEL_ORDER = ['Bendy', 'Chell', 'ATLAS', 'P-Body'] PLAYER_MODELS_REV = {value: key for key, value in PLAYER_MODELS.items()} COMPILE_CFG = ConfigFile('compile.cfg') COMPILE_CFG.set_defaults(COMPILE_DEFAULTS) window = None UI = {} chosen_thumb = StringVar( value=COMPILE_CFG.get_val('Screenshot', 'Type', 'AUTO') ) tk_screenshot = None # The preview image shown # Location we copy custom screenshots to SCREENSHOT_LOC = os.path.abspath(os.path.join( os.getcwd(), '..', 'config', 'screenshot.jpg'