Exemple #1
0
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()
Exemple #2
0
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()
Exemple #3
0
 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,
         )
Exemple #4
0
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)
Exemple #5
0
    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)
Exemple #6
0
 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()
Exemple #7
0
 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()
Exemple #8
0
    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'))
Exemple #9
0
 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
Exemple #10
0
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
Exemple #11
0
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
Exemple #12
0
    def export(
        self,
        style: packages.Style,
        selected_objects: dict,
        should_refresh=False,
    ) -> Tuple[bool, bool]:
        """Generate the editoritems.txt and vbsp_config.

        - If no backup is present, the original editoritems is backed up.
        - For each object type, run its .export() function with the given
        - item.
        - Styles are a special case.
        """

        LOGGER.info('-' * 20)
        LOGGER.info('Exporting Items and Style for "{}"!', self.name)

        LOGGER.info('Style = {}', style.id)
        for obj, selected in selected_objects.items():
            # Skip the massive dict in items
            if obj == 'Item':
                selected = selected[0]
            LOGGER.info('{} = {}', obj, selected)

        # VBSP, VRAD, editoritems
        export_screen.set_length('BACK', len(FILES_TO_BACKUP))
        # files in compiler/
        try:
            num_compiler_files = sum(
                1 for file in utils.install_path('compiler').rglob('*'))
        except FileNotFoundError:
            num_compiler_files = 0

        if self.steamID == utils.STEAM_IDS['APERTURE TAG']:
            # Coop paint gun instance
            num_compiler_files += 1

        if num_compiler_files == 0:
            LOGGER.warning('No compiler files!')
            export_screen.skip_stage('COMP')
        else:
            export_screen.set_length('COMP', num_compiler_files)

        LOGGER.info('Should refresh: {}', should_refresh)
        if should_refresh:
            # Check to ensure the cache needs to be copied over..
            should_refresh = self.cache_invalid()
            if should_refresh:
                LOGGER.info("Cache invalid - copying..")
            else:
                LOGGER.info("Skipped copying cache!")

        # Each object type
        # Editoritems
        # VBSP_config
        # Instance list
        # Editor models.
        # FGD file
        # Gameinfo
        export_screen.set_length('EXP', len(packages.OBJ_TYPES) + 6)

        # Do this before setting music and resources,
        # those can take time to compute.
        export_screen.show()
        try:

            if should_refresh:
                # Count the files.
                export_screen.set_length(
                    'RES',
                    sum(1 for file in res_system.walk_folder_repeat()),
                )
            else:
                export_screen.skip_stage('RES')
                export_screen.skip_stage('MUS')

            # Make the folders we need to copy files to, if desired.
            os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True)

            # Start off with the style's data.
            vbsp_config = Property(None, [])
            vbsp_config += style.config.copy()

            all_items = style.items.copy()
            renderables = style.renderables.copy()

            export_screen.step('EXP')

            vpk_success = True

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

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

                try:
                    obj_data.cls.export(
                        packages.ExportData(
                            game=self,
                            selected=selected,
                            all_items=all_items,
                            renderables=renderables,
                            vbsp_conf=vbsp_config,
                            selected_style=style,
                        ))
                except packages.NoVPKExport:
                    # Raised by StyleVPK to indicate it failed to copy.
                    vpk_success = False

                export_screen.step('EXP')

            vbsp_config.set_key(('Options', 'Game_ID'), self.steamID)
            vbsp_config.set_key(
                ('Options', 'dev_mode'),
                srctools.bool_as_int(optionWindow.DEV_MODE.get()))

            # If there are multiple of these blocks, merge them together.
            # They will end up in this order.
            vbsp_config.merge_children(
                'Textures',
                'Fizzlers',
                'Options',
                'StyleVars',
                'DropperItems',
                'Conditions',
                'Quotes',
                'PackTriggers',
            )

            for name, file, ext in FILES_TO_BACKUP:
                item_path = self.abs_path(file + ext)
                backup_path = self.abs_path(file + '_original' + ext)

                if not os.path.isfile(item_path):
                    # We can't backup at all.
                    should_backup = False
                elif name == 'Editoritems':
                    should_backup = not os.path.isfile(backup_path)
                else:
                    # Always backup the non-_original file, it'd be newer.
                    # But only if it's Valves - not our own.
                    should_backup = should_backup_app(item_path)
                    backup_is_good = should_backup_app(backup_path)
                    LOGGER.info(
                        '{}{}: normal={}, backup={}',
                        file,
                        ext,
                        'Valve' if should_backup else 'BEE2',
                        'Valve' if backup_is_good else 'BEE2',
                    )

                    if not should_backup and not backup_is_good:
                        # It's a BEE2 application, we have a problem.
                        # Both the real and backup are bad, we need to get a
                        # new one.
                        try:
                            os.remove(backup_path)
                        except FileNotFoundError:
                            pass
                        try:
                            os.remove(item_path)
                        except FileNotFoundError:
                            pass

                        export_screen.reset()
                        if messagebox.askokcancel(
                                title=_('BEE2 - Export Failed!'),
                                message=_(
                                    'Compiler file {file} missing. '
                                    'Exit Steam applications, then press OK '
                                    'to verify your game cache. You can then '
                                    'export again.').format(file=file + ext, ),
                                master=TK_ROOT,
                        ):
                            webbrowser.open('steam://validate/' +
                                            str(self.steamID))
                        return False, vpk_success

                if should_backup:
                    LOGGER.info('Backing up original {}!', name)
                    shutil.copy(item_path, backup_path)
                export_screen.step('BACK')

            # Backup puzzles, if desired
            backup.auto_backup(selected_game, export_screen)

            # Special-case: implement the UnlockDefault stlylevar here,
            # so all items are modified.
            if selected_objects['StyleVar']['UnlockDefault']:
                LOGGER.info('Unlocking Items!')
                for i, item in enumerate(all_items):
                    # If the Unlock Default Items stylevar is enabled, we
                    # want to force the corridors and obs room to be
                    # deletable and copyable
                    # Also add DESIRES_UP, so they place in the correct orientation
                    if item.id in _UNLOCK_ITEMS:
                        all_items[i] = copy.copy(item)
                        item.deletable = item.copiable = True
                        item.facing = editoritems.DesiredFacing.UP

            LOGGER.info('Editing Gameinfo...')
            self.edit_gameinfo(True)
            export_screen.step('EXP')

            if not GEN_OPTS.get_bool('General', 'preserve_bee2_resource_dir'):
                LOGGER.info('Adding ents to FGD.')
                self.edit_fgd(True)
            export_screen.step('EXP')

            # AtomicWriter writes to a temporary file, then renames in one step.
            # This ensures editoritems won't be half-written.
            LOGGER.info('Writing Editoritems script...')
            with srctools.AtomicWriter(
                    self.abs_path('portal2_dlc2/scripts/editoritems.txt')
            ) as editor_file:
                editoritems.Item.export(editor_file, all_items, renderables)
            export_screen.step('EXP')

            LOGGER.info('Writing Editoritems database...')
            with open(self.abs_path('bin/bee2/editor.bin'), 'wb') as inst_file:
                pick = pickletools.optimize(pickle.dumps(all_items))
                inst_file.write(pick)
            export_screen.step('EXP')

            LOGGER.info('Writing VBSP Config!')
            os.makedirs(self.abs_path('bin/bee2/'), exist_ok=True)
            with open(self.abs_path('bin/bee2/vbsp_config.cfg'),
                      'w',
                      encoding='utf8') as vbsp_file:
                for line in vbsp_config.export():
                    vbsp_file.write(line)
            export_screen.step('EXP')

            if num_compiler_files > 0:
                LOGGER.info('Copying Custom Compiler!')
                compiler_src = utils.install_path('compiler')
                for comp_file in compiler_src.rglob('*'):
                    # Ignore folders.
                    if comp_file.is_dir():
                        continue

                    dest = self.abs_path('bin' /
                                         comp_file.relative_to(compiler_src))

                    LOGGER.info('\t* {} -> {}', comp_file, dest)

                    folder = Path(dest).parent
                    if not folder.exists():
                        folder.mkdir(parents=True, exist_ok=True)

                    try:
                        if os.path.isfile(dest):
                            # First try and give ourselves write-permission,
                            # if it's set read-only.
                            utils.unset_readonly(dest)
                        shutil.copy(comp_file, dest)
                    except PermissionError:
                        # We might not have permissions, if the compiler is currently
                        # running.
                        export_screen.reset()
                        messagebox.showerror(
                            title=_('BEE2 - Export Failed!'),
                            message=_('Copying compiler file {file} failed. '
                                      'Ensure {game} is not running.').format(
                                          file=comp_file,
                                          game=self.name,
                                      ),
                            master=TK_ROOT,
                        )
                        return False, vpk_success
                    export_screen.step('COMP')

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

            LOGGER.info('Optimizing editor models...')
            self.clean_editor_models(all_items)
            export_screen.step('EXP')

            self.generate_fizzler_sides(vbsp_config)

            if self.steamID == utils.STEAM_IDS['APERTURE TAG']:
                os.makedirs(self.abs_path('sdk_content/maps/instances/bee2/'),
                            exist_ok=True)
                with open(
                        self.abs_path(
                            'sdk_content/maps/instances/bee2/tag_coop_gun.vmf'
                        ), 'w') as f:
                    TAG_COOP_INST_VMF.export(f)

            export_screen.reset()  # Hide loading screen, we're done
            return True, vpk_success
        except loadScreen.Cancelled:
            return False, False
Exemple #13
0
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'))
Exemple #14
0
"""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
Exemple #15
0
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')
Exemple #16
0
"""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
Exemple #17
0
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
Exemple #18
0
"""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
Exemple #19
0
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
Exemple #20
0
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
Exemple #21
0
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):
Exemple #22
0
    # 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(
Exemple #23
0
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