def load_transforms() -> None: """Load all the BSP transforms. We need to do this differently when frozen, since they're embedded in our executable. """ # Find the modules in the conditions package. # PyInstaller messes this up a bit. if utils.FROZEN: # This is the PyInstaller loader injected during bootstrap. # See PyInstaller/loader/pyimod03_importers.py # toc is a PyInstaller-specific attribute containing a set of # all frozen modules. loader = pkgutil.get_loader('postcomp.transforms') for module in loader.toc: if module.startswith('postcomp.transforms'): LOGGER.debug('Importing transform {}', module) sys.modules[module] = importlib.import_module(module) else: # We can just delegate to the regular postcompiler finder. try: transform_loc = Path(os.environ['BSP_TRANSFORMS']) except KeyError: transform_loc = utils.install_path('../HammerAddons/transforms/') if not transform_loc.exists(): raise ValueError( f'Invalid BSP transforms location "{transform_loc.resolve()}"!\n' 'Clone TeamSpen210/HammerAddons next to BEE2.4, or set the ' 'environment variable BSP_TRANSFORMS to the location.' ) finder = PluginFinder('postcomp.transforms', [ PluginSource(transform_loc, recurse=True), ]) sys.meta_path.append(finder) finder.load_all()
def load_transforms() -> None: """Load all the BSP transforms. We need to do this differently when frozen, since they're embedded in our executable. """ if utils.FROZEN: # We embedded a copy of all the transforms in this package, which auto-imports the others. # noinspection PyUnresolvedReferences from postcomp import transforms else: # We can just delegate to the regular postcompiler finder. try: transform_loc = Path(os.environ['BSP_TRANSFORMS']) except KeyError: transform_loc = utils.install_path('../HammerAddons/transforms/') if not transform_loc.exists(): raise ValueError( f'Invalid BSP transforms location "{transform_loc.resolve()}"!\n' 'Clone TeamSpen210/HammerAddons next to BEE2.4, or set the ' 'environment variable BSP_TRANSFORMS to the location.' ) finder = PluginFinder('postcomp.transforms', [ PluginSource(transform_loc, recurse=True), ]) sys.meta_path.append(finder) finder.load_all()
def load_snd() -> None: """Load all the sounds.""" for name, fname in SOUNDS.items(): LOGGER.info('Loading sound "{}" -> sounds/{}.ogg', name, fname) SOUNDS[name] = pyglet.media.load( str(utils.install_path('sounds/{}.ogg'.format(fname))), streaming=False, )
def _load_special(path: str) -> Image.Image: """Various special images we have to load.""" img: Image.Image try: img = Image.open(utils.install_path(f'images/BEE2/{path}.png')) img.load() return img.convert('RGBA') except Exception: LOGGER.warning('"{}" icon could not be loaded!', path, exc_info=True) return Image.new('RGBA', (64, 64), PETI_ITEM_BG)
def ticker() -> None: """We need to constantly trigger pyglet.clock.tick(). Instead of re-registering this, cache off the command name. Additionally, load sounds gradually in the background. """ if _todo: name = _todo.pop() fname = SOUNDS[name] path = str(utils.install_path('sounds/{}.ogg'.format(fname))) LOGGER.info('Loading sound "{}" -> {}', name, path) SOUNDS[name] = pyglet.media.load(path, streaming=False) tick() TK_ROOT.tk.call(ticker_cmd)
def fx(name, e=None): """Play a sound effect stored in the sounds{} dict.""" if not play_sound: return # Defer loading these until we actually need them, speeds up # startup a little. try: sound = SOUNDS[name] except KeyError: raise ValueError(f'Not a valid sound? "{name}"') if type(sound) is str: LOGGER.info('Loading sound "{}" -> sounds/{}.ogg', name, sound) sound = SOUNDS[name] = pyglet.media.load( str(utils.install_path('sounds/{}.ogg'.format(sound))), streaming=False, ) sound.play()
def edit_fgd(self, add_lines: bool = False) -> None: """Add our FGD files to the game folder. This is necessary so that VBSP offsets the entities properly, if they're in instances. Add_line determines if we are adding or removing it. """ # We do this in binary to ensure non-ASCII characters pass though # untouched. fgd_path = self.abs_path('bin/portal2.fgd') try: with open(fgd_path, 'rb') as file: data = file.readlines() except FileNotFoundError: LOGGER.warning('No FGD file? ("{}")', fgd_path) return for i, line in enumerate(data): match = re.match( br'// BEE\W*2 EDIT FLAG\W*=\W*([01])', line, re.IGNORECASE, ) if match: if match.group(1) == b'0': LOGGER.info('FGD editing disabled by file.') return # User specifically disabled us. # Delete all data after this line. del data[i:] break with srctools.AtomicWriter(fgd_path, is_bytes=True) as file: for line in data: file.write(line) if add_lines: file.write( b'// BEE 2 EDIT FLAG = 1 \n' b'// Added automatically by BEE2. Set above to "0" to ' b'allow editing below text without being overwritten.\n' b'\n\n') with utils.install_path('BEE2.fgd').open('rb') as bee2_fgd: shutil.copyfileobj(bee2_fgd, file) file.write(imp_res_read_binary(srctools, 'srctools.fgd'))
async def load(self, name: str) -> Optional[Source]: """Load the given UI sound into a source.""" global sounds fname = SOUNDS[name] path = str(utils.install_path('sounds/{}.ogg'.format(fname))) LOGGER.info('Loading sound "{}" -> {}', name, path) try: src = await trio.to_thread.run_sync( functools.partial( decoder.decode, file=None, filename=path, streaming=False, )) except Exception: LOGGER.exception("Couldn't load sound {}:", name) LOGGER.info('UI sounds disabled.') sounds = NullSound() _nursery.cancel_scope.cancel() return None else: self.sources[name] = src return src
def load_palettes(): """Scan and read in all palettes in the specified directory.""" # Load our builtin palettes: for builtin_pal in utils.install_path('palettes/').glob('*' + PAL_EXT): LOGGER.info('Loading builtin "{}"', builtin_pal.stem) pal_list.append(Palette.parse(str(builtin_pal))) for name in os.listdir(PAL_DIR): # this is both files and dirs LOGGER.info('Loading "{}"', name) path = os.path.join(PAL_DIR, name) pos_file, prop_file = None, None try: if name.endswith(PAL_EXT): try: pal_list.append(Palette.parse(path)) except KeyValError as exc: # We don't need the traceback, this isn't an error in the app # itself. LOGGER.warning( 'Could not parse palette file, skipping:\n{}', exc) continue elif name.endswith('.zip'): # Extract from a zip with zipfile.ZipFile(path) as zip_file: pos_file = zip_file.open('positions.txt') prop_file = zip_file.open('properties.txt') elif os.path.isdir(path): # Open from the subfolder pos_file = open(os.path.join(path, 'positions.txt')) prop_file = open(os.path.join(path, 'properties.txt')) else: # A non-palette file, skip it. LOGGER.debug('Skipping "{}"', name) continue except (KeyError, FileNotFoundError, zipfile.BadZipFile): # KeyError is returned by zipFile.open() if file is not present LOGGER.warning('Bad palette file "{}"!', name) continue else: # Legacy parsing of BEE2.2 files.. pal = parse_legacy(pos_file, prop_file, name) if pal is not None: pal_list.append(pal) finally: if pos_file: pos_file.close() if prop_file: prop_file.close() LOGGER.warning('"{}" is a legacy palette - resaving!', name) # Resave with the new format, then delete originals. if name.endswith('.zip'): pal.save() os.remove(path) else: # Folders can't be overwritten... pal.prevent_overwrite = True pal.save() shutil.rmtree(path) # Ensure the list has a defined order.. pal_list.sort(key=str) return pal_list
def make_splash_screen( max_width: float, max_height: float, base_height: int, text1_bbox: tuple[int, int, int, int], text2_bbox: tuple[int, int, int, int], ) -> tuple[tk.PhotoImage, int, int]: """Create the splash screen image. This uses a random screenshot from the splash_screens directory. It then adds the gradients on top. """ import random folder = utils.install_path('images/splash_screen') user_folder = folder / 'user' path = Path('<nothing>') if user_folder.exists(): folder = user_folder try: path = random.choice(list(folder.iterdir())) with path.open('rb') as img_file: image = Image.open(img_file) image.load() except (FileNotFoundError, IndexError, IOError): # Not found, substitute a gray block. LOGGER.warning('No splash screen found (tried "{}")', path) image = Image.new( mode='RGB', size=(round(max_width), round(max_height)), color=(128, 128, 128), ) else: if image.height > max_height: image = image.resize(( round(image.width / image.height * max_height), round(max_height), )) if image.width > max_width: image = image.resize(( round(max_width), round(image.height / image.width * max_width), )) draw = ImageDraw.Draw(image, 'RGBA') rect_top = image.height - base_height - 40 draw.rectangle( ( 0, rect_top + 40, image.width, image.height, ), fill=(0, 150, 120, 64), ) # Add a gradient above the rectangle.. for y in range(40): draw.rectangle( ( 0, rect_top + y, image.width, image.height, ), fill=(0, 150, 120, int(y * 128/40)), ) # Draw the shadows behind the text. # This is done by progressively drawing smaller rectangles # with a low alpha. The center is overdrawn more making it thicker. for x1, y1, x2, y2 in [text1_bbox, text2_bbox]: for border in reversed(range(5)): draw.rectangle( ( x1 - border, y1 - border, x2 + border, y2 + border, ), fill=(0, 150, 120, 20), ) logo_img = Image.open(utils.install_path('images/BEE2/splash_logo.png')) draw.bitmap((10, 10), logo_img) tk_img = ImageTk.PhotoImage(image=image) return tk_img, image.width, image.height
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
tkImgWidgets = Union[tk.Label, ttk.Label, tk.Button, ttk.Button] tkImgWidgetsT = TypeVar('tkImgWidgetsT', tk.Label, ttk.Label, tk.Button, ttk.Button) WidgetWeakRef = Union['WeakRef[tk.Label], WeakRef[ttk.Label], WeakRef[tk.Button], WeakRef[ttk.Button]'] ArgT = TypeVar('ArgT') # Used to keep track of the used handles, so we can deduplicate them. _handles: dict[tuple, Handle] = {} # Matches widgets to the handle they use. _wid_tk: dict[WidgetWeakRef, Handle] = {} # TK images have unique IDs, so preserve discarded image objects. _unused_tk_img: dict[tuple[int, int], list[tk.PhotoImage]] = {} LOGGER = srctools.logger.get_logger('img') FSYS_BUILTIN = RawFileSystem(str(utils.install_path('images'))) PACK_SYSTEMS: dict[str, FileSystem] = {} # Silence DEBUG messages from Pillow, they don't help. logging.getLogger('PIL').setLevel(logging.INFO) # Colour of the palette item background PETI_ITEM_BG = (229, 232, 233) PETI_ITEM_BG_HEX = '#{:2X}{:2X}{:2X}'.format(*PETI_ITEM_BG) def _load_special(path: str) -> Image.Image: """Various special images we have to load.""" img: Image.Image try: img = Image.open(utils.install_path(f'images/BEE2/{path}.png'))
"""Backup and restore P2C maps. """ import utils import srctools.logger import tk_tools if __name__ == '__main__': utils.fix_cur_directory() LOGGER = srctools.logger.init_logging( str(utils.install_path('logs/backup.log')), __name__, on_error=tk_tools.on_error, ) utils.setup_localisations(LOGGER) else: LOGGER = srctools.logger.get_logger(__name__) import tkinter as tk from tkinter import ttk from tkinter import filedialog from tkinter import messagebox from tk_tools import TK_ROOT from datetime import datetime from io import BytesIO, TextIOWrapper import time import os import shutil import string
def setup(logger: logging.Logger) -> None: """Setup gettext localisations.""" global _TRANSLATOR # Get the 'en_US' style language code lang_code = locale.getdefaultlocale()[0] # Allow overriding through command line. if len(sys.argv) > 1: for arg in sys.argv[1:]: if arg.casefold().startswith('lang='): lang_code = arg[5:] break # Expands single code to parent categories. expanded_langs = gettext_mod._expand_lang(lang_code) logger.info('Language: {!r}', lang_code) logger.debug('Language codes: {!r}', expanded_langs) # Add these to Property's default flags, so config files can also # be localised. for lang in expanded_langs: PROP_FLAGS_DEFAULT['lang_' + lang] = True lang_folder = utils.install_path('i18n') for lang in expanded_langs: try: file = open(lang_folder / (lang + '.mo').format(lang), 'rb') except FileNotFoundError: continue with file: _TRANSLATOR = gettext_mod.GNUTranslations(file) break else: # To help identify missing translations, replace everything with # something noticeable. if lang_code == 'dummy': _TRANSLATOR = DummyTranslations() # No translations, fallback to English. # That's fine if the user's language is actually English. else: if 'en' not in expanded_langs: logger.warning( "Can't find translation for codes: {!r}!", expanded_langs, ) _TRANSLATOR = gettext_mod.NullTranslations() # Add these functions to builtins, plus _=gettext _TRANSLATOR.install(['gettext', 'ngettext']) # Override the global funcs, to more efficiently delegate if people import # later. globals()['gettext'] = _TRANSLATOR.gettext globals()['ngettext'] = _TRANSLATOR.ngettext # Some lang-specific overrides.. if gettext('__LANG_USE_SANS_SERIF__') == 'YES': # For Japanese/Chinese, we want a 'sans-serif' / gothic font # style. try: from tkinter import font except ImportError: return font_names = [ 'TkDefaultFont', 'TkHeadingFont', 'TkTooltipFont', 'TkMenuFont', 'TkTextFont', 'TkCaptionFont', 'TkSmallCaptionFont', 'TkIconFont', # Note - not fixed-width... ] for font_name in font_names: font.nametofont(font_name).configure(family='sans-serif')
"""Backup and restore P2C maps. """ import utils import srctools.logger import tk_tools if __name__ == '__main__': utils.fix_cur_directory() LOGGER = srctools.logger.init_logging( str(utils.install_path('logs/backup.log')), __name__, on_error=tk_tools.on_error, ) utils.setup_localisations(LOGGER) else: LOGGER = srctools.logger.get_logger(__name__) import tkinter as tk from tkinter import ttk from tkinter import filedialog from tkinter import messagebox from tk_tools import TK_ROOT from datetime import datetime from io import BytesIO, TextIOWrapper import time import os import shutil import string import atexit
import tk_tools import utils from srctools.logger import init_logging, get_logger if __name__ == '__main__': utils.fix_cur_directory() LOGGER = init_logging(str(utils.install_path('logs/compiler_pane.log')), __name__, on_error=tk_tools.on_error) utils.setup_localisations(LOGGER) else: LOGGER = get_logger(__name__) from tkinter import * from tk_tools import TK_ROOT, FileField from tkinter import ttk from tkinter import filedialog from PIL import Image, ImageTk from BEE2_config import ConfigFile, GEN_OPTS, option_handler from packageLoader import CORRIDOR_COUNTS, CorrDesc from tooltip import add_tooltip, set_tooltip from srctools import Property, AtomicWriter import selectorWin import tkMarkdown import SubPane import img import base64 from typing import Dict, Tuple, Optional
"""Run the BEE2.""" # First do a few things as early as possible. import utils import srctools.logger utils.fix_cur_directory() # We need to initialise logging as early as possible - that way # it can record any errors in the initialisation of modules. import tk_tools LOGGER = srctools.logger.init_logging( str(utils.install_path('logs/BEE2.log')), on_error=tk_tools.on_error, ) utils.setup_localisations(LOGGER) # BEE2_config creates this config file to allow easy cross-module access from BEE2_config import GEN_OPTS from tk_tools import TK_ROOT import UI import loadScreen import paletteLoader import packageLoader import gameMan import logWindow import img import music_conf
def load_palettes(): """Scan and read in all palettes in the specified directory.""" # Load our builtin palettes: for builtin_pal in utils.install_path('palettes/').glob('*' + PAL_EXT): LOGGER.info('Loading builtin "{}"', builtin_pal.stem) pal_list.append(Palette.parse(str(builtin_pal))) for name in os.listdir(PAL_DIR): # this is both files and dirs LOGGER.info('Loading "{}"', name) path = os.path.join(PAL_DIR, name) pos_file, prop_file = None, None try: if name.endswith(PAL_EXT): try: pal_list.append(Palette.parse(path)) except KeyValError as exc: # We don't need the traceback, this isn't an error in the app # itself. LOGGER.warning('Could not parse palette file, skipping:\n{}', exc) continue elif name.endswith('.zip'): # Extract from a zip with zipfile.ZipFile(path) as zip_file: pos_file = zip_file.open('positions.txt') prop_file = zip_file.open('properties.txt') elif os.path.isdir(path): # Open from the subfolder pos_file = open(os.path.join(path, 'positions.txt')) prop_file = open(os.path.join(path, 'properties.txt')) else: # A non-palette file, skip it. LOGGER.debug('Skipping "{}"', name) continue except (KeyError, FileNotFoundError, zipfile.BadZipFile): # KeyError is returned by zipFile.open() if file is not present LOGGER.warning('Bad palette file "{}"!', name) continue else: # Legacy parsing of BEE2.2 files.. pal = parse_legacy(pos_file, prop_file, name) if pal is not None: pal_list.append(pal) finally: if pos_file: pos_file.close() if prop_file: prop_file.close() LOGGER.warning('"{}" is a legacy palette - resaving!', name) # Resave with the new format, then delete originals. if name.endswith('.zip'): pal.save() os.remove(path) else: # Folders can't be overwritten... pal.prevent_overwrite = True pal.save() shutil.rmtree(path) # Ensure the list has a defined order.. pal_list.sort(key=str) return pal_list
import srctools.logger import logging import utils from typing import Iterable, Union, Dict, Tuple LOGGER = srctools.logger.get_logger('img') cached_img = {} # type: Dict[Tuple[str, int, int], ImageTk.PhotoImage] # r, g, b, size -> image cached_squares = { } # type: Dict[Union[Tuple[float, float, float, int], Tuple[str, int]], ImageTk.PhotoImage] filesystem = FileSystemChain( # Highest priority is the in-built UI images. RawFileSystem(str(utils.install_path('images'))), ) # Silence DEBUG messages from Pillow, they don't help. logging.getLogger('PIL').setLevel(logging.INFO) def load_filesystems(systems: Iterable[FileSystem]): """Load in the filesystems used in packages.""" for sys in systems: filesystem.add_sys(sys, 'resources/BEE2/') def tuple_size(size: Union[Tuple[int, int], int]) -> Tuple[int, int]: """Return an xy tuple given a size or tuple.""" if isinstance(size, tuple): return size
import utils from typing import Iterable, Union, Dict, Tuple LOGGER = srctools.logger.get_logger('img') cached_img = {} # type: Dict[Tuple[str, int, int], ImageTk.PhotoImage] # r, g, b, size -> image cached_squares = {} # type: Dict[Union[Tuple[float, float, float, int], Tuple[str, int]], ImageTk.PhotoImage] # Colour of the palette item background PETI_ITEM_BG = Vec(229, 232, 233) filesystem = FileSystemChain( # Highest priority is the in-built UI images. RawFileSystem(str(utils.install_path('images'))), ) # Silence DEBUG messages from Pillow, they don't help. logging.getLogger('PIL').setLevel(logging.INFO) def load_filesystems(systems: Iterable[FileSystem]): """Load in the filesystems used in packages.""" for sys in systems: filesystem.add_sys(sys, 'resources/BEE2/') def tuple_size(size: Union[Tuple[int, int], int]) -> Tuple[int, int]: """Return an xy tuple given a size or tuple.""" if isinstance(size, tuple):
# noinspection PyCompatibility from idlelib.redirector import WidgetRedirector except ImportError: # Python 3.5 and below # noinspection PyCompatibility, PyUnresolvedReferences from idlelib.WidgetRedirector import WidgetRedirector import utils # Put this in a module so it's a singleton, and we can always import the same # object. TK_ROOT = tk.Tk() # Set icons for the application. ICO_PATH = str(utils.install_path('BEE2.ico')) if utils.WIN: # Ensure everything has our icon (including dialogs) TK_ROOT.wm_iconbitmap(default=ICO_PATH) def set_window_icon(window: Union[tk.Toplevel, tk.Tk]): """Set the window icon.""" window.wm_iconbitmap(ICO_PATH) import ctypes # Use Windows APIs to tell the taskbar to group us as our own program, # not with python.exe. Then our icon will apply, and also won't group # with other scripts. try: ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
def make_splash_screen( max_width: float, max_height: float, base_height: int, text1_bbox: Tuple[int, int, int, int], text2_bbox: Tuple[int, int, int, int], ): """Create the splash screen image. This uses a random screenshot from the splash_screens directory. It then adds the gradients on top. """ import random folder = str(utils.install_path('images/splash_screen')) path = '<nothing>' try: path = random.choice(os.listdir(folder)) with open(os.path.join(folder, path), 'rb') as img_file: image = Image.open(img_file) image.load() except (FileNotFoundError, IndexError, IOError): # Not found, substitute a gray block. LOGGER.warning('No splash screen found (tried "{}")', path) image = Image.new( mode='RGB', size=(round(max_width), round(max_height)), color=(128, 128, 128), ) else: if image.height > max_height: image = image.resize(( round(image.width / image.height * max_height), round(max_height), )) if image.width > max_width: image = image.resize(( round(max_width), round(image.height / image.width * max_width), )) draw = ImageDraw.Draw(image, 'RGBA') rect_top = image.height - base_height - 40 draw.rectangle( ( 0, rect_top + 40, image.width, image.height, ), fill=(0, 150, 120, 64), ) # Add a gradient above the rectangle.. for y in range(40): draw.rectangle( ( 0, rect_top + y, image.width, image.height, ), fill=(0, 150, 120, int(y * 128/40)), ) # Draw the shadows behind the text. # This is done by progressively drawing smaller rectangles # with a low alpha. The center is overdrawn more making it thicker. for x1, y1, x2, y2 in [text1_bbox, text2_bbox]: for border in reversed(range(5)): draw.rectangle( ( x1 - border, y1 - border, x2 + border, y2 + border, ), fill=(0, 150, 120, 20), ) tk_img = ImageTk.PhotoImage(image=image) return tk_img, image.width, image.height