Example #1
0
    def __init__(self):
        """Constructor for the PyLNP library."""
        self.bundle = ''
        if hasattr(sys, 'frozen'):
            os.chdir(os.path.dirname(sys.executable))
            if sys.platform == 'win32':
                self.bundle = 'win'
            elif sys.platform.startswith('linux'):
                self.bundle = 'linux'
            elif sys.platform == 'darwin':
                self.bundle = 'osx'
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.dirname(os.path.abspath(__file__)))
        errorlog.start()

        self.lnp_dir = self.identify_folder_name(BASEDIR, 'LNP')
        if not os.path.isdir(self.lnp_dir):
            print('WARNING: LNP folder is missing!', file=sys.stderr)
        self.keybinds_dir = self.identify_folder_name(self.lnp_dir, 'Keybinds')
        self.graphics_dir = self.identify_folder_name(self.lnp_dir, 'Graphics')
        self.utils_dir = self.identify_folder_name(self.lnp_dir, 'Utilities')
        self.colors_dir = self.identify_folder_name(self.lnp_dir, 'Colors')
        self.embarks_dir = self.identify_folder_name(self.lnp_dir, 'Embarks')

        self.folders = []
        self.df_dir = ''
        self.settings = None
        self.init_dir = ''
        self.save_dir = ''
        self.autorun = []
        self.running = {}

        config_file = 'PyLNP.json'
        if os.access(os.path.join(self.lnp_dir, 'PyLNP.json'), os.F_OK):
            config_file = os.path.join(self.lnp_dir, 'PyLNP.json')
        self.config = JSONConfiguration(config_file)
        self.userconfig = JSONConfiguration('PyLNP.user')

        self.load_autorun()
        self.find_df_folder()

        self.new_version = None

        self.ui = TkGui(self)
        self.check_update()
        self.ui.start()
Example #2
0
    def __init__(self):
        """Constructor for the PyLNP library."""
        self.bundle = ''
        if hasattr(sys, 'frozen'):
            os.chdir(os.path.dirname(sys.executable))
            if sys.platform == 'win32':
                self.bundle = 'win'
            elif sys.platform.startswith('linux'):
                self.bundle = 'linux'
            elif sys.platform == 'darwin':
                self.bundle = 'osx'
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.dirname(os.path.abspath(__file__)))
        errorlog.start()

        self.lnp_dir = self.identify_folder_name(BASEDIR, 'LNP')
        if not os.path.isdir(self.lnp_dir):
            print('WARNING: LNP folder is missing!', file=sys.stderr)
        self.keybinds_dir = self.identify_folder_name(self.lnp_dir, 'Keybinds')
        self.graphics_dir = self.identify_folder_name(self.lnp_dir, 'Graphics')
        self.utils_dir = self.identify_folder_name(self.lnp_dir, 'Utilities')
        self.colors_dir = self.identify_folder_name(self.lnp_dir, 'Colors')
        self.embarks_dir = self.identify_folder_name(self.lnp_dir, 'Embarks')

        self.folders = []
        self.df_dir = ''
        self.settings = None
        self.init_dir = ''
        self.save_dir = ''
        self.autorun = []
        self.running = {}

        config_file = 'PyLNP.json'
        if os.access(os.path.join(self.lnp_dir, 'PyLNP.json'), os.F_OK):
            config_file = os.path.join(self.lnp_dir, 'PyLNP.json')
        self.config = JSONConfiguration(config_file)
        self.userconfig = JSONConfiguration('PyLNP.user')

        self.load_autorun()
        self.find_df_folder()

        self.new_version = None

        self.ui = TkGui(self)
        self.check_update()
        self.ui.start()
Example #3
0
class PyLNP(object):
    """
    PyLNP library class.

    Acts as an abstraction layer between the UI and the Dwarf Fortress
    instance.
    """
    def __init__(self):
        """Constructor for the PyLNP library."""
        self.bundle = ''
        if hasattr(sys, 'frozen'):
            os.chdir(os.path.dirname(sys.executable))
            if sys.platform == 'win32':
                self.bundle = 'win'
            elif sys.platform.startswith('linux'):
                self.bundle = 'linux'
            elif sys.platform == 'darwin':
                self.bundle = 'osx'
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.dirname(os.path.abspath(__file__)))
        errorlog.start()

        self.lnp_dir = self.identify_folder_name(BASEDIR, 'LNP')
        if not os.path.isdir(self.lnp_dir):
            print('WARNING: LNP folder is missing!', file=sys.stderr)
        self.keybinds_dir = self.identify_folder_name(self.lnp_dir, 'Keybinds')
        self.graphics_dir = self.identify_folder_name(self.lnp_dir, 'Graphics')
        self.utils_dir = self.identify_folder_name(self.lnp_dir, 'Utilities')
        self.colors_dir = self.identify_folder_name(self.lnp_dir, 'Colors')
        self.embarks_dir = self.identify_folder_name(self.lnp_dir, 'Embarks')

        self.folders = []
        self.df_dir = ''
        self.settings = None
        self.init_dir = ''
        self.save_dir = ''
        self.autorun = []
        self.running = {}

        config_file = 'PyLNP.json'
        if os.access(os.path.join(self.lnp_dir, 'PyLNP.json'), os.F_OK):
            config_file = os.path.join(self.lnp_dir, 'PyLNP.json')
        self.config = JSONConfiguration(config_file)
        self.userconfig = JSONConfiguration('PyLNP.user')

        self.load_autorun()
        self.find_df_folder()

        self.new_version = None

        self.ui = TkGui(self)
        self.check_update()
        self.ui.start()

    @staticmethod
    def identify_folder_name(base, name):
        """
        Allows folder names to be lowercase on case-sensitive systems.
        Returns "base/name" where name is lowercase if the lower case version
        exists and the standard case version does not.

        Params:
            base
                The path containing the desired folder.
            name
                The standard case name of the desired folder.
        """
        normal = os.path.join(base, name)
        lower = os.path.join(base, name.lower())
        if os.path.isdir(lower) and not os.path.isdir(normal):
            return lower
        return normal

    def load_params(self):
        """Loads settings from the selected Dwarf Fortress instance."""
        try:
            self.settings.read_settings()
        except IOError:
            sys.excepthook(*sys.exc_info())
            msg = ("Failed to read settings, "
                   "{0} not really a DF dir?").format(self.df_dir)
            raise IOError(msg)

    def save_params(self):
        """Saves settings to the selected Dwarf Fortress instance."""
        self.settings.write_settings()

    def save_config(self):
        """Saves LNP configuration."""
        self.userconfig.save_data()

    def restore_defaults(self):
        """Copy default settings into the selected Dwarf Fortress instance."""
        shutil.copy(os.path.join(self.lnp_dir, 'Defaults', 'init.txt'),
                    os.path.join(self.init_dir, 'init.txt'))
        shutil.copy(os.path.join(self.lnp_dir, 'Defaults', 'd_init.txt'),
                    os.path.join(self.init_dir, 'd_init.txt'))
        self.load_params()

    def run_df(self, force=False):
        """Launches Dwarf Fortress."""
        result = None
        if sys.platform == 'win32':
            result = self.run_program(
                os.path.join(self.df_dir, 'Dwarf Fortress.exe'), force, True)
        else:
            # Linux/OSX: Run DFHack if available
            if os.path.isfile(os.path.join(self.df_dir, 'dfhack')):
                result = self.run_program(os.path.join(self.df_dir, 'dfhack'),
                                          force, True, True)
                if result == False:
                    raise Exception('Failed to launch a new terminal.')
            else:
                result = self.run_program(os.path.join(self.df_dir, 'df'))
        for prog in self.autorun:
            if os.access(os.path.join(self.utils_dir, prog), os.F_OK):
                self.run_program(os.path.join(self.utils_dir, prog))
        if self.userconfig.get_bool('autoClose'):
            sys.exit()
        return result

    def run_program(self,
                    path,
                    force=False,
                    is_df=False,
                    spawn_terminal=False):
        """
        Launches an external program.

        Params:
            path
                The path of the program to launch.
            spawn_terminal
                Whether or not to spawn a new terminal for this app.
                Used only for DFHack.
        """
        try:
            path = os.path.abspath(path)
            workdir = os.path.dirname(path)
            run_args = path
            nonchild = False
            if spawn_terminal:
                if sys.platform.startswith('linux'):
                    script = 'xdg-terminal'
                    if self.bundle == "linux":
                        script = os.path.join(sys._MEIPASS, script)
                    if force or self.check_program_not_running(path, True):
                        retcode = subprocess.call(
                            [os.path.abspath(script), path],
                            cwd=os.path.dirname(path))
                        return retcode == 0
                    self.ui.on_program_running(path, is_df)
                    return None
                elif sys.platform == 'darwin':
                    nonchild = True
                    run_args = ['open', '-a', 'Terminal.app', path]
            elif path.endswith(
                    '.jar'):  # Explicitly launch JAR files with Java
                run_args = ['java', '-jar', os.path.basename(path)]
            elif path.endswith('.app'):  # OS X application bundle
                nonchild = True
                run_args = ['open', path]
                workdir = path
            if force or self.check_program_not_running(path, nonchild):
                self.running[path] = subprocess.Popen(run_args, cwd=workdir)
                return True
            self.ui.on_program_running(path, is_df)
            return None
        except OSError:
            sys.excepthook(*sys.exc_info())
            return False

    def check_program_not_running(self, path, nonchild=False):
        """
        Returns True if a program is not currently running.

        Params:
            path
                The path of the program.
            nonchild
                If set to True, attempts to check for the process among all
                running processes, not just known child processes. Used for
                DFHack on Linux and OS X; currently unsupported for Windows.
        """
        if nonchild:
            ps = subprocess.Popen('ps axww',
                                  shell=True,
                                  stdout=subprocess.PIPE)
            s = ps.stdout.read()
            ps.wait()
            return path not in s
        else:
            if path not in self.running:
                return True
            else:
                self.running[path].poll()
                return self.running[path].returncode is not None

    def open_folder_idx(self, i):
        """Opens the folder specified by index i, as listed in PyLNP.json."""
        open_folder(
            os.path.join(
                BASEDIR,
                self.config['folders'][i][1].replace('<df>', self.df_dir)))

    def open_savegames(self):
        """Opens the save game folder."""
        open_folder(self.save_dir)

    def open_utils(self):
        """Opens the utilities folder."""
        open_folder(self.utils_dir)

    def open_graphics(self):
        """Opens the graphics pack folder."""
        open_folder(self.graphics_dir)

    @staticmethod
    def open_main_folder():
        """Opens the folder containing the program."""
        open_folder('.')

    def open_lnp_folder(self):
        """Opens the folder containing data for the LNP."""
        open_folder(self.lnp_dir)

    def open_df_folder(self):
        """Opens the Dwarf Fortress folder."""
        open_folder(self.df_dir)

    def open_init_folder(self):
        """Opens the init folder in the selected Dwarf Fortress instance."""
        open_folder(self.init_dir)

    def open_link_idx(self, i):
        """Opens the link specified by index i, as listed in PyLNP.json."""
        self.open_url(self.config['links'][i][1])

    @staticmethod
    def open_url(url):
        """Launches a web browser to the Dwarf Fortress webpage."""
        import webbrowser
        webbrowser.open(url)

    def find_df_folder(self):
        """Locates all suitable Dwarf Fortress installations (folders starting
        with "Dwarf Fortress" or "df")"""
        self.folders = folders = tuple([
            o for o in glob.glob(os.path.join(BASEDIR, 'Dwarf Fortress*')) +
            glob.glob(os.path.join(BASEDIR, 'df*')) if os.path.isdir(o)
        ])
        self.df_dir = ''
        if len(folders) == 1:
            self.set_df_folder(folders[0])

    def set_df_folder(self, path):
        """
        Selects the Dwarf Fortress instance to operate on.

        :param path: The path of the Dwarf Fortress instance to use.
        """
        self.df_dir = os.path.abspath(path)
        self.init_dir = os.path.join(self.df_dir, 'data', 'init')
        self.save_dir = os.path.join(self.df_dir, 'data', 'save')
        self.settings = DFConfiguration(self.df_dir)
        self.install_extras()
        self.load_params()
        self.read_hacks()

    @staticmethod
    def get_text_files(directory):
        """
        Returns a list of .txt files in <directory>.
        Excludes all filenames beginning with "readme" (case-insensitive).

        Params:
            directory
                The directory to search.
        """
        temp = glob.glob(os.path.join(directory, '*.txt'))
        result = []
        for f in temp:
            if not os.path.basename(f).lower().startswith('readme'):
                result.append(f)
        return result

    def read_keybinds(self):
        """Returns a list of keybinding files."""
        return tuple([
            os.path.basename(o) for o in self.get_text_files(self.keybinds_dir)
        ])

    def read_graphics(self):
        """Returns a list of graphics directories."""
        packs = [
            os.path.basename(o)
            for o in glob.glob(os.path.join(self.graphics_dir, '*'))
            if os.path.isdir(o)
        ]
        result = []
        for p in packs:
            font = self.settings.read_value(
                os.path.join(self.graphics_dir, p, 'data', 'init', 'init.txt'),
                'FONT')
            graphics = self.settings.read_value(
                os.path.join(self.graphics_dir, p, 'data', 'init', 'init.txt'),
                'GRAPHICS_FONT')
            result.append((p, font, graphics))
        return tuple(result)

    def current_pack(self):
        """
        Returns the currently installed graphics pack.
        If the pack cannot be identified, returns "FONT/GRAPHICS_FONT".
        """
        packs = self.read_graphics()
        for p in packs:
            if (self.settings.FONT == p[1]
                    and self.settings.GRAPHICS_FONT == p[2]):
                return p[0]
        return str(self.settings.FONT) + '/' + str(self.settings.GRAPHICS_FONT)

    @staticmethod
    def read_utility_lists(path):
        """
        Reads a list of filenames from a utility list (e.g. include.txt).

        :param path: The file to read.
        """
        result = []
        try:
            util_file = open(path)
            for line in util_file:
                for match in re.findall(r'\[(.+)\]', line):
                    result.append(match)
        except IOError:
            pass
        return result

    def read_utilities(self):
        """Returns a list of utility programs."""
        exclusions = self.read_utility_lists(
            os.path.join(self.utils_dir, 'exclude.txt'))
        # Allow for an include list of filenames that will be treated as valid
        # utilities. Useful for e.g. Linux, where executables rarely have
        # extensions.
        inclusions = self.read_utility_lists(
            os.path.join(self.utils_dir, 'include.txt'))
        progs = []
        patterns = ['*.jar']  # Java applications
        if sys.platform in ['windows', 'win32']:
            patterns.append('*.exe')  # Windows executables
            patterns.append('*.bat')  # Batch files
        else:
            patterns.append('*.sh')  # Shell scripts for Linux and OS X
        for root, dirnames, filenames in os.walk(self.utils_dir):
            if sys.platform == 'darwin':
                for dirname in dirnames:
                    if fnmatch.fnmatch(dirname, '*.app'):
                        # OS X application bundles are really directories
                        progs.append(
                            os.path.relpath(os.path.join(root, dirname),
                                            os.path.join(self.utils_dir)))
            for filename in filenames:
                if ((any(fnmatch.fnmatch(filename, p)
                         for p in patterns) or filename in inclusions)
                        and filename not in exclusions):
                    progs.append(
                        os.path.relpath(os.path.join(root, filename),
                                        os.path.join(self.utils_dir)))

        return progs

    def read_embarks(self):
        """Returns a list of embark profiles."""
        return tuple([
            os.path.basename(o) for o in self.get_text_files(self.embarks_dir)
        ])

    def toggle_autoclose(self):
        """Toggle automatic closing of the UI when launching DF."""
        self.userconfig['autoClose'] = not self.userconfig.get_bool(
            'autoClose')
        self.userconfig.save_data()

    def toggle_autorun(self, item):
        """
        Toggles autorun for the specified item.

        Params:
            item
                The item to toggle autorun for.
        """
        if item in self.autorun:
            self.autorun.remove(item)
        else:
            self.autorun.append(item)
        self.save_autorun()

    def load_autorun(self):
        """Loads autorun settings."""
        self.autorun = []
        try:
            for line in open(os.path.join(self.utils_dir, 'autorun.txt')):
                self.autorun.append(line)
        except IOError:
            pass

    def save_autorun(self):
        """Saves autorun settings."""
        autofile = open(os.path.join(self.utils_dir, 'autorun.txt'), 'w')
        autofile.write("\n".join(self.autorun))
        autofile.close()

    def cycle_option(self, field):
        """
        Cycles an option field between its possible values.

        :param field: The field to cycle.
        """
        self.settings.cycle_item(field)
        self.save_params()

    def set_option(self, field, value):
        """
        Sets a field to a specific value.

        Params:
            field
                The field to set.
            value
                The new value for the field.
        """
        self.settings.set_value(field, value)
        self.save_params()

    def load_keybinds(self, filename):
        """
        Overwrites Dwarf Fortress keybindings from a file.

        Params:
            filename
                The keybindings file to use.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        target = os.path.join(self.init_dir, 'interface.txt')
        shutil.copyfile(os.path.join(self.keybinds_dir, filename), target)

    def keybind_exists(self, filename):
        """
        Returns whether or not a keybindings file already exists.

        Params:
            filename
                The filename to check.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        return os.access(os.path.join(self.keybinds_dir, filename), os.F_OK)

    def save_keybinds(self, filename):
        """
        Save current keybindings to a file.

        Params:
            filename
                The name of the new keybindings file.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        filename = os.path.join(self.keybinds_dir, filename)
        shutil.copyfile(os.path.join(self.init_dir, 'interface.txt'), filename)
        self.read_keybinds()

    def delete_keybinds(self, filename):
        """
        Deletes a keybindings file.

        Params:
            filename
                The filename to delete.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        os.remove(os.path.join(self.keybinds_dir, filename))

    def install_graphics(self, pack):
        """
        Installs the graphics pack located in LNP/Graphics/<pack>.

        Params:
            pack
                The name of the pack to install.

        Returns:
            True if successful,
            False if an exception occured
            None if required files are missing (raw/graphics, data/init)
        """
        gfx_dir = os.path.join(self.graphics_dir, pack)
        if (os.path.isdir(gfx_dir)
                and os.path.isdir(os.path.join(gfx_dir, 'raw', 'graphics'))
                and os.path.isdir(os.path.join(gfx_dir, 'data', 'init'))):
            try:
                # Delete old graphics
                if os.path.isdir(os.path.join(self.df_dir, 'raw', 'graphics')):
                    dir_util.remove_tree(
                        os.path.join(self.df_dir, 'raw', 'graphics'))
                # Copy new raws
                dir_util.copy_tree(os.path.join(gfx_dir, 'raw'),
                                   os.path.join(self.df_dir, 'raw'))
                if os.path.isdir(os.path.join(self.df_dir, 'data', 'art')):
                    dir_util.remove_tree(
                        os.path.join(self.df_dir, 'data', 'art'))
                dir_util.copy_tree(os.path.join(gfx_dir, 'data', 'art'),
                                   os.path.join(self.df_dir, 'data', 'art'))
                self.patch_inits(gfx_dir)
                shutil.copyfile(
                    os.path.join(gfx_dir, 'data', 'init', 'colors.txt'),
                    os.path.join(self.df_dir, 'data', 'init', 'colors.txt'))
                try:  # TwbT support
                    os.remove(
                        os.path.join(self.df_dir, 'data', 'init',
                                     'overrides.txt'))
                except:
                    pass
                try:  # TwbT support
                    shutil.copyfile(
                        os.path.join(gfx_dir, 'data', 'init', 'overrides.txt'),
                        os.path.join(self.df_dir, 'data', 'init',
                                     'overrides.txt'))
                except:
                    pass
            except Exception:
                sys.excepthook(*sys.exc_info())
                return False
            else:
                return True
        else:
            return None
        self.load_params()

    def patch_inits(self, gfx_dir):
        """
        Installs init files from a graphics pack by selectively changing
        specific fields. All settings outside of the mentioned fields are
        preserved.

        TODO: Consider if there's a better option than listing all fields
        explicitly...
        """
        d_init_fields = [
            'WOUND_COLOR_NONE',
            'WOUND_COLOR_MINOR',
            'WOUND_COLOR_INHIBITED',
            'WOUND_COLOR_FUNCTION_LOSS',
            'WOUND_COLOR_BROKEN',
            'WOUND_COLOR_MISSING',
            'SKY',
            'CHASM',
            'PILLAR_TILE',
            # Tracks
            'TRACK_N',
            'TRACK_S',
            'TRACK_E',
            'TRACK_W',
            'TRACK_NS',
            'TRACK_NE',
            'TRACK_NW',
            'TRACK_SE',
            'TRACK_SW',
            'TRACK_EW',
            'TRACK_NSE',
            'TRACK_NSW',
            'TRACK_NEW',
            'TRACK_SEW',
            'TRACK_NSEW',
            'TRACK_RAMP_N',
            'TRACK_RAMP_S',
            'TRACK_RAMP_E',
            'TRACK_RAMP_W',
            'TRACK_RAMP_NS',
            'TRACK_RAMP_NE',
            'TRACK_RAMP_NW',
            'TRACK_RAMP_SE',
            'TRACK_RAMP_SW',
            'TRACK_RAMP_EW',
            'TRACK_RAMP_NSE',
            'TRACK_RAMP_NSW',
            'TRACK_RAMP_NEW',
            'TRACK_RAMP_SEW',
            'TRACK_RAMP_NSEW',
            # Trees
            'TREE_ROOT_SLOPING',
            'TREE_TRUNK_SLOPING',
            'TREE_ROOT_SLOPING_DEAD',
            'TREE_TRUNK_SLOPING_DEAD',
            'TREE_ROOTS',
            'TREE_ROOTS_DEAD',
            'TREE_BRANCHES',
            'TREE_BRANCHES_DEAD',
            'TREE_SMOOTH_BRANCHES',
            'TREE_SMOOTH_BRANCHES_DEAD',
            'TREE_TRUNK_PILLAR',
            'TREE_TRUNK_PILLAR_DEAD',
            'TREE_CAP_PILLAR',
            'TREE_CAP_PILLAR_DEAD',
            'TREE_TRUNK_N',
            'TREE_TRUNK_S',
            'TREE_TRUNK_N_DEAD',
            'TREE_TRUNK_S_DEAD',
            'TREE_TRUNK_EW',
            'TREE_TRUNK_EW_DEAD',
            'TREE_CAP_WALL_N',
            'TREE_CAP_WALL_S',
            'TREE_CAP_WALL_N_DEAD',
            'TREE_CAP_WALL_S_DEAD',
            'TREE_TRUNK_E',
            'TREE_TRUNK_W',
            'TREE_TRUNK_E_DEAD',
            'TREE_TRUNK_W_DEAD',
            'TREE_TRUNK_NS',
            'TREE_TRUNK_NS_DEAD',
            'TREE_CAP_WALL_E',
            'TREE_CAP_WALL_W',
            'TREE_CAP_WALL_E_DEAD',
            'TREE_CAP_WALL_W_DEAD',
            'TREE_TRUNK_NW',
            'TREE_CAP_WALL_NW',
            'TREE_TRUNK_NW_DEAD',
            'TREE_CAP_WALL_NW_DEAD',
            'TREE_TRUNK_NE',
            'TREE_CAP_WALL_NE',
            'TREE_TRUNK_NE_DEAD',
            'TREE_CAP_WALL_NE_DEAD',
            'TREE_TRUNK_SW',
            'TREE_CAP_WALL_SW',
            'TREE_TRUNK_SW_DEAD',
            'TREE_CAP_WALL_SW_DEAD',
            'TREE_TRUNK_SE',
            'TREE_CAP_WALL_SE',
            'TREE_TRUNK_SE_DEAD',
            'TREE_CAP_WALL_SE_DEAD',
            'TREE_TRUNK_NSE',
            'TREE_TRUNK_NSE_DEAD',
            'TREE_TRUNK_NSW',
            'TREE_TRUNK_NSW_DEAD',
            'TREE_TRUNK_NEW',
            'TREE_TRUNK_NEW_DEAD',
            'TREE_TRUNK_SEW',
            'TREE_TRUNK_SEW_DEAD',
            'TREE_TRUNK_NSEW',
            'TREE_TRUNK_NSEW_DEAD',
            'TREE_TRUNK_BRANCH_N',
            'TREE_TRUNK_BRANCH_N_DEAD',
            'TREE_TRUNK_BRANCH_S',
            'TREE_TRUNK_BRANCH_S_DEAD',
            'TREE_TRUNK_BRANCH_E',
            'TREE_TRUNK_BRANCH_E_DEAD',
            'TREE_TRUNK_BRANCH_W',
            'TREE_TRUNK_BRANCH_W_DEAD',
            'TREE_BRANCH_NS',
            'TREE_BRANCH_NS_DEAD',
            'TREE_BRANCH_EW',
            'TREE_BRANCH_EW_DEAD',
            'TREE_BRANCH_NW',
            'TREE_BRANCH_NW_DEAD',
            'TREE_BRANCH_NE',
            'TREE_BRANCH_NE_DEAD',
            'TREE_BRANCH_SW',
            'TREE_BRANCH_SW_DEAD',
            'TREE_BRANCH_SE',
            'TREE_BRANCH_SE_DEAD',
            'TREE_BRANCH_NSE',
            'TREE_BRANCH_NSE_DEAD',
            'TREE_BRANCH_NSW',
            'TREE_BRANCH_NSW_DEAD',
            'TREE_BRANCH_NEW',
            'TREE_BRANCH_NEW_DEAD',
            'TREE_BRANCH_SEW',
            'TREE_BRANCH_SEW_DEAD',
            'TREE_BRANCH_NSEW',
            'TREE_BRANCH_NSEW_DEAD',
            'TREE_TWIGS',
            'TREE_TWIGS_DEAD',
            'TREE_CAP_RAMP',
            'TREE_CAP_RAMP_DEAD',
            'TREE_CAP_FLOOR1',
            'TREE_CAP_FLOOR2',
            'TREE_CAP_FLOOR1_DEAD',
            'TREE_CAP_FLOOR2_DEAD',
            'TREE_CAP_FLOOR3',
            'TREE_CAP_FLOOR4',
            'TREE_CAP_FLOOR3_DEAD',
            'TREE_CAP_FLOOR4_DEAD',
            'TREE_TRUNK_INTERIOR',
            'TREE_TRUNK_INTERIOR_DEAD'
        ]
        init_fields = [
            'FONT', 'FULLFONT', 'GRAPHICS', 'GRAPHICS_FONT',
            'GRAPHICS_FULLFONT', 'TRUETYPE'
        ]
        self.settings.read_file(
            os.path.join(gfx_dir, 'data', 'init', 'init.txt'), init_fields,
            False)
        self.settings.read_file(
            os.path.join(gfx_dir, 'data', 'init', 'd_init.txt'), d_init_fields,
            False)
        self.save_params()

    def update_savegames(self):
        """Update save games with current raws."""
        saves = [
            o for o in glob.glob(os.path.join(self.save_dir, '*'))
            if os.path.isdir(o) and not o.endswith('current')
        ]
        count = 0
        if saves:
            for save in saves:
                count = count + 1
                # Delete old graphics
                if os.path.isdir(os.path.join(save, 'raw', 'graphics')):
                    dir_util.remove_tree(os.path.join(save, 'raw', 'graphics'))
                # Copy new raws
                dir_util.copy_tree(os.path.join(self.df_dir, 'raw'),
                                   os.path.join(save, 'raw'))
        return count

    def simplify_graphics(self):
        """Removes unnecessary files from all graphics packs."""
        for pack in self.read_graphics():
            self.simplify_pack(pack)

    def simplify_pack(self, pack):
        """
        Removes unnecessary files from LNP/Graphics/<pack>.

        Params:
            pack
                The pack to simplify.

        Returns:
          The number of files removed if successful
          False if an exception occurred
          None if folder is empty
        """
        pack = os.path.join(self.graphics_dir, pack)
        files_before = sum(len(f) for (_, _, f) in os.walk(pack))
        if files_before == 0:
            return None
        tmp = tempfile.mkdtemp()
        try:
            dir_util.copy_tree(pack, tmp)
            if os.path.isdir(pack):
                dir_util.remove_tree(pack)

            os.makedirs(pack)
            os.makedirs(os.path.join(pack, 'data', 'art'))
            os.makedirs(os.path.join(pack, 'raw', 'graphics'))
            os.makedirs(os.path.join(pack, 'raw', 'objects'))
            os.makedirs(os.path.join(pack, 'data', 'init'))

            dir_util.copy_tree(os.path.join(tmp, 'data', 'art'),
                               os.path.join(pack, 'data', 'art'))
            dir_util.copy_tree(os.path.join(tmp, 'raw', 'graphics'),
                               os.path.join(pack, 'raw', 'graphics'))
            dir_util.copy_tree(os.path.join(tmp, 'raw', 'objects'),
                               os.path.join(pack, 'raw', 'objects'))
            shutil.copyfile(os.path.join(tmp, 'data', 'init', 'colors.txt'),
                            os.path.join(pack, 'data', 'init', 'colors.txt'))
            shutil.copyfile(os.path.join(tmp, 'data', 'init', 'init.txt'),
                            os.path.join(pack, 'data', 'init', 'init.txt'))
            shutil.copyfile(os.path.join(tmp, 'data', 'init', 'd_init.txt'),
                            os.path.join(pack, 'data', 'init', 'd_init.txt'))
            shutil.copyfile(
                os.path.join(tmp, 'data', 'init', 'overrides.txt'),
                os.path.join(pack, 'data', 'init', 'overrides.txt'))
        except IOError:
            sys.excepthook(*sys.exc_info())
            retval = False
        else:
            files_after = sum(len(f) for (_, _, f) in os.walk(pack))
            retval = files_after - files_before
        if os.path.isdir(tmp):
            dir_util.remove_tree(tmp)
        return retval

    def install_extras(self):
        """
        Installs extra utilities to the Dwarf Fortress folder, if this has not
        yet been done.
        """
        extras_dir = os.path.join(self.lnp_dir, 'Extras')
        if not os.path.isdir(extras_dir):
            return
        install_file = os.path.join(self.df_dir,
                                    'PyLNP{0}.txt'.format(VERSION))
        if not os.access(install_file, os.F_OK):
            dir_util.copy_tree(extras_dir, self.df_dir)
            textfile = open(install_file, 'w')
            textfile.write('PyLNP V{0} extras installed!\nTime: {1}'.format(
                VERSION,
                datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
            textfile.close()

    def updates_configured(self):
        """Returns True if update checking have been configured."""
        return self.config.get_string('updates/checkURL') != ''

    def check_update(self):
        """Checks for updates using the URL specified in PyLNP.json."""
        if not self.updates_configured():
            return
        if self.userconfig.get_number('updateDays') == -1:
            return
        if self.userconfig.get_number('nextUpdate') < time.time():
            t = Thread(target=self.perform_update_check)
            t.daemon = True
            t.start()

    def perform_update_check(self):
        """Performs the actual update check. Runs in a thread."""
        try:
            req = Request(self.config.get_string('updates/checkURL'),
                          headers={'User-Agent': 'PyLNP'})
            version_text = urlopen(req, timeout=3).read()
            # Note: versionRegex must capture the version number in a group
            new_version = re.search(
                self.config.get_string('updates/versionRegex'),
                version_text).group(1)
            if new_version != self.config.get_string('updates/packVersion'):
                self.new_version = new_version
                self.ui.on_update_available()
        except URLError as ex:
            print("Error checking for updates: " + str(ex.reason),
                  file=sys.stderr)
        except:
            pass

    def next_update(self, days):
        """Sets the next update check to occur in <days> days."""
        self.userconfig['nextUpdate'] = (time.time() + days * 24 * 60 * 60)
        self.userconfig['updateDays'] = days
        self.save_config()

    def start_update(self):
        """Launches a webbrowser to the specified update URL."""
        self.open_url(self.config.get_string('updates/downloadURL'))

    def read_colors(self):
        """Returns a list of color schemes."""
        return tuple([
            os.path.splitext(os.path.basename(p))[0]
            for p in self.get_text_files(self.colors_dir)
        ])

    def get_colors(self, colorscheme=None):
        """
        Returns RGB tuples for all 16 colors in <colorscheme>.txt, or
        data/init/colors.txt if no scheme is provided."""
        result = []
        f = os.path.join(self.df_dir, 'data', 'init', 'colors.txt')
        if colorscheme is not None:
            f = os.path.join(self.lnp_dir, 'colors', colorscheme + '.txt')
        for c in [
                'BLACK', 'BLUE', 'GREEN', 'CYAN', 'RED', 'MAGENTA', 'BROWN',
                'LGRAY', 'DGRAY', 'LBLUE', 'LGREEN', 'LCYAN', 'LRED',
                'LMAGENTA', 'YELLOW', 'WHITE'
        ]:
            result.append((int(self.settings.read_value(f, c + '_R')),
                           int(self.settings.read_value(f, c + '_G')),
                           int(self.settings.read_value(f, c + '_B'))))
        return result

    def load_colors(self, filename):
        """
        Replaces the current DF color scheme.

        Params:
          filename
            The name of the new colorscheme to install (filename without
            extension).
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        shutil.copyfile(os.path.join(self.lnp_dir, 'colors', filename),
                        os.path.join(self.init_dir, 'colors.txt'))

    def save_colors(self, filename):
        """
        Save current keybindings to a file.

        Params:
            filename
                The name of the new keybindings file.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        filename = os.path.join(self.colors_dir, filename)
        shutil.copyfile(os.path.join(self.init_dir, 'colors.txt'), filename)
        self.read_colors()

    def color_exists(self, filename):
        """
        Returns whether or not a color scheme already exists.

        Params:
            filename
                The filename to check.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        return os.access(os.path.join(self.colors_dir, filename), os.F_OK)

    def delete_colors(self, filename):
        """
        Deletes a color scheme file.

        Params:
            filename
                The filename to delete.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        os.remove(os.path.join(self.colors_dir, filename))

    def read_hacks(self):
        """Reads which hacks are enabled."""
        try:
            f = open(os.path.join(self.df_dir, 'PyLNP_dfhack_onload.init'))
            hacklines = f.readlines()
            for h in self.get_hacks().values():
                h['enabled'] = h['command'] + '\n' in hacklines
            f.close()
        except IOError:
            for h in self.get_hacks().values():
                h['enabled'] = False

    def get_hacks(self):
        """Returns dict of available hacks."""
        return self.config.get_dict('dfhack')

    def get_hack(self, title):
        """
        Returns the hack titled <title>, or None if this does not exist.

        Params:
            title
                The title of the hack.
        """
        try:
            return self.get_hacks()[title]
        except KeyError:
            return None

    def toggle_hack(self, name):
        """
        Toggles the hack <name>.

        Params:
            name
                The name of the hack to toggle.
        """
        self.get_hack(name)['enabled'] = not self.get_hack(name)['enabled']
        self.rebuild_hacks()

    def rebuild_hacks(self):
        """Rebuilds PyLNP_dfhack_onload.init with the enabled hacks."""
        f = open(os.path.join(self.df_dir, 'PyLNP_dfhack_onload.init'), 'w')
        f.write('# Generated by PyLNP\n\n')
        for k, h in self.get_hacks().items():
            if h['enabled']:
                f.write('# ' + str(k) + '\n')
                f.write('# ' + str(h['tooltip']) + '\n')
                f.write(h['command'] + '\n\n')
        f.flush()
        f.close()

    def install_embarks(self, files):
        """
        Installs a list of embark profiles.

        Params:
            files
                List of files to install.
        """
        out = open(os.path.join(self.init_dir, 'embark_profiles.txt'), 'w')
        for f in files:
            embark = open(os.path.join(self.embarks_dir, f))
            out.write(embark.read() + "\n\n")
        out.flush()
        out.close()
Example #4
0
class PyLNP(object):
    """
    PyLNP library class.

    Acts as an abstraction layer between the UI and the Dwarf Fortress
    instance.
    """
    def __init__(self):
        """Constructor for the PyLNP library."""
        self.bundle = ''
        if hasattr(sys, 'frozen'):
            os.chdir(os.path.dirname(sys.executable))
            if sys.platform == 'win32':
                self.bundle = 'win'
            elif sys.platform.startswith('linux'):
                self.bundle = 'linux'
            elif sys.platform == 'darwin':
                self.bundle = 'osx'
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.dirname(os.path.abspath(__file__)))
        errorlog.start()

        self.lnp_dir = self.identify_folder_name(BASEDIR, 'LNP')
        if not os.path.isdir(self.lnp_dir):
            print('WARNING: LNP folder is missing!', file=sys.stderr)
        self.keybinds_dir = self.identify_folder_name(self.lnp_dir, 'Keybinds')
        self.graphics_dir = self.identify_folder_name(self.lnp_dir, 'Graphics')
        self.utils_dir = self.identify_folder_name(self.lnp_dir, 'Utilities')
        self.colors_dir = self.identify_folder_name(self.lnp_dir, 'Colors')
        self.embarks_dir = self.identify_folder_name(self.lnp_dir, 'Embarks')

        self.folders = []
        self.df_dir = ''
        self.settings = None
        self.init_dir = ''
        self.save_dir = ''
        self.autorun = []
        self.running = {}

        config_file = 'PyLNP.json'
        if os.access(os.path.join(self.lnp_dir, 'PyLNP.json'), os.F_OK):
            config_file = os.path.join(self.lnp_dir, 'PyLNP.json')
        self.config = JSONConfiguration(config_file)
        self.userconfig = JSONConfiguration('PyLNP.user')

        self.load_autorun()
        self.find_df_folder()

        self.new_version = None

        self.ui = TkGui(self)
        self.check_update()
        self.ui.start()

    @staticmethod
    def identify_folder_name(base, name):
        """
        Allows folder names to be lowercase on case-sensitive systems.
        Returns "base/name" where name is lowercase if the lower case version
        exists and the standard case version does not.

        Params:
            base
                The path containing the desired folder.
            name
                The standard case name of the desired folder.
        """
        normal = os.path.join(base, name)
        lower = os.path.join(base, name.lower())
        if os.path.isdir(lower) and not os.path.isdir(normal):
            return lower
        return normal

    def load_params(self):
        """Loads settings from the selected Dwarf Fortress instance."""
        try:
            self.settings.read_settings()
        except IOError:
            sys.excepthook(*sys.exc_info())
            msg = ("Failed to read settings, "
                   "{0} not really a DF dir?").format(self.df_dir)
            raise IOError(msg)

    def save_params(self):
        """Saves settings to the selected Dwarf Fortress instance."""
        self.settings.write_settings()

    def save_config(self):
        """Saves LNP configuration."""
        self.userconfig.save_data()

    def restore_defaults(self):
        """Copy default settings into the selected Dwarf Fortress instance."""
        shutil.copy(
            os.path.join(self.lnp_dir, 'Defaults', 'init.txt'),
            os.path.join(self.init_dir, 'init.txt')
            )
        shutil.copy(
            os.path.join(self.lnp_dir, 'Defaults', 'd_init.txt'),
            os.path.join(self.init_dir, 'd_init.txt')
        )
        self.load_params()

    def run_df(self, force=False):
        """Launches Dwarf Fortress."""
        result = None
        if sys.platform == 'win32':
            result = self.run_program(
                os.path.join(self.df_dir, 'Dwarf Fortress.exe'), force, True)
        else:
            # Linux/OSX: Run DFHack if available
            if os.path.isfile(os.path.join(self.df_dir, 'dfhack')):
                result = self.run_program(
                    os.path.join(self.df_dir, 'dfhack'), force, True, True)
                if result == False:
                    raise Exception('Failed to launch a new terminal.')
            else:
                result = self.run_program(os.path.join(self.df_dir, 'df'))
        for prog in self.autorun:
            if os.access(os.path.join(self.utils_dir, prog), os.F_OK):
                self.run_program(os.path.join(self.utils_dir, prog))
        if self.userconfig.get_bool('autoClose'):
            sys.exit()
        return result

    def run_program(self, path, force=False, is_df=False, spawn_terminal=False):
        """
        Launches an external program.

        Params:
            path
                The path of the program to launch.
            spawn_terminal
                Whether or not to spawn a new terminal for this app.
                Used only for DFHack.
        """
        try:
            path = os.path.abspath(path)
            workdir = os.path.dirname(path)
            run_args = path
            nonchild = False
            if spawn_terminal:
                if sys.platform.startswith('linux'):
                    script = 'xdg-terminal'
                    if self.bundle == "linux":
                        script = os.path.join(sys._MEIPASS, script)
                    if force or self.check_program_not_running(path, True):
                        retcode = subprocess.call(
                            [os.path.abspath(script), path],
                            cwd=os.path.dirname(path))
                        return retcode == 0
                    self.ui.on_program_running(path, is_df)
                    return None
                elif sys.platform == 'darwin':
                    nonchild = True
                    run_args = ['open', '-a', 'Terminal.app', path]
            elif path.endswith('.jar'):  # Explicitly launch JAR files with Java
                run_args = ['java', '-jar', os.path.basename(path)]
            elif path.endswith('.app'):  # OS X application bundle
                nonchild = True
                run_args = ['open', path]
                workdir = path
            if force or self.check_program_not_running(path, nonchild):
                self.running[path] = subprocess.Popen(run_args, cwd=workdir)
                return True
            self.ui.on_program_running(path, is_df)
            return None
        except OSError:
            sys.excepthook(*sys.exc_info())
            return False

    def check_program_not_running(self, path, nonchild=False):
        """
        Returns True if a program is not currently running.

        Params:
            path
                The path of the program.
            nonchild
                If set to True, attempts to check for the process among all
                running processes, not just known child processes. Used for
                DFHack on Linux and OS X; currently unsupported for Windows.
        """
        if nonchild:
            ps = subprocess.Popen('ps axww', shell=True, stdout=subprocess.PIPE)
            s = ps.stdout.read()
            ps.wait()
            return path not in s
        else:
            if path not in self.running:
                return True
            else:
                self.running[path].poll()
                return self.running[path].returncode is not None

    def open_folder_idx(self, i):
        """Opens the folder specified by index i, as listed in PyLNP.json."""
        open_folder(os.path.join(
            BASEDIR, self.config['folders'][i][1].replace(
                '<df>', self.df_dir)))

    def open_savegames(self):
        """Opens the save game folder."""
        open_folder(self.save_dir)

    def open_utils(self):
        """Opens the utilities folder."""
        open_folder(self.utils_dir)

    def open_graphics(self):
        """Opens the graphics pack folder."""
        open_folder(self.graphics_dir)

    @staticmethod
    def open_main_folder():
        """Opens the folder containing the program."""
        open_folder('.')

    def open_lnp_folder(self):
        """Opens the folder containing data for the LNP."""
        open_folder(self.lnp_dir)

    def open_df_folder(self):
        """Opens the Dwarf Fortress folder."""
        open_folder(self.df_dir)

    def open_init_folder(self):
        """Opens the init folder in the selected Dwarf Fortress instance."""
        open_folder(self.init_dir)

    def open_link_idx(self, i):
        """Opens the link specified by index i, as listed in PyLNP.json."""
        self.open_url(self.config['links'][i][1])

    @staticmethod
    def open_url(url):
        """Launches a web browser to the Dwarf Fortress webpage."""
        import webbrowser
        webbrowser.open(url)

    def find_df_folder(self):
        """Locates all suitable Dwarf Fortress installations (folders starting
        with "Dwarf Fortress" or "df")"""
        self.folders = folders = tuple([
            o for o in
            glob.glob(os.path.join(BASEDIR, 'Dwarf Fortress*')) +
            glob.glob(os.path.join(BASEDIR, 'df*')) if os.path.isdir(o)
            ])
        self.df_dir = ''
        if len(folders) == 1:
            self.set_df_folder(folders[0])

    def set_df_folder(self, path):
        """
        Selects the Dwarf Fortress instance to operate on.

        :param path: The path of the Dwarf Fortress instance to use.
        """
        self.df_dir = os.path.abspath(path)
        self.init_dir = os.path.join(self.df_dir, 'data', 'init')
        self.save_dir = os.path.join(self.df_dir, 'data', 'save')
        self.settings = DFConfiguration(self.df_dir)
        self.install_extras()
        self.load_params()
        self.read_hacks()

    @staticmethod
    def get_text_files(directory):
        """
        Returns a list of .txt files in <directory>.
        Excludes all filenames beginning with "readme" (case-insensitive).

        Params:
            directory
                The directory to search.
        """
        temp = glob.glob(os.path.join(directory, '*.txt'))
        result = []
        for f in temp:
            if not os.path.basename(f).lower().startswith('readme'):
                result.append(f)
        return result

    def read_keybinds(self):
        """Returns a list of keybinding files."""
        return tuple([
            os.path.basename(o) for o in self.get_text_files(self.keybinds_dir)
            ])

    def read_graphics(self):
        """Returns a list of graphics directories."""
        packs = [
            os.path.basename(o) for o in
            glob.glob(os.path.join(self.graphics_dir, '*')) if
            os.path.isdir(o)]
        result = []
        for p in packs:
            font = self.settings.read_value(os.path.join(
                self.graphics_dir, p, 'data', 'init', 'init.txt'), 'FONT')
            graphics = self.settings.read_value(
                os.path.join(self.graphics_dir, p, 'data', 'init', 'init.txt'),
                'GRAPHICS_FONT')
            result.append((p, font, graphics))
        return tuple(result)

    def current_pack(self):
        """
        Returns the currently installed graphics pack.
        If the pack cannot be identified, returns "FONT/GRAPHICS_FONT".
        """
        packs = self.read_graphics()
        for p in packs:
            if (self.settings.FONT == p[1] and
                    self.settings.GRAPHICS_FONT == p[2]):
                return p[0]
        return str(self.settings.FONT)+'/'+str(self.settings.GRAPHICS_FONT)

    @staticmethod
    def read_utility_lists(path):
        """
        Reads a list of filenames from a utility list (e.g. include.txt).

        :param path: The file to read.
        """
        result = []
        try:
            util_file = open(path)
            for line in util_file:
                for match in re.findall(r'\[(.+)\]', line):
                    result.append(match)
        except IOError:
            pass
        return result

    def read_utilities(self):
        """Returns a list of utility programs."""
        exclusions = self.read_utility_lists(os.path.join(
            self.utils_dir, 'exclude.txt'))
        # Allow for an include list of filenames that will be treated as valid
        # utilities. Useful for e.g. Linux, where executables rarely have
        # extensions.
        inclusions = self.read_utility_lists(os.path.join(
            self.utils_dir, 'include.txt'))
        progs = []
        patterns = ['*.jar']  # Java applications
        if sys.platform in ['windows', 'win32']:
            patterns.append('*.exe')  # Windows executables
            patterns.append('*.bat')  # Batch files
        else:
            patterns.append('*.sh')  # Shell scripts for Linux and OS X
        for root, dirnames, filenames in os.walk(self.utils_dir):
            if sys.platform == 'darwin':
                for dirname in dirnames:
                    if fnmatch.fnmatch(dirname, '*.app'):
                        # OS X application bundles are really directories
                        progs.append(os.path.relpath(
                            os.path.join(root, dirname),
                            os.path.join(self.utils_dir)))
            for filename in filenames:
                if ((
                        any(fnmatch.fnmatch(filename, p) for p in patterns) or
                        filename in inclusions) and
                        filename not in exclusions):
                    progs.append(os.path.relpath(
                        os.path.join(root, filename),
                        os.path.join(self.utils_dir)))

        return progs

    def read_embarks(self):
        """Returns a list of embark profiles."""
        return tuple([
            os.path.basename(o) for o in self.get_text_files(self.embarks_dir)])

    def toggle_autoclose(self):
        """Toggle automatic closing of the UI when launching DF."""
        self.userconfig['autoClose'] = not self.userconfig.get_bool('autoClose')
        self.userconfig.save_data()

    def toggle_autorun(self, item):
        """
        Toggles autorun for the specified item.

        Params:
            item
                The item to toggle autorun for.
        """
        if item in self.autorun:
            self.autorun.remove(item)
        else:
            self.autorun.append(item)
        self.save_autorun()

    def load_autorun(self):
        """Loads autorun settings."""
        self.autorun = []
        try:
            for line in open(os.path.join(self.utils_dir, 'autorun.txt')):
                self.autorun.append(line)
        except IOError:
            pass

    def save_autorun(self):
        """Saves autorun settings."""
        autofile = open(os.path.join(self.utils_dir, 'autorun.txt'), 'w')
        autofile.write("\n".join(self.autorun))
        autofile.close()

    def cycle_option(self, field):
        """
        Cycles an option field between its possible values.

        :param field: The field to cycle.
        """
        self.settings.cycle_item(field)
        self.save_params()

    def set_option(self, field, value):
        """
        Sets a field to a specific value.

        Params:
            field
                The field to set.
            value
                The new value for the field.
        """
        self.settings.set_value(field, value)
        self.save_params()

    def load_keybinds(self, filename):
        """
        Overwrites Dwarf Fortress keybindings from a file.

        Params:
            filename
                The keybindings file to use.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        target = os.path.join(self.init_dir, 'interface.txt')
        shutil.copyfile(os.path.join(self.keybinds_dir, filename), target)

    def keybind_exists(self, filename):
        """
        Returns whether or not a keybindings file already exists.

        Params:
            filename
                The filename to check.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        return os.access(os.path.join(self.keybinds_dir, filename), os.F_OK)

    def save_keybinds(self, filename):
        """
        Save current keybindings to a file.

        Params:
            filename
                The name of the new keybindings file.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        filename = os.path.join(self.keybinds_dir, filename)
        shutil.copyfile(os.path.join(self.init_dir, 'interface.txt'), filename)
        self.read_keybinds()

    def delete_keybinds(self, filename):
        """
        Deletes a keybindings file.

        Params:
            filename
                The filename to delete.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        os.remove(os.path.join(self.keybinds_dir, filename))

    def install_graphics(self, pack):
        """
        Installs the graphics pack located in LNP/Graphics/<pack>.

        Params:
            pack
                The name of the pack to install.

        Returns:
            True if successful,
            False if an exception occured
            None if required files are missing (raw/graphics, data/init)
        """
        gfx_dir = os.path.join(self.graphics_dir, pack)
        if (os.path.isdir(gfx_dir) and
                os.path.isdir(os.path.join(gfx_dir, 'raw', 'graphics')) and
                os.path.isdir(os.path.join(gfx_dir, 'data', 'init'))):
            try:
                # Delete old graphics
                if os.path.isdir(os.path.join(self.df_dir, 'raw', 'graphics')):
                    dir_util.remove_tree(
                        os.path.join(self.df_dir, 'raw', 'graphics'))
                # Copy new raws
                dir_util.copy_tree(
                    os.path.join(gfx_dir, 'raw'),
                    os.path.join(self.df_dir, 'raw'))
                if os.path.isdir(os.path.join(self.df_dir, 'data', 'art')):
                    dir_util.remove_tree(
                        os.path.join(self.df_dir, 'data', 'art'))
                dir_util.copy_tree(
                    os.path.join(gfx_dir, 'data', 'art'),
                    os.path.join(self.df_dir, 'data', 'art'))
                self.patch_inits(gfx_dir)
                shutil.copyfile(
                    os.path.join(gfx_dir, 'data', 'init', 'colors.txt'),
                    os.path.join(self.df_dir, 'data', 'init', 'colors.txt'))
                try: # TwbT support
                    os.remove(os.path.join(
                        self.df_dir, 'data', 'init', 'overrides.txt'))
                except:
                    pass
                try: # TwbT support
                    shutil.copyfile(
                        os.path.join(gfx_dir, 'data', 'init', 'overrides.txt'),
                        os.path.join(
                            self.df_dir, 'data', 'init', 'overrides.txt'))
                except:
                    pass
            except Exception:
                sys.excepthook(*sys.exc_info())
                return False
            else:
                return True
        else:
            return None
        self.load_params()

    def patch_inits(self, gfx_dir):
        """
        Installs init files from a graphics pack by selectively changing
        specific fields. All settings outside of the mentioned fields are
        preserved.

        TODO: Consider if there's a better option than listing all fields
        explicitly...
        """
        d_init_fields = [
            'WOUND_COLOR_NONE', 'WOUND_COLOR_MINOR',
            'WOUND_COLOR_INHIBITED', 'WOUND_COLOR_FUNCTION_LOSS',
            'WOUND_COLOR_BROKEN', 'WOUND_COLOR_MISSING', 'SKY', 'CHASM',
            'PILLAR_TILE',
            # Tracks
            'TRACK_N', 'TRACK_S', 'TRACK_E', 'TRACK_W', 'TRACK_NS',
            'TRACK_NE', 'TRACK_NW', 'TRACK_SE', 'TRACK_SW', 'TRACK_EW',
            'TRACK_NSE', 'TRACK_NSW', 'TRACK_NEW', 'TRACK_SEW',
            'TRACK_NSEW', 'TRACK_RAMP_N', 'TRACK_RAMP_S', 'TRACK_RAMP_E',
            'TRACK_RAMP_W', 'TRACK_RAMP_NS', 'TRACK_RAMP_NE',
            'TRACK_RAMP_NW', 'TRACK_RAMP_SE', 'TRACK_RAMP_SW',
            'TRACK_RAMP_EW', 'TRACK_RAMP_NSE', 'TRACK_RAMP_NSW',
            'TRACK_RAMP_NEW', 'TRACK_RAMP_SEW', 'TRACK_RAMP_NSEW',
            # Trees
            'TREE_ROOT_SLOPING', 'TREE_TRUNK_SLOPING',
            'TREE_ROOT_SLOPING_DEAD', 'TREE_TRUNK_SLOPING_DEAD',
            'TREE_ROOTS', 'TREE_ROOTS_DEAD', 'TREE_BRANCHES',
            'TREE_BRANCHES_DEAD', 'TREE_SMOOTH_BRANCHES',
            'TREE_SMOOTH_BRANCHES_DEAD', 'TREE_TRUNK_PILLAR',
            'TREE_TRUNK_PILLAR_DEAD', 'TREE_CAP_PILLAR',
            'TREE_CAP_PILLAR_DEAD', 'TREE_TRUNK_N', 'TREE_TRUNK_S',
            'TREE_TRUNK_N_DEAD', 'TREE_TRUNK_S_DEAD', 'TREE_TRUNK_EW',
            'TREE_TRUNK_EW_DEAD', 'TREE_CAP_WALL_N', 'TREE_CAP_WALL_S',
            'TREE_CAP_WALL_N_DEAD', 'TREE_CAP_WALL_S_DEAD', 'TREE_TRUNK_E',
            'TREE_TRUNK_W', 'TREE_TRUNK_E_DEAD', 'TREE_TRUNK_W_DEAD',
            'TREE_TRUNK_NS', 'TREE_TRUNK_NS_DEAD', 'TREE_CAP_WALL_E',
            'TREE_CAP_WALL_W', 'TREE_CAP_WALL_E_DEAD',
            'TREE_CAP_WALL_W_DEAD', 'TREE_TRUNK_NW', 'TREE_CAP_WALL_NW',
            'TREE_TRUNK_NW_DEAD', 'TREE_CAP_WALL_NW_DEAD', 'TREE_TRUNK_NE',
            'TREE_CAP_WALL_NE', 'TREE_TRUNK_NE_DEAD',
            'TREE_CAP_WALL_NE_DEAD', 'TREE_TRUNK_SW', 'TREE_CAP_WALL_SW',
            'TREE_TRUNK_SW_DEAD', 'TREE_CAP_WALL_SW_DEAD', 'TREE_TRUNK_SE',
            'TREE_CAP_WALL_SE', 'TREE_TRUNK_SE_DEAD',
            'TREE_CAP_WALL_SE_DEAD', 'TREE_TRUNK_NSE',
            'TREE_TRUNK_NSE_DEAD', 'TREE_TRUNK_NSW', 'TREE_TRUNK_NSW_DEAD',
            'TREE_TRUNK_NEW', 'TREE_TRUNK_NEW_DEAD', 'TREE_TRUNK_SEW',
            'TREE_TRUNK_SEW_DEAD', 'TREE_TRUNK_NSEW',
            'TREE_TRUNK_NSEW_DEAD', 'TREE_TRUNK_BRANCH_N',
            'TREE_TRUNK_BRANCH_N_DEAD', 'TREE_TRUNK_BRANCH_S',
            'TREE_TRUNK_BRANCH_S_DEAD', 'TREE_TRUNK_BRANCH_E',
            'TREE_TRUNK_BRANCH_E_DEAD', 'TREE_TRUNK_BRANCH_W',
            'TREE_TRUNK_BRANCH_W_DEAD', 'TREE_BRANCH_NS',
            'TREE_BRANCH_NS_DEAD', 'TREE_BRANCH_EW', 'TREE_BRANCH_EW_DEAD',
            'TREE_BRANCH_NW', 'TREE_BRANCH_NW_DEAD', 'TREE_BRANCH_NE',
            'TREE_BRANCH_NE_DEAD', 'TREE_BRANCH_SW', 'TREE_BRANCH_SW_DEAD',
            'TREE_BRANCH_SE', 'TREE_BRANCH_SE_DEAD', 'TREE_BRANCH_NSE',
            'TREE_BRANCH_NSE_DEAD', 'TREE_BRANCH_NSW',
            'TREE_BRANCH_NSW_DEAD', 'TREE_BRANCH_NEW',
            'TREE_BRANCH_NEW_DEAD', 'TREE_BRANCH_SEW',
            'TREE_BRANCH_SEW_DEAD', 'TREE_BRANCH_NSEW',
            'TREE_BRANCH_NSEW_DEAD', 'TREE_TWIGS', 'TREE_TWIGS_DEAD',
            'TREE_CAP_RAMP', 'TREE_CAP_RAMP_DEAD', 'TREE_CAP_FLOOR1',
            'TREE_CAP_FLOOR2', 'TREE_CAP_FLOOR1_DEAD',
            'TREE_CAP_FLOOR2_DEAD', 'TREE_CAP_FLOOR3', 'TREE_CAP_FLOOR4',
            'TREE_CAP_FLOOR3_DEAD', 'TREE_CAP_FLOOR4_DEAD',
            'TREE_TRUNK_INTERIOR', 'TREE_TRUNK_INTERIOR_DEAD']
        init_fields = [
            'FONT', 'FULLFONT', 'GRAPHICS', 'GRAPHICS_FONT',
            'GRAPHICS_FULLFONT', 'TRUETYPE']
        self.settings.read_file(
            os.path.join(gfx_dir, 'data', 'init', 'init.txt'), init_fields,
            False)
        self.settings.read_file(
            os.path.join(gfx_dir, 'data', 'init', 'd_init.txt'), d_init_fields,
            False)
        self.save_params()

    def update_savegames(self):
        """Update save games with current raws."""
        saves = [
            o for o in glob.glob(os.path.join(self.save_dir, '*'))
            if os.path.isdir(o) and not o.endswith('current')]
        count = 0
        if saves:
            for save in saves:
                count = count + 1
                # Delete old graphics
                if os.path.isdir(os.path.join(save, 'raw', 'graphics')):
                    dir_util.remove_tree(os.path.join(save, 'raw', 'graphics'))
                # Copy new raws
                dir_util.copy_tree(
                    os.path.join(self.df_dir, 'raw'),
                    os.path.join(save, 'raw'))
        return count

    def simplify_graphics(self):
        """Removes unnecessary files from all graphics packs."""
        for pack in self.read_graphics():
            self.simplify_pack(pack)

    def simplify_pack(self, pack):
        """
        Removes unnecessary files from LNP/Graphics/<pack>.

        Params:
            pack
                The pack to simplify.

        Returns:
          The number of files removed if successful
          False if an exception occurred
          None if folder is empty
        """
        pack = os.path.join(self.graphics_dir, pack)
        files_before = sum(len(f) for (_, _, f) in os.walk(pack))
        if files_before == 0:
            return None
        tmp = tempfile.mkdtemp()
        try:
            dir_util.copy_tree(pack, tmp)
            if os.path.isdir(pack):
                dir_util.remove_tree(pack)

            os.makedirs(pack)
            os.makedirs(os.path.join(pack, 'data', 'art'))
            os.makedirs(os.path.join(pack, 'raw', 'graphics'))
            os.makedirs(os.path.join(pack, 'raw', 'objects'))
            os.makedirs(os.path.join(pack, 'data', 'init'))

            dir_util.copy_tree(
                os.path.join(tmp, 'data', 'art'),
                os.path.join(pack, 'data', 'art'))
            dir_util.copy_tree(
                os.path.join(tmp, 'raw', 'graphics'),
                os.path.join(pack, 'raw', 'graphics'))
            dir_util.copy_tree(
                os.path.join(tmp, 'raw', 'objects'),
                os.path.join(pack, 'raw', 'objects'))
            shutil.copyfile(
                os.path.join(tmp, 'data', 'init', 'colors.txt'),
                os.path.join(pack, 'data', 'init', 'colors.txt'))
            shutil.copyfile(
                os.path.join(tmp, 'data', 'init', 'init.txt'),
                os.path.join(pack, 'data', 'init', 'init.txt'))
            shutil.copyfile(
                os.path.join(tmp, 'data', 'init', 'd_init.txt'),
                os.path.join(pack, 'data', 'init', 'd_init.txt'))
            shutil.copyfile(
                os.path.join(tmp, 'data', 'init', 'overrides.txt'),
                os.path.join(pack, 'data', 'init', 'overrides.txt'))
        except IOError:
            sys.excepthook(*sys.exc_info())
            retval = False
        else:
            files_after = sum(len(f) for (_, _, f) in os.walk(pack))
            retval = files_after - files_before
        if os.path.isdir(tmp):
            dir_util.remove_tree(tmp)
        return retval

    def install_extras(self):
        """
        Installs extra utilities to the Dwarf Fortress folder, if this has not
        yet been done.
        """
        extras_dir = os.path.join(self.lnp_dir, 'Extras')
        if not os.path.isdir(extras_dir):
            return
        install_file = os.path.join(self.df_dir, 'PyLNP{0}.txt'.format(VERSION))
        if not os.access(install_file, os.F_OK):
            dir_util.copy_tree(extras_dir, self.df_dir)
            textfile = open(install_file, 'w')
            textfile.write(
                'PyLNP V{0} extras installed!\nTime: {1}'.format(
                    VERSION, datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
            textfile.close()

    def updates_configured(self):
        """Returns True if update checking have been configured."""
        return self.config.get_string('updates/checkURL') != ''

    def check_update(self):
        """Checks for updates using the URL specified in PyLNP.json."""
        if not self.updates_configured():
            return
        if self.userconfig.get_number('updateDays') == -1:
            return
        if self.userconfig.get_number('nextUpdate') < time.time():
            t = Thread(target=self.perform_update_check)
            t.daemon = True
            t.start()

    def perform_update_check(self):
        """Performs the actual update check. Runs in a thread."""
        try:
            req = Request(
                self.config.get_string('updates/checkURL'),
                headers={'User-Agent':'PyLNP'})
            version_text = urlopen(req, timeout=3).read()
            # Note: versionRegex must capture the version number in a group
            new_version = re.search(
                self.config.get_string('updates/versionRegex'),
                version_text).group(1)
            if new_version != self.config.get_string('updates/packVersion'):
                self.new_version = new_version
                self.ui.on_update_available()
        except URLError as ex:
            print(
                "Error checking for updates: " + str(ex.reason),
                file=sys.stderr)
        except:
            pass

    def next_update(self, days):
        """Sets the next update check to occur in <days> days."""
        self.userconfig['nextUpdate'] = (time.time() + days * 24 * 60 * 60)
        self.userconfig['updateDays'] = days
        self.save_config()

    def start_update(self):
        """Launches a webbrowser to the specified update URL."""
        self.open_url(self.config.get_string('updates/downloadURL'))

    def read_colors(self):
        """Returns a list of color schemes."""
        return tuple([
            os.path.splitext(os.path.basename(p))[0] for p in
            self.get_text_files(self.colors_dir)])

    def get_colors(self, colorscheme=None):
        """
        Returns RGB tuples for all 16 colors in <colorscheme>.txt, or
        data/init/colors.txt if no scheme is provided."""
        result = []
        f = os.path.join(self.df_dir, 'data', 'init', 'colors.txt')
        if colorscheme is not None:
            f = os.path.join(self.lnp_dir, 'colors', colorscheme+'.txt')
        for c in [
                'BLACK', 'BLUE', 'GREEN', 'CYAN', 'RED', 'MAGENTA', 'BROWN',
                'LGRAY', 'DGRAY', 'LBLUE', 'LGREEN', 'LCYAN', 'LRED',
                'LMAGENTA', 'YELLOW', 'WHITE']:
            result.append((
                int(self.settings.read_value(f, c+'_R')),
                int(self.settings.read_value(f, c+'_G')),
                int(self.settings.read_value(f, c+'_B'))))
        return result

    def load_colors(self, filename):
        """
        Replaces the current DF color scheme.

        Params:
          filename
            The name of the new colorscheme to install (filename without
            extension).
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        shutil.copyfile(
            os.path.join(self.lnp_dir, 'colors', filename),
            os.path.join(self.init_dir, 'colors.txt'))

    def save_colors(self, filename):
        """
        Save current keybindings to a file.

        Params:
            filename
                The name of the new keybindings file.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        filename = os.path.join(self.colors_dir, filename)
        shutil.copyfile(os.path.join(self.init_dir, 'colors.txt'), filename)
        self.read_colors()

    def color_exists(self, filename):
        """
        Returns whether or not a color scheme already exists.

        Params:
            filename
                The filename to check.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        return os.access(os.path.join(self.colors_dir, filename), os.F_OK)

    def delete_colors(self, filename):
        """
        Deletes a color scheme file.

        Params:
            filename
                The filename to delete.
        """
        if not filename.endswith('.txt'):
            filename = filename + '.txt'
        os.remove(os.path.join(self.colors_dir, filename))

    def read_hacks(self):
        """Reads which hacks are enabled."""
        try:
            f = open(os.path.join(self.df_dir, 'PyLNP_dfhack_onload.init'))
            hacklines = f.readlines()
            for h in self.get_hacks().values():
                h['enabled'] = h['command']+'\n' in hacklines
            f.close()
        except IOError:
            for h in self.get_hacks().values():
                h['enabled'] = False

    def get_hacks(self):
        """Returns dict of available hacks."""
        return self.config.get_dict('dfhack')

    def get_hack(self, title):
        """
        Returns the hack titled <title>, or None if this does not exist.

        Params:
            title
                The title of the hack.
        """
        try:
            return self.get_hacks()[title]
        except KeyError:
            return None

    def toggle_hack(self, name):
        """
        Toggles the hack <name>.

        Params:
            name
                The name of the hack to toggle.
        """
        self.get_hack(name)['enabled'] = not self.get_hack(name)['enabled']
        self.rebuild_hacks()

    def rebuild_hacks(self):
        """Rebuilds PyLNP_dfhack_onload.init with the enabled hacks."""
        f = open(os.path.join(self.df_dir, 'PyLNP_dfhack_onload.init'), 'w')
        f.write('# Generated by PyLNP\n\n')
        for k, h in self.get_hacks().items():
            if h['enabled']:
                f.write('# '+str(k)+'\n')
                f.write('# '+str(h['tooltip'])+'\n')
                f.write(h['command']+'\n\n')
        f.flush()
        f.close()

    def install_embarks(self, files):
        """
        Installs a list of embark profiles.

        Params:
            files
                List of files to install.
        """
        out = open(os.path.join(self.init_dir, 'embark_profiles.txt'), 'w')
        for f in files:
            embark = open(os.path.join(self.embarks_dir, f))
            out.write(embark.read()+"\n\n")
        out.flush()
        out.close()
Example #5
0
class PyLNP(object):
    """
    PyLNP library class.

    Acts as an abstraction layer between the UI and the Dwarf Fortress
    instance.
    """
    def __init__(self):
        """Constructor for the PyLNP library."""
        # pylint:disable=global-statement
        global lnp
        lnp = self
        self.args = self.parse_commandline()

        self.BASEDIR = '.'
        if sys.platform == 'win32':
            self.os = 'win'
        elif sys.platform.startswith('linux'):
            self.os = 'linux'
        elif sys.platform == 'darwin':
            self.os = 'osx'

        self.bundle = ''
        if hasattr(sys, 'frozen'):
            self.bundle = self.os
            os.chdir(os.path.dirname(sys.executable))
            if self.bundle == 'osx':
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.join(os.path.dirname(__file__), '..'))

        from . import update

        self.folders = []
        self.df_info = None
        self.settings = None
        self.running = {}
        self.autorun = []
        self.updater = None
        self.config = None
        self.userconfig = None
        self.ui = None

        self.initialize_program()

        self.initialize_df()

        self.new_version = None

        self.initialize_ui()
        update.check_update()
        self.ui.start()

    def initialize_program(self):
        """Initializes the main program (errorlog, path registration, etc.)."""
        from . import paths, utilities, errorlog
        self.BASEDIR = '.'
        self.detect_basedir()
        paths.clear()
        paths.register('root', self.BASEDIR)
        errorlog.start()

        paths.register('lnp', self.BASEDIR, 'LNP')
        if not os.path.isdir(paths.get('lnp')):
            print('WARNING: LNP folder is missing!', file=sys.stderr)
        paths.register('keybinds', paths.get('lnp'), 'Keybinds')
        paths.register('graphics', paths.get('lnp'), 'Graphics')
        paths.register('utilities', paths.get('lnp'), 'Utilities')
        paths.register('colors', paths.get('lnp'), 'Colors')
        paths.register('embarks', paths.get('lnp'), 'Embarks')
        paths.register('tilesets', paths.get('lnp'), 'Tilesets')
        paths.register('baselines', paths.get('lnp'), 'Baselines')
        paths.register('mods', paths.get('lnp'), 'Mods')

        config_file = 'PyLNP.json'
        if os.access(paths.get('lnp', 'PyLNP.json'), os.F_OK):
            config_file = paths.get('lnp', 'PyLNP.json')

        default_config = {
            "folders": [
                ["Savegame folder", "<df>/data/save"],
                ["Utilities folder", "LNP/Utilities"],
                ["Graphics folder", "LNP/Graphics"],
                ["-", "-"],
                ["Main folder", ""],
                ["LNP folder", "LNP"],
                ["Dwarf Fortress folder", "<df>"],
                ["Init folder", "<df>/data/init"]
            ],
            "links": [
                ["DF Homepage", "http://www.bay12games.com/dwarves/"],
                ["DF Wiki", "http://dwarffortresswiki.org/"],
                ["DF Forums", "http://www.bay12forums.com/smf/"]
            ],
            "hideUtilityPath": False,
            "hideUtilityExt": False,
            "updates": {
                "updateMethod": ""
            }
        }
        self.config = JSONConfiguration(config_file, default_config)
        self.userconfig = JSONConfiguration('PyLNP.user')
        self.autorun = []
        utilities.load_autorun()

    def initialize_df(self):
        """Initializes the DF folder and related variables."""
        from . import df
        self.df_info = None
        self.folders = []
        self.settings = None
        df.find_df_folder()

    def initialize_ui(self):
        """Instantiates the UI object."""
        from tkgui.tkgui import TkGui
        self.ui = TkGui()

    def reload_program(self):
        """Reloads the program to allow the user to change DF folders."""
        self.args.df_folder = None
        self.initialize_program()
        self.initialize_df()
        self.initialize_ui()
        self.ui.start()

    def parse_commandline(self):
        """Parses and acts on command line options."""
        args = self.get_commandline_args()
        if args.debug == 1:
            log.set_level(log.DEBUG)
        elif args.debug is not None and args.debug > 1:
            log.set_level(log.VERBOSE)
        log.d(args)
        return args

    @staticmethod
    def get_commandline_args():
        """Responsible for the actual parsing of command line options."""
        import argparse
        parser = argparse.ArgumentParser(
            description="PyLNP " +VERSION)
        parser.add_argument(
            '-d', '--debug', action='count',
            help='Turn on debugging output (use twice for extra verbosity)')
        parser.add_argument(
            '--raw-lint', action='store_true',
            help='Verify contents of raw files and exit')
        parser.add_argument(
            'df_folder', nargs='?',
            help='Dwarf Fortress folder to use (if it exists)')
        parser.add_argument(
            '--version', action='version', version="PyLNP "+VERSION)
        return parser.parse_known_args()[0]

    def save_config(self):
        """Saves LNP configuration."""
        self.userconfig.save_data()

    def detect_basedir(self):
        """Detects the location of Dwarf Fortress by walking up the directory
        tree."""
        prev_path = '.'

        from . import df
        try:
            while os.path.abspath(self.BASEDIR) != prev_path:
                df.find_df_folders()
                if len(self.folders) != 0:
                    return
                prev_path = os.path.abspath(self.BASEDIR)
                self.BASEDIR = os.path.join(self.BASEDIR, '..')
        except UnicodeDecodeError:
            print(
                "ERROR: PyLNP is being stored in a path containing non-ASCII "
                "characters, and cannot continue. Folder names may only use "
                "the characters A-Z, 0-9, and basic punctuation.\n"
                "Alternatively, you may run PyLNP from source using Python 3.",
                file=sys.stderr)
            sys.exit(1)
        log.e("Could not find any Dwarf Fortress installations.")
        sys.exit(2)
Example #6
0
 def initialize_ui(self):
     """Instantiates the UI object."""
     from tkgui.tkgui import TkGui
     self.ui = TkGui()
Example #7
0
class PyLNP(object):
    """
    PyLNP library class.

    Acts as an abstraction layer between the UI and the Dwarf Fortress
    instance.
    """
    def __init__(self):
        """Constructor for the PyLNP library."""
        # pylint:disable=global-statement
        global lnp
        lnp = self
        self.args = self.parse_commandline()

        self.BASEDIR = '.'
        if sys.platform == 'win32':
            self.os = 'win'
        elif sys.platform.startswith('linux'):
            self.os = 'linux'
        elif sys.platform == 'darwin':
            self.os = 'osx'

        self.bundle = ''
        if hasattr(sys, 'frozen'):
            self.bundle = self.os
            os.chdir(os.path.dirname(sys.executable))
            if self.bundle == 'osx':
                # OS X bundles start in different directory
                os.chdir('../../..')
        else:
            os.chdir(os.path.join(os.path.dirname(__file__), '..'))

        from . import update

        self.folders = []
        self.df_info = None
        self.settings = None
        self.running = {}
        self.autorun = []
        self.updater = None
        self.config = None
        self.userconfig = None
        self.ui = None

        self.initialize_program()

        self.initialize_df()

        self.new_version = None

        self.initialize_ui()
        update.check_update()
        from . import paths
        save_dir = paths.get('save')
        saves_exist = os.path.isdir(save_dir) and os.listdir(save_dir)
        if paths.get('df') and not saves_exist:
            self.ui.on_query_migration()
        self.ui.start()

    def initialize_program(self):
        """Initializes the main program (errorlog, path registration, etc.)."""
        from . import paths, utilities, errorlog
        self.BASEDIR = '.'
        self.detect_basedir()
        paths.clear()
        paths.register('root', self.BASEDIR)
        errorlog.start()

        paths.register('lnp', self.BASEDIR, 'LNP')
        if not os.path.isdir(paths.get('lnp')):
            log.w('LNP folder is missing!')
        paths.register('keybinds', paths.get('lnp'), 'Keybinds')
        paths.register('graphics', paths.get('lnp'), 'Graphics')
        paths.register('utilities', paths.get('lnp'), 'Utilities')
        paths.register('colors', paths.get('lnp'), 'Colors')
        paths.register('embarks', paths.get('lnp'), 'Embarks')
        paths.register('tilesets', paths.get('lnp'), 'Tilesets')
        paths.register('baselines', paths.get('lnp'), 'Baselines')
        paths.register('mods', paths.get('lnp'), 'Mods')

        config_file = 'PyLNP.json'
        if os.access(paths.get('lnp', 'PyLNP.json'), os.F_OK):
            config_file = paths.get('lnp', 'PyLNP.json')

        default_config = {
            "folders": [["Savegame folder", "<df>/data/save"],
                        ["Utilities folder", "LNP/Utilities"],
                        ["Graphics folder", "LNP/Graphics"], ["-", "-"],
                        ["Main folder", ""], ["LNP folder", "LNP"],
                        ["Dwarf Fortress folder", "<df>"],
                        ["Init folder", "<df>/data/init"]],
            "links": [["DF Homepage", "http://www.bay12games.com/dwarves/"],
                      ["DF Wiki", "http://dwarffortresswiki.org/"],
                      ["DF Forums", "http://www.bay12forums.com/smf/"]],
            "to_import":
            [['text_prepend', '<df>/gamelog.txt'],
             ['text_prepend', '<df>/ss_fix.log'],
             ['text_prepend', '<df>/dfhack.history'],
             ['copy_add', '<df>/data/save'],
             ['copy_add', '<df>/soundsense', 'LNP/Utilities/Soundsense/packs'],
             ['copy_add', 'LNP/Utilities/Soundsense/packs'],
             ['copy_add', 'User Generated Content']],
            "hideUtilityPath":
            False,
            "hideUtilityExt":
            False,
            "updates": {
                "updateMethod": ""
            }
        }
        self.config = JSONConfiguration(config_file, default_config)
        self.userconfig = JSONConfiguration('PyLNP.user')
        self.autorun = []
        utilities.load_autorun()

        if self.args.terminal_test_parent:
            from . import terminal
            sys.exit(
                terminal.terminal_test_parent(
                    self.args.terminal_test_parent[0]))
        if self.args.terminal_test_child:
            from . import terminal
            sys.exit(
                terminal.terminal_test_child(self.args.terminal_test_child[0]))

    def initialize_df(self):
        """Initializes the DF folder and related variables."""
        from . import df
        self.df_info = None
        self.folders = []
        self.settings = None
        df.find_df_folder()

    def initialize_ui(self):
        """Instantiates the UI object."""
        from tkgui.tkgui import TkGui
        self.ui = TkGui()

    def reload_program(self):
        """Reloads the program to allow the user to change DF folders."""
        self.args.df_folder = None
        self.initialize_program()
        self.initialize_df()
        self.initialize_ui()
        self.ui.start()

    def parse_commandline(self):
        """Parses and acts on command line options."""
        args = self.get_commandline_args()
        if args.debug == 1:
            log.set_level(log.DEBUG)
        elif args.debug is not None and args.debug > 1:
            log.set_level(log.VERBOSE)
        if args.release_prep:
            args.raw_lint = True
        log.d(args)
        return args

    @staticmethod
    def get_commandline_args():
        """Responsible for the actual parsing of command line options."""
        import argparse
        parser = argparse.ArgumentParser(description="PyLNP " + VERSION)
        parser.add_argument(
            '-d',
            '--debug',
            action='count',
            help='Turn on debugging output (use twice for extra verbosity)')
        parser.add_argument('--raw-lint',
                            action='store_true',
                            help='Verify contents of raw files and exit')
        parser.add_argument('df_folder',
                            nargs='?',
                            help='Dwarf Fortress folder to use (if it exists)')
        parser.add_argument('--version',
                            action='version',
                            version="PyLNP " + VERSION)
        parser.add_argument('--df-executable',
                            action='store',
                            help='Override DF/DFHack executable name')
        parser.add_argument('--release-prep',
                            action='store_true',
                            help=argparse.SUPPRESS)
        parser.add_argument('--terminal-test-parent',
                            nargs=1,
                            help=argparse.SUPPRESS)
        parser.add_argument('--terminal-test-child',
                            nargs=1,
                            help=argparse.SUPPRESS)
        return parser.parse_known_args()[0]

    def save_config(self):
        """Saves LNP configuration."""
        self.userconfig.save_data()

    def detect_basedir(self):
        """Detects the location of Dwarf Fortress by walking up the directory
        tree."""
        prev_path = '.'

        from . import df
        try:
            while os.path.abspath(self.BASEDIR) != prev_path:
                df.find_df_folders()
                if len(self.folders) != 0:
                    return
                # pylint:disable=redefined-variable-type
                prev_path = os.path.abspath(self.BASEDIR)
                self.BASEDIR = os.path.join(self.BASEDIR, '..')
        except UnicodeDecodeError:
            # This seems to no longer be an issue, but leaving in the check
            # just in case
            log.e(
                "PyLNP is being stored in a path containing non-ASCII "
                "characters, and cannot continue. Folder names may only use "
                "the characters A-Z, 0-9, and basic punctuation.\n"
                "Alternatively, you may run PyLNP from source using Python 3.")
            sys.exit(1)
        log.e("Could not find any Dwarf Fortress installations.")
        sys.exit(2)
Example #8
0
 def initialize_ui(self):
     """Instantiates the UI object."""
     from tkgui.tkgui import TkGui
     self.ui = TkGui()