From 1bb0f2d45719f5ff989e6632cb4ffe23d459258f Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 11:11:54 +1100 Subject: [PATCH 01/29] ignore the testing data --- .hgignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.hgignore b/.hgignore index c9f4347..84b0f78 100644 --- a/.hgignore +++ b/.hgignore @@ -6,3 +6,7 @@ dist PyLNP.user stderr.txt stdout.txt + +Dwarf Fortress * +LNP +__pycache__ From 0fd9ae1bf809c6a2c3695fb843fe1fab91ea25f2 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 11:30:47 +1100 Subject: [PATCH 02/29] Working graphics tab; rewrite graphics install to use reduced raw format. Still a lot of style cleanup and deduplication to go, now I've got this in a auto-mergeable repo. --HG-- branch : mods-tab-and-graphics-loading --- core/ModProcessor.py | 247 +++++++++++++++++++++++++++++++++++++++++++ core/graphics.py | 84 +++------------ core/raws.py | 226 +++++++++++++++++++++++++++++++++++++++ tkgui/mods.py | 180 +++++++++++++++++++++++++++++++ tkgui/tkgui.py | 2 + 5 files changed, 672 insertions(+), 67 deletions(-) create mode 100644 core/ModProcessor.py create mode 100644 core/raws.py create mode 100644 tkgui/mods.py diff --git a/core/ModProcessor.py b/core/ModProcessor.py new file mode 100644 index 0000000..bd1bc6b --- /dev/null +++ b/core/ModProcessor.py @@ -0,0 +1,247 @@ +import os, shutil, filecmp, glob, tempfile +import difflib, sys, time + +from . import paths +from . import raws + +paths.register('mods', 'LNP', 'mods') + +def simplify_mod_and_df_folders_to_defined_format(): + raws.simplify_mods() + +def do_merge_seq (mod_text, vanilla_text, gen_text): + """Merges sequences of lines. Returns empty string if a line changed by the mod + has been changed by a previous mod, or merged lines otherwise. + + Params: + mod_text + The lines of the mod file being added to the merge. + vanilla_text + The lines of the corresponding vanilla file. + gen_text + The lines of the previously merged file. + + Returns: + Merged lines if no changes in mod and gen files overlap. + Empty string otherwise. + """ + # special cases - where merging is not required because two are equal + if vanilla_text == gen_text: + return mod_text + if vanilla_text == mod_text: + return gen_text + + # returns list of 5-tuples describing how to turn vanilla into mod or gen lines + # each specifies an operation, and start+end line nums for each change + # we then compose a text from these by concatenation, + # returning false if the mod changes lines which have already been changed. + van_mod_match = difflib.SequenceMatcher(None, vanilla_text, mod_text) + van_gen_match = difflib.SequenceMatcher(None, vanilla_text, gen_text) + van_mod_ops = van_mod_match.get_opcodes() + van_gen_ops = van_gen_match.get_opcodes() + + output_file_temp = [] + # holds the line we're up to, effectively truncates blocks which were + # partially covered in the previous iteration. + cur_v = 0 + while van_mod_ops and van_gen_ops: + # get names from the next set of opcodes + mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] + gen_tag, gen_i1, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] + # if the mod is vanilla for these lines + if mod_tag == 'equal': + # if the gen lines are also vanilla + if gen_tag == 'equal' : + # append the shorter block to new genned lines + if mod_i2 < gen_i2: + output_file_temp += vanilla_text[cur_v:mod_i2] + cur_v = mod_i2 + van_mod_ops.pop(0) + else: + output_file_temp += vanilla_text[cur_v:gen_i2] + cur_v = gen_i2 + van_gen_ops.pop(0) + if mod_i2 == gen_i2 : + van_mod_ops.pop(0) + # otherwise append current genned lines + else: + output_file_temp += gen_text[gen_j1:gen_j2] + cur_v = gen_i2 + van_gen_ops.pop(0) + if mod_i2 == gen_i2 : + van_mod_ops.pop(0) + # if mod has changes from vanilla + else: + # if no earlier mod changed this section, adopt these changes + if gen_tag == 'equal': + output_file_temp += mod_text[mod_j1:mod_j2] + cur_v = mod_i2 + van_mod_ops.pop(0) + if mod_i2 == gen_i2 : + van_gen_ops.pop(0) + # if the changes would overlap, we can't handle that yet + else: + # we'll revisit this later, to allow overlapping merges + # (important for graphics pack inclusion) + # probably have to return (status, lines) tuple. + return '' + # clean up trailing opcodes, to avoid dropping the end of the file + if van_mod_ops: + mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] + output_file_temp += mod_text[mod_j1:mod_j2] + if van_gen_ops: + gen_tag, gen_i1, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] + output_file_temp += gen_text[gen_j1:gen_j2] + return output_file_temp + +def get_lines_from_file(filename): + """Get lines from a file, managing encoding. + Trying to merge files with diferent encoding causes errors; + a strict encoding often merges but at worst produces known issues. + Unicode handling changed a lot between versions... + TODO: test and improve this function! + """ + if sys.version_info[0] == 3: # Python 3.4 works for me + return open(filename, encoding='utf-8', errors='ignore').readlines() + else: # Forget handling encoding and hope for the best + return open(filename).readlines() + +def do_merge_files(mod_file_name, van_file_name, gen_file_name): + ''' calls merge sequence on the files, and returns true if they could be (and were) merged + or false if the merge was conflicting (and thus skipped). + ''' + van_lines = get_lines_from_file(van_file_name) + mod_lines = get_lines_from_file(mod_file_name) + gen_lines = [] + if os.path.isfile(gen_file_name): + gen_lines = get_lines_from_file(gen_file_name) + + gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) + if gen_lines: + gen_file = open(gen_file_name,"w") + for line in gen_lines: + gen_file.write(line) + return True + else: + return False + +def merge_a_mod(mod): + '''Merges the specified mod, and returns status (0|1|2|3) like an exit code. + + 0: Merge was successful, all well + 1: Potential compatibility issues, no merge problems + 2: Non-fatal error, overlapping lines or non-existent mod etc + 3: Fatal error, respond by rebuilding to previous mod''' + mod_raw_folder = os.path.join(mods_folder, mod, 'raw') + if not os.path.isdir(mod_raw_folder): + return 2 + status = merge_raw_folders(mod_raw_folder, vanilla_raw_folder) + if status < 2: + with open(os.path.join(mods_folder, 'temp', 'raw', 'installed_mods.txt'), 'a') as log: + log.write(mod + '\n') + return status + +def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): + '''Behind the wrapper, to allow tricks by overriding the global variables''' + status = 0 + for file_tuple in os.walk(mod_raw_folder): + for item in file_tuple[2]: + file = os.path.join(file_tuple[0], item) + file = os.path.relpath(file, mod_raw_folder) + if os.path.isfile(os.path.join(vanilla_raw_folder, file)): + if not do_merge_files(os.path.join(mod_raw_folder, file), + os.path.join(vanilla_raw_folder, file), + os.path.join(mixed_raw_folder, file)): + return 3 + elif os.path.isfile(os.path.join(mixed_raw_folder, file)): + return 3 + else: + shutil.copy(os.path.join(mod_raw_folder, file), + os.path.join(mixed_raw_folder, file)) + status = 1 + return status + +def clear_temp(): + global mixed_raw_folder, vanilla_raw_folder + mixed_raw_folder = os.path.join(mods_folder, 'temp', 'raw') + if os.path.exists(os.path.join(mods_folder, 'temp')): + shutil.rmtree(os.path.join(mods_folder, 'temp')) + shutil.copytree(vanilla_raw_folder, mixed_raw_folder) + with open(os.path.join(mods_folder, 'temp', 'raw', 'installed_mods.txt'), + 'w') as log: + log.write('# List of mods merged by PyLNP mod loader\n' + + os.path.dirname(vanilla_raw_folder)[:-4] + '\n') + +def read_mods(): + """Returns a list of mod packs""" + # should go in tkgui/mods.py later + return [os.path.basename(o) for o in + glob.glob(os.path.join(paths.get('mods'), '*')) + if os.path.isdir(o) and not os.path.basename(o)=='temp'] + +def init_paths(lnpdir): + global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder + raws.simplify_mods() + installed_raw_folder = os.path.join(paths.get('df'), 'raw') + mods_folder = os.path.join(lnpdir, 'Mods') + vanilla_raw_folder = raws.find_vanilla_raws(version=None) + mod_folders_list = read_mods() + clear_temp() + +def make_mod_from_installed_raws(name): + '''Capture whatever unavailable mods a user currently has installed as a mod called $name. + + * If `installed_mods.txt` is not present, compare to vanilla + * Otherwise, rebuild as much as possible then compare installed and rebuilt + * Harder than I first thought... but not impossible''' + mod_load_order = get_installed_mods_from_log() + if mod_load_order: + clear_temp() + for mod in mod_load_order: + merge_a_mod(mod) + best_effort_reconstruction = os.path.join(mods_folder, 'temp2', 'raw') + shutil.copytree(os.path.join(mods_folder, 'temp', 'raw'), + os.path.join(mods_folder, 'temp2', 'raw')) + else: + best_effort_reconstruction = vanilla_raw_folder + + clear_temp() + merge_raw_folders(best_effort_reconstruction, installed_raw_folder) + simplify_mod_folder('temp') + if os.path.isdir(os.path.join(mods_folder, 'temp2')): + shutil.rmtree(os.path.join(mods_folder, 'temp2')) + + if os.path.isdir(os.path.join(mods_folder, 'temp')): + if not name or os.path.isdir(os.path.join(mods_folder, name)): + name = 'Extracted Mod at '+str(int(time.time())) + shutil.copytree(os.path.join(mods_folder, 'temp'), os.path.join(mods_folder, name)) + return_val = 'User unique mods extracted as "'+str(name)+'"' + return return_val + else: + return 'No unique user mods found.' + +def get_installed_mods_from_log(): + '''Return best possible mod load order to recreate installed with available''' + logged = read_installation_log(os.path.join(installed_raw_folder, 'installed_mods.txt')) + # return list overlap - like set intersection, but ordered + return [mod for mod in logged if mod in mods_folders_list] + +def read_installation_log(file): + '''Read an 'installed_mods.txt' and return it's full contents.''' + try: + with open(file) as f: + file_contents = list(f.readlines()) + except IOError: + return [] + mods_list = [] + for line in file_contents: + if not line.strip() or line.startswith('#'): + continue + mods_list.append(line.strip()) + return mods_list + +mods_folder = os.path.join('LNP', 'Mods') +vanilla_folder = '' +vanilla_raw_folder = '' +mixed_raw_folder = '' +mod_folders_list = read_mods() diff --git a/core/graphics.py b/core/graphics.py index 669bb34..9655725 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -7,7 +7,7 @@ import distutils.dir_util as dir_util from .launcher import open_folder from .lnp import lnp -from . import colors, df, paths +from . import colors, df, paths, raws def open_graphics(): """Opens the graphics pack folder.""" @@ -58,7 +58,7 @@ def install_graphics(pack): False if an exception occured None if required files are missing (raw/graphics, data/init) """ - gfx_dir = os.path.join(paths.get('graphics'), pack) + gfx_dir = raws.rebuild_pack(pack, 'graphics') 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'))): @@ -104,13 +104,20 @@ def install_graphics(pack): pass except Exception: sys.excepthook(*sys.exc_info()) - result = False + if os.path.isdir(gfx_dir): + dir_util.remove_tree(gfx_dir) + return False else: - result = True - df.load_params() - return result + if os.path.isdir(gfx_dir): + dir_util.remove_tree(gfx_dir) + return True else: + if os.path.isdir(gfx_dir): + dir_util.remove_tree(gfx_dir) return None + if os.path.isdir(gfx_dir): + dir_util.remove_tree(gfx_dir) + df.load_params() def validate_pack(pack): """Checks for presence of all required files for a pack install.""" @@ -193,7 +200,7 @@ def patch_inits(gfx_dir): 'TREE_TRUNK_INTERIOR', 'TREE_TRUNK_INTERIOR_DEAD'] init_fields = [ 'FONT', 'FULLFONT', 'GRAPHICS', 'GRAPHICS_FONT', - 'GRAPHICS_FULLFONT', 'TRUETYPE', 'PRINT_MODE'] + 'GRAPHICS_FULLFONT', 'TRUETYPE'] init_fields = [f for f in init_fields if lnp.settings.version_has_option(f)] d_init_fields = [ f for f in d_init_fields if lnp.settings.version_has_option(f)] @@ -208,68 +215,11 @@ def patch_inits(gfx_dir): def simplify_graphics(): """Removes unnecessary files from all graphics packs.""" - for pack in read_graphics(): - simplify_pack(pack) + raws.simplify_graphics() def simplify_pack(pack): - """ - Removes unnecessary files from LNP/Graphics/. - - 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(paths.get('graphics'), 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 + """Removes unnecessary files from one graphics pack.""" + raws.simplify_graphics_pack(pack) def savegames_to_update(): """Returns a list of savegames that will be updated.""" diff --git a/core/raws.py b/core/raws.py new file mode 100644 index 0000000..c7d0f2e --- /dev/null +++ b/core/raws.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Advanced raw and data folder management, for mods or graphics packs.""" +from __future__ import print_function, unicode_literals, absolute_import + +import os, shutil, filecmp, sys, glob, tempfile +import distutils.dir_util as dir_util + +from . import paths + +def read_graphics(): + """Returns a list of graphics directories.""" + graphics_path = paths.get('graphics') + packs = [ + os.path.basename(o) for o in + glob.glob(os.path.join(graphics_path, '*')) if + os.path.isdir(o)] + result = [] + for p in packs: + if not validate_pack(p): + continue + init_path = os.path.join(graphics_path, p, 'data', 'init', 'init.txt') + font, graphics = lnp.settings.read_values( + init_path, 'FONT', 'GRAPHICS_FONT') + result.append((p, font, graphics)) + return tuple(result) + +def read_mods(): + """Returns a list of mod packs""" + # should go in tkgui/mods.py later + return [os.path.basename(o) for o in + glob.glob(os.path.join(paths.get('mods'), '*')) + if os.path.isdir(o)] + +paths.register('baselines', 'LNP', 'baselines') + +def rebuild_pack(pack, folder='graphics'): + """Takes a reduced graphics pack, and rebuilds it using vanilla raws in a tempdir. + The tmpdir MUST be removed by the calling function after use. + (TODO: pass a filelike object instead?) + + Params: + pack + The pack to simplify. + folder + The parent folder of the pack (either 'mods' or 'graphics') + + Returns: + The full path to a tmpdir containingthe rebuild graphics pack. + """ + if not (folder=='graphics' or folder=='mods'): + return False + pack = os.path.join(paths.get(folder), pack) + tmp = tempfile.mkdtemp() + dir_util.copy_tree(find_vanilla_raws(), tmp) + dir_util.copy_tree(pack, tmp) + return tmp + +def simplify_graphics(): + """Removes unnecessary files from all graphics packs.""" + for pack in read_graphics(): + simplify_graphics_pack() + +def simplify_graphics_pack(pack): + """Removes unnecessary files from one graphics pack.""" + simplify_pack(pack, 'graphics') + remove_vanilla_raws_from_pack(pack, 'graphics') + remove_empty_dirs(pack, 'graphics') + +def simplify_mods(): + """Removes unnecessary files from all mod packs.""" + for pack in read_mods(): + simplify_pack(pack, 'mods') + remove_vanilla_raws_from_pack(pack, 'mods') + remove_empty_dirs(pack, 'mods') + +def find_vanilla_raws(version=''): + # override for testing: + return os.path.join('LNP', 'Baselines', 'df_40_11_win', 'raw') + """Finds vanilla raws for the requested version. + If required, unzip a DF release to create the folder in LNP/Baselines/. + + Params: + version + String indicating version in format 'df_40_15' + None returns the latest available version. + + Returns: + The path to a vanilla 'raw' folder + None if path could not be found or made + """ + baselines_path = os.path.join(paths.get('baselines')) + available = [os.path.relpath(item, baselines_path) for item in + glob.glob(os.path.join(baselines_path, 'df_??_?*'))] + if version == None: + version = available[-1][0:8] + version_dir = os.path.join(baselines_path, version + '_win') + + if os.path.isdir(version_dir): + return os.path.join(version_dir, 'raw') + elif os.path.isfile(version_dir + '.zip'): + file = zipfile.ZipFile(version_dir + '.zip') + file.extractall(version_dir) + return os.path.join(version_dir, 'raw') + # TODO: attempt to download requested file from Bay12 Games + return None + +def simplify_pack(pack, folder): + """Removes unnecessary files from LNP//. + Necessary files means: + * 'raw/objects/' and 'raw/graphics/' + * 'data/art/' for graphics, and specific files in 'data/init/' + * readme.txt and manifest.json for mods + + Params: + pack + The pack to simplify. + folder + The parent folder of the pack (either 'mods' or 'graphics') + + Returns: + The number of files removed if successful + False if an exception occurred + None if folder is empty + """ + if not (folder=='graphics' or folder=='mods'): + return False + pack = os.path.join(paths.get(folder), 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, 'raw', 'graphics')) + os.makedirs(os.path.join(pack, 'raw', 'objects')) + + if os.path.exists(os.path.join(tmp, 'raw', 'graphics')): + dir_util.copy_tree( + os.path.join(tmp, 'raw', 'graphics'), + os.path.join(pack, 'raw', 'graphics')) + if os.path.exists(os.path.join(tmp, 'raw', 'objects')): + dir_util.copy_tree( + os.path.join(tmp, 'raw', 'objects'), + os.path.join(pack, 'raw', 'objects')) + + if folder=='mods'and os.path.exists(os.path.join(tmp, 'manifest.json')): + shutil.copyfile( + os.path.join(tmp, 'manifest.json'), + os.path.join(pack, 'manifest.json')) + if folder=='mods'and os.path.exists(os.path.join(tmp, 'readme.txt')): + shutil.copyfile( + os.path.join(tmp, 'readme.txt'), + os.path.join(pack, 'readme.txt')) + if folder=='mods'and os.path.exists(os.path.join(tmp, 'installed_mods.txt')): + shutil.copyfile( + os.path.join(tmp, 'raw', 'installed_mods.txt'), + os.path.join(pack, 'raw', 'installed_mods.txt')) + + if folder=='graphics': + os.makedirs(os.path.join(pack, 'data', 'init')) + os.makedirs(os.path.join(pack, 'data', 'art')) + dir_util.copy_tree( + os.path.join(tmp, 'data', 'art'), + os.path.join(pack, 'data', 'art')) + for filename in ('colors.txt', 'init.txt', + 'd_init.txt'): #, 'overrides.txt' + shutil.copyfile( + os.path.join(tmp, 'data', 'init', filename), + os.path.join(pack, 'data', 'init', filename)) + + 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 remove_vanilla_raws_from_pack(pack, folder): + """Remove files identical to vanilla raws, return files removed + + Params: + pack + The pack to simplify. + folder + The parent folder of the pack (either 'mods' or 'graphics') + """ + raw_folder = os.path.join(paths.get(folder), pack, 'raw') + vanilla_raw_folder = find_vanilla_raws() + for root, dirs, files in os.walk(raw_folder): + for file in files: + file = os.path.join(root, file) + # silently clean up so empty dirs can be removed + silently_kill = ('Thumbs.db', 'installed_mods.txt') + if any(file.endswith(k) for k in silently_kill): + os.remove(file) + continue + file = os.path.relpath(file, raw_folder) + # if there's an identical vanilla file, remove the mod file + if os.path.isfile(os.path.join(vanilla_raw_folder, file)): + if filecmp.cmp(os.path.join(vanilla_raw_folder, file), + os.path.join(raw_folder, file)): + os.remove(os.path.join(raw_folder, file)) + +def remove_empty_dirs(pack, folder): + """Removes empty subdirs in a mods or graphics pack. + + Params: + pack + The pack to simplify. + folder + The parent folder of the pack (either 'mods' or 'graphics') + """ + pack = os.path.join(paths.get(folder), pack) + for n in range(3): + # only catches the lowest level each iteration + for root, dirs, files in os.walk(pack): + if not dirs and not files: + os.rmdir(root) diff --git a/tkgui/mods.py b/tkgui/mods.py new file mode 100644 index 0000000..373da0e --- /dev/null +++ b/tkgui/mods.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint:disable=unused-wildcard-import,wildcard-import,invalid-name,attribute-defined-outside-init +"""Mods tab for the TKinter GUI.""" +from __future__ import print_function, unicode_literals, absolute_import + +from . import controls +from .tab import Tab +import sys, os, shutil + +from core import ModProcessor +from core import paths + +if sys.version_info[0] == 3: # Alternate import names + # pylint:disable=import-error + from tkinter import * + from tkinter.ttk import * + import tkinter.simpledialog as simpledialog +else: + # pylint:disable=import-error + from Tkinter import * + from ttk import * + import tkSimpleDialog as simpledialog + +class ModsTab(Tab): + """Mods tab for the TKinter GUI.""" + def create_variables(self): + self.installed = Variable() + self.available = Variable() + + def read_data(self): + ModProcessor.init_paths(paths.get('lnp')) + available = ModProcessor.mod_folders_list + installed = ModProcessor.get_installed_mods_from_log() + available = [m for m in available if m not in installed] + self.available.set(tuple(available)) + self.installed.set(tuple(installed)) + + def create_controls(self): + Grid.columnconfigure(self, 0, weight=1) + Grid.rowconfigure(self, 0, weight=1) + Grid.rowconfigure(self, 2, weight=1) + + f = controls.create_control_group(self, 'Installed') + install_frame, self.installed_list = controls.create_file_list( + f, None, self.installed, selectmode='multiple') + self.installed_list.bind( + "", lambda e: self.remove_from_installed()) + reorder_frame = controls.create_control_group(install_frame, None) + controls.create_trigger_button( + reorder_frame, '↑', 'Move up', self.move_up).pack() + controls.create_trigger_button( + reorder_frame, '↓', 'Move down', self.move_down).pack() + reorder_frame.grid(row=0, column=2, sticky="nse") + + f.grid(row=0, column=0, sticky="nsew") + + f = controls.create_control_group(self, None, True) + controls.create_trigger_button( + f, '⇑', 'Add', self.add_to_installed).grid( + row=0, column=0, sticky="nsew") + controls.create_trigger_button( + f, '⇓', 'Remove', self.remove_from_installed).grid( + row=0, column=1, sticky="nsew") + f.grid(row=1, column=0, sticky="ew") + + f = controls.create_control_group(self, 'Available') + _, self.available_list = controls.create_file_list( + f, None, self.available, selectmode='multiple') + self.available_list.bind( + "", lambda e: self.add_to_installed()) + f.grid(row=2, column=0, sticky="nsew") + + f = controls.create_control_group(self, None, True) + controls.create_trigger_button( + f, 'Simplify Mods', 'Simplify Mods', + ModProcessor.simplify_mod_and_df_folders_to_defined_format).grid( + row=0, column=0, sticky="nsew") + controls.create_trigger_button( + f, 'Install Mods', 'Copy "installed" mods to DF folder. ' + 'WARNING: do not combine with graphics. May be unstable.', + self.install_mods).grid(row=0, column=1, sticky="nsew") + controls.create_trigger_button( + f, 'Create Mod from Installed', 'Creates a mod from unique changes ' + 'to your installed raws. Use to preserve custom tweaks.', + self.create_from_installed).grid( + row=1, column=0, sticky="nsew", columnspan=2) + f.grid(row=3, column=0, sticky="ew") + + def move_up(self): + if len(self.installed_list.curselection()) == 0: + return + selection = [int(i) for i in self.installed_list.curselection()] + newlist = list(self.installed_list.get(0, END)) + for i in range(1, len(newlist)): + j = i + while j in selection and i-1 not in selection and j < len(newlist): + newlist[j-1], newlist[j] = newlist[j], newlist[j-1] + j += 1 + self.installed_list.delete(0, END) + for i in newlist: + self.installed_list.insert(END, i) + first_missed = False + for i in range(0, len(newlist)): + if i not in selection: + first_missed = True + else: + self.installed_list.select_set(i - int(first_missed)) + self.perform_merge() + + def move_down(self): + if len(self.installed_list.curselection()) == 0: + return + selection = [int(i) for i in self.installed_list.curselection()] + newlist = list(self.installed_list.get(0, END)) + for i in range(len(newlist) - 1, 0, -1): + j = i + while i not in selection and j-1 in selection and j > 0: + newlist[j-1], newlist[j] = newlist[j], newlist[j-1] + j -= 1 + self.installed_list.delete(0, END) + for i in newlist: + self.installed_list.insert(END, i) + first_missed = False + for i in range(len(newlist), 0, -1): + if i - 1 not in selection: + first_missed = True + else: + self.installed_list.select_set(i - 1 + int(first_missed)) + self.perform_merge() + + def create_from_installed(self): + m = simpledialog.askstring("Create Mod", "New mod name:") + if m is not None and m != '': + ModProcessor.make_mod_from_installed_raws(m) + self.read_data() + + def add_to_installed(self): + if len(self.available_list.curselection()) == 0: + return + for i in self.available_list.curselection(): + self.installed_list.insert(END, self.available_list.get(i)) + for i in self.available_list.curselection()[::-1]: + self.available_list.delete(i) + self.perform_merge() + + def remove_from_installed(self): + if len(self.installed_list.curselection()) == 0: + return + for i in self.installed_list.curselection()[::-1]: + self.available_list.insert(END, self.installed_list.get(i)) + self.installed_list.delete(i) + + #Re-sort items + temp_list = sorted(list(self.available_list.get(0, END))) + self.available_list.delete(0, END) + for item in temp_list: + self.available_list.insert(END, item) + + self.perform_merge() + + def perform_merge(self): + ModProcessor.clear_temp() + # Set status to unknown before merging + for i, _ in enumerate(self.installed_list.get(0, END)): + self.installed_list.itemconfig(i, bg='white') + status = 3 + colors = ['green', 'yellow', 'orange', 'red'] + for i, mod in enumerate(self.installed_list.get(0, END)): + status = ModProcessor.merge_a_mod(mod) + self.installed_list.itemconfig(i, bg=colors[status]) + if status == 3: + return + + def install_mods(self): + # Assumption: Everything in Mods/temp goes into the DF folder. Adjust + # if needed. + shutil.rmtree(os.path.join(paths.get('df'), 'raw')) + shutil.copytree(os.path.join(paths.get('lnp'), 'Mods', 'temp', 'raw'), + os.path.join(paths.get('df'), 'raw')) diff --git a/tkgui/tkgui.py b/tkgui/tkgui.py index a93e616..d507ef2 100644 --- a/tkgui/tkgui.py +++ b/tkgui/tkgui.py @@ -17,6 +17,7 @@ from .utilities import UtilitiesTab from .advanced import AdvancedTab from .dfhack import DFHackTab +from .mods import ModsTab from core.lnp import lnp from core import df, launcher, paths, update @@ -144,6 +145,7 @@ def __init__(self): self.create_tab(AdvancedTab, 'Advanced') if 'dfhack' in lnp.df_info.variations: self.create_tab(DFHackTab, 'DFHack') + self.create_tab(ModsTab, 'Mods') n.enable_traversal() n.pack(fill=BOTH, expand=Y, padx=2, pady=3) From 6145c416e128691f297a8b782633c83a18093a5c Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 11:36:05 +1100 Subject: [PATCH 03/29] Rename ModProcessor to fit system --HG-- branch : mods-tab-and-graphics-loading --- core/{ModProcessor.py => mods.py} | 0 tkgui/mods.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 8 deletions(-) rename core/{ModProcessor.py => mods.py} (100%) diff --git a/core/ModProcessor.py b/core/mods.py similarity index 100% rename from core/ModProcessor.py rename to core/mods.py diff --git a/tkgui/mods.py b/tkgui/mods.py index 373da0e..4070e44 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -8,7 +8,7 @@ from .tab import Tab import sys, os, shutil -from core import ModProcessor +from core import mods from core import paths if sys.version_info[0] == 3: # Alternate import names @@ -29,9 +29,9 @@ def create_variables(self): self.available = Variable() def read_data(self): - ModProcessor.init_paths(paths.get('lnp')) - available = ModProcessor.mod_folders_list - installed = ModProcessor.get_installed_mods_from_log() + mods.init_paths(paths.get('lnp')) + available = mods.mod_folders_list + installed = mods.get_installed_mods_from_log() available = [m for m in available if m not in installed] self.available.set(tuple(available)) self.installed.set(tuple(installed)) @@ -74,7 +74,7 @@ def create_controls(self): f = controls.create_control_group(self, None, True) controls.create_trigger_button( f, 'Simplify Mods', 'Simplify Mods', - ModProcessor.simplify_mod_and_df_folders_to_defined_format).grid( + mods.simplify_mod_and_df_folders_to_defined_format).grid( row=0, column=0, sticky="nsew") controls.create_trigger_button( f, 'Install Mods', 'Copy "installed" mods to DF folder. ' @@ -132,7 +132,7 @@ def move_down(self): def create_from_installed(self): m = simpledialog.askstring("Create Mod", "New mod name:") if m is not None and m != '': - ModProcessor.make_mod_from_installed_raws(m) + mods.make_mod_from_installed_raws(m) self.read_data() def add_to_installed(self): @@ -160,14 +160,14 @@ def remove_from_installed(self): self.perform_merge() def perform_merge(self): - ModProcessor.clear_temp() + mods.clear_temp() # Set status to unknown before merging for i, _ in enumerate(self.installed_list.get(0, END)): self.installed_list.itemconfig(i, bg='white') status = 3 colors = ['green', 'yellow', 'orange', 'red'] for i, mod in enumerate(self.installed_list.get(0, END)): - status = ModProcessor.merge_a_mod(mod) + status = mods.merge_a_mod(mod) self.installed_list.itemconfig(i, bg=colors[status]) if status == 3: return From f486ee37410bf7455759803752f08878ea299032 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 12:10:25 +1100 Subject: [PATCH 04/29] Fixed logical placement of functions. I'm now happy that there's a decent system to where functions reside, including SST and a tighter focus for raws.py --HG-- branch : mods-tab-and-graphics-loading --- core/graphics.py | 12 ++++++--- core/mods.py | 26 +++++++++++++------- core/raws.py | 64 ------------------------------------------------ tkgui/mods.py | 5 ++-- 4 files changed, 29 insertions(+), 78 deletions(-) diff --git a/core/graphics.py b/core/graphics.py index 9655725..f39e01f 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -58,7 +58,10 @@ def install_graphics(pack): False if an exception occured None if required files are missing (raw/graphics, data/init) """ - gfx_dir = raws.rebuild_pack(pack, 'graphics') + gfx_dir = tempfile.mkdtemp() + dir_util.copy_tree(raws.find_vanilla_raws(), tmp) + dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), tmp) + 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'))): @@ -215,11 +218,14 @@ def patch_inits(gfx_dir): def simplify_graphics(): """Removes unnecessary files from all graphics packs.""" - raws.simplify_graphics() + for pack in read_graphics(): + simplify_pack(pack) def simplify_pack(pack): """Removes unnecessary files from one graphics pack.""" - raws.simplify_graphics_pack(pack) + raws.simplify_pack(pack, 'graphics') + raws.remove_vanilla_raws_from_pack(pack, 'graphics') + raws.remove_empty_dirs(pack, 'graphics') def savegames_to_update(): """Returns a list of savegames that will be updated.""" diff --git a/core/mods.py b/core/mods.py index bd1bc6b..bd0abb5 100644 --- a/core/mods.py +++ b/core/mods.py @@ -6,8 +6,23 @@ paths.register('mods', 'LNP', 'mods') -def simplify_mod_and_df_folders_to_defined_format(): - raws.simplify_mods() +def read_mods(): + """Returns a list of mod packs""" + # should go in tkgui/mods.py later + return [os.path.basename(o) for o in + glob.glob(os.path.join(paths.get('mods'), '*')) + if os.path.isdir(o) and not os.path.basename(o)=='temp'] + +def simplify_mods(): + """Removes unnecessary files from all mods.""" + for pack in read_mods(): + simplify_pack(pack, 'mods') + +def simplify_pack(pack): + """Removes unnecessary files from one mod.""" + raws.simplify_pack(pack, 'mods') + raws.remove_vanilla_raws_from_pack(pack, 'mods') + raws.remove_empty_dirs(pack, 'mods') def do_merge_seq (mod_text, vanilla_text, gen_text): """Merges sequences of lines. Returns empty string if a line changed by the mod @@ -172,13 +187,6 @@ def clear_temp(): log.write('# List of mods merged by PyLNP mod loader\n' + os.path.dirname(vanilla_raw_folder)[:-4] + '\n') -def read_mods(): - """Returns a list of mod packs""" - # should go in tkgui/mods.py later - return [os.path.basename(o) for o in - glob.glob(os.path.join(paths.get('mods'), '*')) - if os.path.isdir(o) and not os.path.basename(o)=='temp'] - def init_paths(lnpdir): global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder raws.simplify_mods() diff --git a/core/raws.py b/core/raws.py index c7d0f2e..d72cd06 100644 --- a/core/raws.py +++ b/core/raws.py @@ -8,72 +8,8 @@ from . import paths -def read_graphics(): - """Returns a list of graphics directories.""" - graphics_path = paths.get('graphics') - packs = [ - os.path.basename(o) for o in - glob.glob(os.path.join(graphics_path, '*')) if - os.path.isdir(o)] - result = [] - for p in packs: - if not validate_pack(p): - continue - init_path = os.path.join(graphics_path, p, 'data', 'init', 'init.txt') - font, graphics = lnp.settings.read_values( - init_path, 'FONT', 'GRAPHICS_FONT') - result.append((p, font, graphics)) - return tuple(result) - -def read_mods(): - """Returns a list of mod packs""" - # should go in tkgui/mods.py later - return [os.path.basename(o) for o in - glob.glob(os.path.join(paths.get('mods'), '*')) - if os.path.isdir(o)] - paths.register('baselines', 'LNP', 'baselines') -def rebuild_pack(pack, folder='graphics'): - """Takes a reduced graphics pack, and rebuilds it using vanilla raws in a tempdir. - The tmpdir MUST be removed by the calling function after use. - (TODO: pass a filelike object instead?) - - Params: - pack - The pack to simplify. - folder - The parent folder of the pack (either 'mods' or 'graphics') - - Returns: - The full path to a tmpdir containingthe rebuild graphics pack. - """ - if not (folder=='graphics' or folder=='mods'): - return False - pack = os.path.join(paths.get(folder), pack) - tmp = tempfile.mkdtemp() - dir_util.copy_tree(find_vanilla_raws(), tmp) - dir_util.copy_tree(pack, tmp) - return tmp - -def simplify_graphics(): - """Removes unnecessary files from all graphics packs.""" - for pack in read_graphics(): - simplify_graphics_pack() - -def simplify_graphics_pack(pack): - """Removes unnecessary files from one graphics pack.""" - simplify_pack(pack, 'graphics') - remove_vanilla_raws_from_pack(pack, 'graphics') - remove_empty_dirs(pack, 'graphics') - -def simplify_mods(): - """Removes unnecessary files from all mod packs.""" - for pack in read_mods(): - simplify_pack(pack, 'mods') - remove_vanilla_raws_from_pack(pack, 'mods') - remove_empty_dirs(pack, 'mods') - def find_vanilla_raws(version=''): # override for testing: return os.path.join('LNP', 'Baselines', 'df_40_11_win', 'raw') diff --git a/tkgui/mods.py b/tkgui/mods.py index 4070e44..c5edcb4 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -9,6 +9,7 @@ import sys, os, shutil from core import mods +from core import raws from core import paths if sys.version_info[0] == 3: # Alternate import names @@ -30,7 +31,7 @@ def create_variables(self): def read_data(self): mods.init_paths(paths.get('lnp')) - available = mods.mod_folders_list + available = mods.read_mods() installed = mods.get_installed_mods_from_log() available = [m for m in available if m not in installed] self.available.set(tuple(available)) @@ -74,7 +75,7 @@ def create_controls(self): f = controls.create_control_group(self, None, True) controls.create_trigger_button( f, 'Simplify Mods', 'Simplify Mods', - mods.simplify_mod_and_df_folders_to_defined_format).grid( + mods.simplify_mods()).grid( row=0, column=0, sticky="nsew") controls.create_trigger_button( f, 'Install Mods', 'Copy "installed" mods to DF folder. ' From a4a364b6dbbdfa115c52ff171ecd5ae4bac4bb0f Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 12:13:50 +1100 Subject: [PATCH 05/29] Fix accidental of reversion of printmode install feature --HG-- branch : mods-tab-and-graphics-loading --- core/graphics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/graphics.py b/core/graphics.py index f39e01f..afec1c5 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -203,7 +203,7 @@ def patch_inits(gfx_dir): 'TREE_TRUNK_INTERIOR', 'TREE_TRUNK_INTERIOR_DEAD'] init_fields = [ 'FONT', 'FULLFONT', 'GRAPHICS', 'GRAPHICS_FONT', - 'GRAPHICS_FULLFONT', 'TRUETYPE'] + 'GRAPHICS_FULLFONT', 'TRUETYPE', 'PRINT_MODE'] init_fields = [f for f in init_fields if lnp.settings.version_has_option(f)] d_init_fields = [ f for f in d_init_fields if lnp.settings.version_has_option(f)] From ab3b437b715dc53e99344af889d1d141e31b1307 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 12 Nov 2014 21:45:53 +1100 Subject: [PATCH 06/29] Fixes, vanilla-DF download functionality. --HG-- branch : mods-tab-and-graphics-loading --- core/graphics.py | 4 ++-- core/mods.py | 6 +++--- core/raws.py | 30 ++++++++++++++++++------------ core/update.py | 37 +++++++++++++++++++++++++++++++++++-- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/core/graphics.py b/core/graphics.py index afec1c5..e4a579f 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -59,8 +59,8 @@ def install_graphics(pack): None if required files are missing (raw/graphics, data/init) """ gfx_dir = tempfile.mkdtemp() - dir_util.copy_tree(raws.find_vanilla_raws(), tmp) - dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), tmp) + dir_util.copy_tree(raws.find_vanilla_raws(), gfx_dir) + dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), gfx_dir) if (os.path.isdir(gfx_dir) and os.path.isdir(os.path.join(gfx_dir, 'raw', 'graphics')) and diff --git a/core/mods.py b/core/mods.py index bd0abb5..3282c59 100644 --- a/core/mods.py +++ b/core/mods.py @@ -16,7 +16,7 @@ def read_mods(): def simplify_mods(): """Removes unnecessary files from all mods.""" for pack in read_mods(): - simplify_pack(pack, 'mods') + simplify_pack(pack) def simplify_pack(pack): """Removes unnecessary files from one mod.""" @@ -189,7 +189,7 @@ def clear_temp(): def init_paths(lnpdir): global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder - raws.simplify_mods() + simplify_mods() installed_raw_folder = os.path.join(paths.get('df'), 'raw') mods_folder = os.path.join(lnpdir, 'Mods') vanilla_raw_folder = raws.find_vanilla_raws(version=None) @@ -232,7 +232,7 @@ def get_installed_mods_from_log(): '''Return best possible mod load order to recreate installed with available''' logged = read_installation_log(os.path.join(installed_raw_folder, 'installed_mods.txt')) # return list overlap - like set intersection, but ordered - return [mod for mod in logged if mod in mods_folders_list] + return [mod for mod in logged if mod in mod_folders_list] def read_installation_log(file): '''Read an 'installed_mods.txt' and return it's full contents.''' diff --git a/core/raws.py b/core/raws.py index d72cd06..e265df4 100644 --- a/core/raws.py +++ b/core/raws.py @@ -6,11 +6,11 @@ import os, shutil, filecmp, sys, glob, tempfile import distutils.dir_util as dir_util -from . import paths +from . import paths, update paths.register('baselines', 'LNP', 'baselines') -def find_vanilla_raws(version=''): +def find_vanilla_raws(version=None): # override for testing: return os.path.join('LNP', 'Baselines', 'df_40_11_win', 'raw') """Finds vanilla raws for the requested version. @@ -19,27 +19,33 @@ def find_vanilla_raws(version=''): Params: version String indicating version in format 'df_40_15' - None returns the latest available version. + None returns the latest available raws. Returns: - The path to a vanilla 'raw' folder - None if path could not be found or made + The path to the requested vanilla 'raw' folder + If requested version unavailable, path to latest version + None if no version was available. """ - baselines_path = os.path.join(paths.get('baselines')) - available = [os.path.relpath(item, baselines_path) for item in - glob.glob(os.path.join(baselines_path, 'df_??_?*'))] + # TODO: handle other DF versions; esp. small pack and non-SDL releases + # and non-zip files? Folder size minimisation? + available = [os.path.basename(item) for item in glob.glob( + os.path.join(paths.get('baselines'), 'df_??_?*'))] if version == None: version = available[-1][0:8] - version_dir = os.path.join(baselines_path, version + '_win') + version_dir = os.path.join(paths.get('baselines'), version) + version_dir_win = os.path.join(paths.get('baselines'), version + '_win') if os.path.isdir(version_dir): return os.path.join(version_dir, 'raw') + elif os.path.isdir(version_dir_win): + return os.path.join(version_dir, 'raw') elif os.path.isfile(version_dir + '.zip'): - file = zipfile.ZipFile(version_dir + '.zip') + file = zipfile.ZipFile(version_dir + '_win.zip') file.extractall(version_dir) return os.path.join(version_dir, 'raw') - # TODO: attempt to download requested file from Bay12 Games - return None + else: + update.download_df_version_to_baselines(version) + return None def simplify_pack(pack, folder): """Removes unnecessary files from LNP//. diff --git a/core/update.py b/core/update.py index d20a43a..c7c5c00 100644 --- a/core/update.py +++ b/core/update.py @@ -3,7 +3,7 @@ """Update handling.""" from __future__ import print_function, unicode_literals, absolute_import -import sys, re, time +import sys, re, time, fnmatch from threading import Thread try: # Python 2 @@ -15,7 +15,7 @@ from urllib.error import URLError from .lnp import lnp -from . import launcher +from . import launcher, paths def updates_configured(): """Returns True if update checking have been configured.""" @@ -63,4 +63,37 @@ def start_update(): """Launches a webbrowser to the specified update URL.""" launcher.open_url(lnp.config.get_string('updates/downloadURL')) +def download_df_version_to_baselines(version='invalid_string'): + """Download the specified version of DF from Bay12 Games + to serve as a baseline, in LNP/Baselines/ + Params: + version + The requested version of DF + + Returns: + True if the download was started (in a thread, for availability later) + False if the download did not start + None if the version string was invalid + """ + # May not actually work! I'm making this up as I go... + pattern = 'df_[234][0123456789]_[0123][0123456789]' + if not fnmatch.fnmatch(version, pattern): + return None + # TODO: actually get appropriate full version + filename = 'df_40_07_win.zip' + + t = Thread(target=download_df_zip_from_bay12(filename)) + t.daemon = True + t.start() # now I have two problems + +def download_df_zip_from_bay12(filename): + """Downloads a zipped version of DF from Bay12 Games. + 'filename' is yhe full name of the file to retrieve, + eg 'df_40_07_win.zip' """ + url = 'http://www.bay12games.com/dwarves/' + filename + req = Request(url, headers={'User-Agent':'PyLNP'}) + archive = urlopen(req, timeout=3).read() + with open(os.path.join(paths.get('baselines'), filename), 'wb') as f: + f.write(archive) + f.close() From 3b952a6574ee35f1c4de33932b934668e8350e7e Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Thu, 13 Nov 2014 13:30:53 +1100 Subject: [PATCH 07/29] Finds and uses correct version of vanilla raws --HG-- branch : mods-tab-and-graphics-loading --- core/mods.py | 2 +- core/raws.py | 22 ++++++++++++++-------- core/update.py | 8 +++++--- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/core/mods.py b/core/mods.py index 3282c59..9fb1d45 100644 --- a/core/mods.py +++ b/core/mods.py @@ -185,7 +185,7 @@ def clear_temp(): with open(os.path.join(mods_folder, 'temp', 'raw', 'installed_mods.txt'), 'w') as log: log.write('# List of mods merged by PyLNP mod loader\n' + - os.path.dirname(vanilla_raw_folder)[:-4] + '\n') + os.path.dirname(vanilla_raw_folder)[14:] + '\n') def init_paths(lnpdir): global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder diff --git a/core/raws.py b/core/raws.py index e265df4..330c4b7 100644 --- a/core/raws.py +++ b/core/raws.py @@ -3,16 +3,15 @@ """Advanced raw and data folder management, for mods or graphics packs.""" from __future__ import print_function, unicode_literals, absolute_import -import os, shutil, filecmp, sys, glob, tempfile +import os, shutil, filecmp, sys, glob, tempfile, re import distutils.dir_util as dir_util -from . import paths, update +from . import paths +from . import update paths.register('baselines', 'LNP', 'baselines') def find_vanilla_raws(version=None): - # override for testing: - return os.path.join('LNP', 'Baselines', 'df_40_11_win', 'raw') """Finds vanilla raws for the requested version. If required, unzip a DF release to create the folder in LNP/Baselines/. @@ -23,6 +22,7 @@ def find_vanilla_raws(version=None): Returns: The path to the requested vanilla 'raw' folder + eg: 'LNP/Baselines/df_40_15/raw' If requested version unavailable, path to latest version None if no version was available. """ @@ -31,15 +31,21 @@ def find_vanilla_raws(version=None): available = [os.path.basename(item) for item in glob.glob( os.path.join(paths.get('baselines'), 'df_??_?*'))] if version == None: - version = available[-1][0:8] + with open(os.path.join(paths.get('df'), 'release notes.txt')) as f: + ver = re.findall(' \d.\d\d.\d\d \\(', f.read())[0] + version = 'df_' + ver[3:4] + '_' + ver[6:7] + if version not in available: + # the download doesn't happen? + update.download_df_version_to_baselines(version) + version = available[-1][0:8] + version_dir = os.path.join(paths.get('baselines'), version) - version_dir_win = os.path.join(paths.get('baselines'), version + '_win') if os.path.isdir(version_dir): return os.path.join(version_dir, 'raw') elif os.path.isdir(version_dir_win): - return os.path.join(version_dir, 'raw') - elif os.path.isfile(version_dir + '.zip'): + return os.path.join(version_dir_win, 'raw') + elif os.path.isfile(version_dir + '_win.zip'): file = zipfile.ZipFile(version_dir + '_win.zip') file.extractall(version_dir) return os.path.join(version_dir, 'raw') diff --git a/core/update.py b/core/update.py index c7c5c00..aea5a12 100644 --- a/core/update.py +++ b/core/update.py @@ -80,12 +80,14 @@ def download_df_version_to_baselines(version='invalid_string'): pattern = 'df_[234][0123456789]_[0123][0123456789]' if not fnmatch.fnmatch(version, pattern): return None - # TODO: actually get appropriate full version - filename = 'df_40_07_win.zip' + filename = version + '_win.zip' + download_df_zip_from_bay12(filename) + return True t = Thread(target=download_df_zip_from_bay12(filename)) t.daemon = True - t.start() # now I have two problems + t.start() + return True def download_df_zip_from_bay12(filename): """Downloads a zipped version of DF from Bay12 Games. From 19a88afd9968554622825b00b78df4895fd78cc4 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Thu, 13 Nov 2014 15:48:33 +1100 Subject: [PATCH 08/29] Hide mods tab if no mods are present --HG-- branch : mods-tab-and-graphics-loading --- tkgui/tkgui.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tkgui/tkgui.py b/tkgui/tkgui.py index d507ef2..fc32819 100644 --- a/tkgui/tkgui.py +++ b/tkgui/tkgui.py @@ -20,7 +20,7 @@ from .mods import ModsTab from core.lnp import lnp -from core import df, launcher, paths, update +from core import df, launcher, paths, update, mods if sys.version_info[0] == 3: # Alternate import names # pylint:disable=import-error @@ -145,7 +145,8 @@ def __init__(self): self.create_tab(AdvancedTab, 'Advanced') if 'dfhack' in lnp.df_info.variations: self.create_tab(DFHackTab, 'DFHack') - self.create_tab(ModsTab, 'Mods') + if mods.read_mods(): + self.create_tab(ModsTab, 'Mods') n.enable_traversal() n.pack(fill=BOTH, expand=Y, padx=2, pady=3) From ad28194aae75672a9ac91c42534f018b6ffcdd16 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Fri, 14 Nov 2014 12:59:29 +1100 Subject: [PATCH 09/29] Misc improvements; fix baseline download Vanilla files in 'LNP/Baselines' are now reduced to the graphics pack format; with improved unzipping/deleting logic a version is ~4MB instead of ~30MB. The function to download additional versions as baselines is now working, though it seems to block despite the thread; having no baseline available still causes an uncaught error. --HG-- branch : mods-tab-and-graphics-loading --- core/raws.py | 75 +++++++++++++++++++++----------------------------- core/update.py | 15 ++++------ 2 files changed, 38 insertions(+), 52 deletions(-) diff --git a/core/raws.py b/core/raws.py index 330c4b7..1c16854 100644 --- a/core/raws.py +++ b/core/raws.py @@ -3,13 +3,15 @@ """Advanced raw and data folder management, for mods or graphics packs.""" from __future__ import print_function, unicode_literals, absolute_import -import os, shutil, filecmp, sys, glob, tempfile, re +import os, shutil, filecmp, sys, glob, tempfile, re, zipfile import distutils.dir_util as dir_util from . import paths from . import update +from .lnp import lnp -paths.register('baselines', 'LNP', 'baselines') +# Needs to be fixed by someone competent +paths.register('baselines', 'LNP', 'Baselines') def find_vanilla_raws(version=None): """Finds vanilla raws for the requested version. @@ -28,30 +30,28 @@ def find_vanilla_raws(version=None): """ # TODO: handle other DF versions; esp. small pack and non-SDL releases # and non-zip files? Folder size minimisation? - available = [os.path.basename(item) for item in glob.glob( - os.path.join(paths.get('baselines'), 'df_??_?*'))] + zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) + for item in zipped: + version = os.path.basename(item)[0:8] + file = os.path.join(paths.get('baselines'), version) + if not os.path.isdir(file): + zipfile.ZipFile(item).extractall(file) + simplify_pack(version, 'baselines') + os.remove(item) + + available = [os.path.basename(item) for item in glob.glob(os.path.join( + paths.get('baselines'), 'df_??_?*')) if os.path.isdir(item)] if version == None: - with open(os.path.join(paths.get('df'), 'release notes.txt')) as f: - ver = re.findall(' \d.\d\d.\d\d \\(', f.read())[0] - version = 'df_' + ver[3:4] + '_' + ver[6:7] - if version not in available: - # the download doesn't happen? - update.download_df_version_to_baselines(version) - version = available[-1][0:8] - - version_dir = os.path.join(paths.get('baselines'), version) - - if os.path.isdir(version_dir): - return os.path.join(version_dir, 'raw') - elif os.path.isdir(version_dir_win): - return os.path.join(version_dir_win, 'raw') - elif os.path.isfile(version_dir + '_win.zip'): - file = zipfile.ZipFile(version_dir + '_win.zip') - file.extractall(version_dir) - return os.path.join(version_dir, 'raw') - else: + version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') + if lnp.df_info.source == "init detection": + # WARNING: likley to be much too early in this case + # User should restore 'release notes.txt' + pass + if version not in available: update.download_df_version_to_baselines(version) - return None + version = available[-1] + + return os.path.join(paths.get('baselines'), version, 'raw') def simplify_pack(pack, folder): """Removes unnecessary files from LNP//. @@ -71,7 +71,8 @@ def simplify_pack(pack, folder): False if an exception occurred None if folder is empty """ - if not (folder=='graphics' or folder=='mods'): + valid_dirs = ('graphics', 'mods', 'baselines') + if not folder in valid_dirs: return False pack = os.path.join(paths.get(folder), pack) files_before = sum(len(f) for (_, _, f) in os.walk(pack)) @@ -96,30 +97,18 @@ def simplify_pack(pack, folder): os.path.join(tmp, 'raw', 'objects'), os.path.join(pack, 'raw', 'objects')) - if folder=='mods'and os.path.exists(os.path.join(tmp, 'manifest.json')): - shutil.copyfile( - os.path.join(tmp, 'manifest.json'), - os.path.join(pack, 'manifest.json')) - if folder=='mods'and os.path.exists(os.path.join(tmp, 'readme.txt')): - shutil.copyfile( - os.path.join(tmp, 'readme.txt'), - os.path.join(pack, 'readme.txt')) - if folder=='mods'and os.path.exists(os.path.join(tmp, 'installed_mods.txt')): - shutil.copyfile( - os.path.join(tmp, 'raw', 'installed_mods.txt'), - os.path.join(pack, 'raw', 'installed_mods.txt')) - - if folder=='graphics': + if not folder=='mods': os.makedirs(os.path.join(pack, 'data', 'init')) os.makedirs(os.path.join(pack, 'data', 'art')) dir_util.copy_tree( os.path.join(tmp, 'data', 'art'), os.path.join(pack, 'data', 'art')) for filename in ('colors.txt', 'init.txt', - 'd_init.txt'): #, 'overrides.txt' - shutil.copyfile( - os.path.join(tmp, 'data', 'init', filename), - os.path.join(pack, 'data', 'init', filename)) + 'd_init.txt', 'overrides.txt'): + if os.path.isfile(os.path.join(tmp, 'data', 'init', filename)): + shutil.copyfile( + os.path.join(tmp, 'data', 'init', filename), + os.path.join(pack, 'data', 'init', filename)) except IOError: sys.excepthook(*sys.exc_info()) diff --git a/core/update.py b/core/update.py index aea5a12..06410c6 100644 --- a/core/update.py +++ b/core/update.py @@ -3,7 +3,7 @@ """Update handling.""" from __future__ import print_function, unicode_literals, absolute_import -import sys, re, time, fnmatch +import sys, re, time, os from threading import Thread try: # Python 2 @@ -76,19 +76,16 @@ def download_df_version_to_baselines(version='invalid_string'): False if the download did not start None if the version string was invalid """ - # May not actually work! I'm making this up as I go... pattern = 'df_[234][0123456789]_[0123][0123456789]' - if not fnmatch.fnmatch(version, pattern): + if not re.match('df_\d\d_\d\d', version): return None filename = version + '_win.zip' - download_df_zip_from_bay12(filename) - return True - + # thread seems to block return, which nullifies time saving... t = Thread(target=download_df_zip_from_bay12(filename)) t.daemon = True t.start() - return True - + return True + def download_df_zip_from_bay12(filename): """Downloads a zipped version of DF from Bay12 Games. 'filename' is yhe full name of the file to retrieve, @@ -96,6 +93,6 @@ def download_df_zip_from_bay12(filename): url = 'http://www.bay12games.com/dwarves/' + filename req = Request(url, headers={'User-Agent':'PyLNP'}) archive = urlopen(req, timeout=3).read() - with open(os.path.join(paths.get('baselines'), filename), 'wb') as f: + with open(os.path.join(paths.get('lnp'), 'Baselines', filename), 'wb') as f: f.write(archive) f.close() From 4f5e0bb41d1ac6f589bbdb0e630c14e2e3bc26ac Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Fri, 14 Nov 2014 13:20:43 +1100 Subject: [PATCH 10/29] Fixed path registration, fixed simplify_mods() on startup? The simplify_mods() function can take a few seconds, as it's doing ~5-10MB of disk access per mod. I *think* it's not running at startup now, but that's a quick fix and needs actual testing. --HG-- branch : mods-tab-and-graphics-loading --- core/mods.py | 5 ++--- core/raws.py | 3 +-- core/update.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/mods.py b/core/mods.py index 9fb1d45..f568016 100644 --- a/core/mods.py +++ b/core/mods.py @@ -4,7 +4,7 @@ from . import paths from . import raws -paths.register('mods', 'LNP', 'mods') +paths.register('mods', paths.get('lnp'), 'mods') def read_mods(): """Returns a list of mod packs""" @@ -189,10 +189,9 @@ def clear_temp(): def init_paths(lnpdir): global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder - simplify_mods() installed_raw_folder = os.path.join(paths.get('df'), 'raw') mods_folder = os.path.join(lnpdir, 'Mods') - vanilla_raw_folder = raws.find_vanilla_raws(version=None) + vanilla_raw_folder = raws.find_vanilla_raws() mod_folders_list = read_mods() clear_temp() diff --git a/core/raws.py b/core/raws.py index 1c16854..1157d6f 100644 --- a/core/raws.py +++ b/core/raws.py @@ -10,8 +10,7 @@ from . import update from .lnp import lnp -# Needs to be fixed by someone competent -paths.register('baselines', 'LNP', 'Baselines') +paths.register('baselines', paths.get('lnp'), 'baselines') def find_vanilla_raws(version=None): """Finds vanilla raws for the requested version. diff --git a/core/update.py b/core/update.py index 06410c6..fe48da8 100644 --- a/core/update.py +++ b/core/update.py @@ -93,6 +93,6 @@ def download_df_zip_from_bay12(filename): url = 'http://www.bay12games.com/dwarves/' + filename req = Request(url, headers={'User-Agent':'PyLNP'}) archive = urlopen(req, timeout=3).read() - with open(os.path.join(paths.get('lnp'), 'Baselines', filename), 'wb') as f: + with open(os.path.join(paths.get('baselines'), filename), 'wb') as f: f.write(archive) f.close() From 2c8842ba104b8c2e1cb42439f4cd6951e3866ac3 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Mon, 17 Nov 2014 10:39:26 +1100 Subject: [PATCH 11/29] rename 'raws.py'->'baselines.py' to avoid name conflict with another fork --HG-- branch : mods-tab-and-graphics-loading --- core/{raws.py => baselines.py} | 0 core/graphics.py | 10 +++++----- core/mods.py | 12 ++++++------ core/update.py | 1 - tkgui/mods.py | 1 - 5 files changed, 11 insertions(+), 13 deletions(-) rename core/{raws.py => baselines.py} (100%) diff --git a/core/raws.py b/core/baselines.py similarity index 100% rename from core/raws.py rename to core/baselines.py diff --git a/core/graphics.py b/core/graphics.py index e4a579f..b61b99c 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -7,7 +7,7 @@ import distutils.dir_util as dir_util from .launcher import open_folder from .lnp import lnp -from . import colors, df, paths, raws +from . import colors, df, paths, baselines def open_graphics(): """Opens the graphics pack folder.""" @@ -59,7 +59,7 @@ def install_graphics(pack): None if required files are missing (raw/graphics, data/init) """ gfx_dir = tempfile.mkdtemp() - dir_util.copy_tree(raws.find_vanilla_raws(), gfx_dir) + dir_util.copy_tree(baselines.find_vanilla_raws(), gfx_dir) dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), gfx_dir) if (os.path.isdir(gfx_dir) and @@ -223,9 +223,9 @@ def simplify_graphics(): def simplify_pack(pack): """Removes unnecessary files from one graphics pack.""" - raws.simplify_pack(pack, 'graphics') - raws.remove_vanilla_raws_from_pack(pack, 'graphics') - raws.remove_empty_dirs(pack, 'graphics') + baselines.simplify_pack(pack, 'graphics') + baselines.remove_vanilla_raws_from_pack(pack, 'graphics') + baselines.remove_empty_dirs(pack, 'graphics') def savegames_to_update(): """Returns a list of savegames that will be updated.""" diff --git a/core/mods.py b/core/mods.py index f568016..e9b07f8 100644 --- a/core/mods.py +++ b/core/mods.py @@ -2,7 +2,7 @@ import difflib, sys, time from . import paths -from . import raws +from . import baselines paths.register('mods', paths.get('lnp'), 'mods') @@ -20,9 +20,9 @@ def simplify_mods(): def simplify_pack(pack): """Removes unnecessary files from one mod.""" - raws.simplify_pack(pack, 'mods') - raws.remove_vanilla_raws_from_pack(pack, 'mods') - raws.remove_empty_dirs(pack, 'mods') + baselines.simplify_pack(pack, 'mods') + baselines.remove_vanilla_raws_from_pack(pack, 'mods') + baselines.remove_empty_dirs(pack, 'mods') def do_merge_seq (mod_text, vanilla_text, gen_text): """Merges sequences of lines. Returns empty string if a line changed by the mod @@ -191,7 +191,7 @@ def init_paths(lnpdir): global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder installed_raw_folder = os.path.join(paths.get('df'), 'raw') mods_folder = os.path.join(lnpdir, 'Mods') - vanilla_raw_folder = raws.find_vanilla_raws() + vanilla_raw_folder = baselines.find_vanilla_raws() mod_folders_list = read_mods() clear_temp() @@ -247,7 +247,7 @@ def read_installation_log(file): mods_list.append(line.strip()) return mods_list -mods_folder = os.path.join('LNP', 'Mods') +mods_folder = paths.get('mods') vanilla_folder = '' vanilla_raw_folder = '' mixed_raw_folder = '' diff --git a/core/update.py b/core/update.py index fe48da8..6ff36c2 100644 --- a/core/update.py +++ b/core/update.py @@ -76,7 +76,6 @@ def download_df_version_to_baselines(version='invalid_string'): False if the download did not start None if the version string was invalid """ - pattern = 'df_[234][0123456789]_[0123][0123456789]' if not re.match('df_\d\d_\d\d', version): return None filename = version + '_win.zip' diff --git a/tkgui/mods.py b/tkgui/mods.py index c5edcb4..8dee098 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -9,7 +9,6 @@ import sys, os, shutil from core import mods -from core import raws from core import paths if sys.version_info[0] == 3: # Alternate import names From 8f49f803dcbd83ff2f820d97b7cee6f5e1ffa277 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Mon, 17 Nov 2014 11:28:09 +1100 Subject: [PATCH 12/29] Moved merge dir to Baselines, cleanup of paths and variables. Extensive cleanup in core/mods.py; mostly around assignment of variables when paths.get() works instead. Fewer global variables. In several places, call a function instead of refering to saved data. Preparing for logic that can merge graphics as well as mods. --HG-- branch : mods-tab-and-graphics-loading --- core/mods.py | 84 +++++++++++++++++++++++---------------------------- tkgui/mods.py | 7 ++--- 2 files changed, 40 insertions(+), 51 deletions(-) diff --git a/core/mods.py b/core/mods.py index e9b07f8..651d2ec 100644 --- a/core/mods.py +++ b/core/mods.py @@ -11,7 +11,7 @@ def read_mods(): # should go in tkgui/mods.py later return [os.path.basename(o) for o in glob.glob(os.path.join(paths.get('mods'), '*')) - if os.path.isdir(o) and not os.path.basename(o)=='temp'] + if os.path.isdir(o)] def simplify_mods(): """Removes unnecessary files from all mods.""" @@ -147,17 +147,17 @@ def merge_a_mod(mod): 1: Potential compatibility issues, no merge problems 2: Non-fatal error, overlapping lines or non-existent mod etc 3: Fatal error, respond by rebuilding to previous mod''' - mod_raw_folder = os.path.join(mods_folder, mod, 'raw') + mod_raw_folder = os.path.join(paths.get('mods'), mod, 'raw') if not os.path.isdir(mod_raw_folder): return 2 status = merge_raw_folders(mod_raw_folder, vanilla_raw_folder) if status < 2: - with open(os.path.join(mods_folder, 'temp', 'raw', 'installed_mods.txt'), 'a') as log: + with open(os.path.join(mixed_raw_folder, 'installed_raws.txt'), 'a') as log: log.write(mod + '\n') return status def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): - '''Behind the wrapper, to allow tricks by overriding the global variables''' + '''Merge the specified folders, output going in LNP/Baselines/temp/raw''' status = 0 for file_tuple in os.walk(mod_raw_folder): for item in file_tuple[2]: @@ -177,64 +177,60 @@ def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): return status def clear_temp(): - global mixed_raw_folder, vanilla_raw_folder - mixed_raw_folder = os.path.join(mods_folder, 'temp', 'raw') - if os.path.exists(os.path.join(mods_folder, 'temp')): - shutil.rmtree(os.path.join(mods_folder, 'temp')) + if os.path.exists(os.path.join(paths.get('baselines'), 'temp')): + shutil.rmtree(os.path.join(paths.get('baselines'), 'temp')) shutil.copytree(vanilla_raw_folder, mixed_raw_folder) - with open(os.path.join(mods_folder, 'temp', 'raw', 'installed_mods.txt'), - 'w') as log: - log.write('# List of mods merged by PyLNP mod loader\n' + - os.path.dirname(vanilla_raw_folder)[14:] + '\n') - -def init_paths(lnpdir): - global mods_folder, mods_folders_list, vanilla_folder, vanilla_raw_folder, installed_raw_folder - installed_raw_folder = os.path.join(paths.get('df'), 'raw') - mods_folder = os.path.join(lnpdir, 'Mods') + with open(os.path.join(mixed_raw_folder, 'installed_raws.txt'), 'w') as log: + log.write('# List of raws merged by PyLNP:\n' + + os.path.dirname(vanilla_raw_folder)[-8:] + '\n') + +def init_paths(): + global vanilla_raw_folder, mixed_raw_folder vanilla_raw_folder = baselines.find_vanilla_raws() - mod_folders_list = read_mods() + mixed_raw_folder = os.path.join(paths.get('baselines'), 'temp', 'raw') clear_temp() def make_mod_from_installed_raws(name): '''Capture whatever unavailable mods a user currently has installed as a mod called $name. - * If `installed_mods.txt` is not present, compare to vanilla + * If `installed_raws.txt` is not present, compare to vanilla * Otherwise, rebuild as much as possible then compare installed and rebuilt * Harder than I first thought... but not impossible''' - mod_load_order = get_installed_mods_from_log() - if mod_load_order: + if get_installed_mods_from_log(): clear_temp() - for mod in mod_load_order: + for mod in get_installed_mods_from_log(): merge_a_mod(mod) - best_effort_reconstruction = os.path.join(mods_folder, 'temp2', 'raw') - shutil.copytree(os.path.join(mods_folder, 'temp', 'raw'), - os.path.join(mods_folder, 'temp2', 'raw')) + reconstruction = os.path.join(paths.get('baselines'), 'temp2', 'raw') + shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), + os.path.join(paths.get('baselines'), 'temp2', 'raw')) else: - best_effort_reconstruction = vanilla_raw_folder + reconstruction = baselines.find_vanilla_raws() clear_temp() - merge_raw_folders(best_effort_reconstruction, installed_raw_folder) - simplify_mod_folder('temp') - if os.path.isdir(os.path.join(mods_folder, 'temp2')): - shutil.rmtree(os.path.join(mods_folder, 'temp2')) - - if os.path.isdir(os.path.join(mods_folder, 'temp')): - if not name or os.path.isdir(os.path.join(mods_folder, name)): - name = 'Extracted Mod at '+str(int(time.time())) - shutil.copytree(os.path.join(mods_folder, 'temp'), os.path.join(mods_folder, name)) - return_val = 'User unique mods extracted as "'+str(name)+'"' - return return_val + merge_raw_folders(reconstruction, os.path.join(paths.get('df'), 'raw')) + + baselines.simplify_pack('temp', 'baselines') + baselines.remove_vanilla_raws_from_pack('temp', 'baselines') + baselines.remove_empty_dirs('temp', 'baselines') + + if os.path.isdir(os.path.join(paths.get('baselines'), 'temp2')): + shutil.rmtree(os.path.join(paths.get('baselines'), 'temp2')) + + if os.path.isdir(os.path.join(paths.get('baselines'), 'temp')): + shutil.copytree(os.path.join(paths.get('baselines'), 'temp'), + os.path.join(paths.get('mods'), name)) else: - return 'No unique user mods found.' + # No unique mods, or graphics packs, were installed + pass def get_installed_mods_from_log(): '''Return best possible mod load order to recreate installed with available''' - logged = read_installation_log(os.path.join(installed_raw_folder, 'installed_mods.txt')) + logged = read_installation_log(os.path.join(paths.get('df'), 'raw', 'installed_raws.txt')) # return list overlap - like set intersection, but ordered - return [mod for mod in logged if mod in mod_folders_list] + return [mod for mod in logged if mod in read_mods()] def read_installation_log(file): - '''Read an 'installed_mods.txt' and return it's full contents.''' + '''Read an 'installed_raws.txt' and return it's full contents.''' try: with open(file) as f: file_contents = list(f.readlines()) @@ -246,9 +242,3 @@ def read_installation_log(file): continue mods_list.append(line.strip()) return mods_list - -mods_folder = paths.get('mods') -vanilla_folder = '' -vanilla_raw_folder = '' -mixed_raw_folder = '' -mod_folders_list = read_mods() diff --git a/tkgui/mods.py b/tkgui/mods.py index 8dee098..159e425 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -29,7 +29,7 @@ def create_variables(self): self.available = Variable() def read_data(self): - mods.init_paths(paths.get('lnp')) + mods.init_paths() available = mods.read_mods() installed = mods.get_installed_mods_from_log() available = [m for m in available if m not in installed] @@ -173,8 +173,7 @@ def perform_merge(self): return def install_mods(self): - # Assumption: Everything in Mods/temp goes into the DF folder. Adjust - # if needed. + """Replaces /raw with the contents LNP/Baselines/temp/raw""" shutil.rmtree(os.path.join(paths.get('df'), 'raw')) - shutil.copytree(os.path.join(paths.get('lnp'), 'Mods', 'temp', 'raw'), + shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), os.path.join(paths.get('df'), 'raw')) From b929b4a16df925f7ea09cdce4aa9a7ed101b4e96 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Tue, 18 Nov 2014 13:28:58 +1100 Subject: [PATCH 13/29] Look for tilesets in /data/art/; LNP/Tilesets no longer used. --- core/graphics.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/core/graphics.py b/core/graphics.py index 171f90c..fd7ad0c 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -253,14 +253,16 @@ def update_savegames(): def open_tilesets(): """Opens the tilesets folder.""" - open_folder(paths.get('tilesets')) + open_folder(os.path.join(paths.get('data'), 'art')) def read_tilesets(): """Returns a list of tileset files.""" - files = glob.glob(os.path.join(paths.get('tilesets'), '*.bmp')) + files = glob.glob(os.path.join(paths.get('data'), 'art', '*.bmp')) if 'legacy' not in lnp.df_info.variations: - files += glob.glob(os.path.join(paths.get('tilesets'), '*.png')) - return tuple([os.path.basename(o) for o in files]) + files += glob.glob(os.path.join(paths.get('data'), 'art', '*.png')) + return tuple([os.path.basename(o) for o in files if not ( + o.endswith('mouse.png') or o.endswith('mouse.bmp') + or o.endswith('shadows.png'))]) def current_tilesets(): """Returns the current tilesets as a tuple (FONT, GRAPHICS_FONT).""" @@ -274,17 +276,11 @@ def install_tilesets(font, graphicsfont): To skip either option, use None as the parameter. """ if font is not None and os.path.isfile( - os.path.join(paths.get('tilesets'), font)): - shutil.copyfile( - os.path.join(paths.get('tilesets'), font), - os.path.join(paths.get('data'), 'art', font)) + os.path.join(paths.get('data'), 'art', font)): df.set_option('FONT', font) df.set_option('FULLFONT', font) if (lnp.settings.version_has_option('GRAPHICS_FONT') and graphicsfont is not None and os.path.isfile( - os.path.join(paths.get('tilesets'), graphicsfont))): - shutil.copyfile( - os.path.join(paths.get('tilesets'), graphicsfont), - os.path.join(paths.get('data'), 'art', graphicsfont)) + os.path.join(paths.get('data'), 'art', graphicsfont))): df.set_option('GRAPHICS_FONT', graphicsfont) df.set_option('GRAPHICS_FULLFONT', graphicsfont) From 2e840057323eca7c628d94f4e47320fea91891ae Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Tue, 18 Nov 2014 14:33:50 +1100 Subject: [PATCH 14/29] Overlapping merges, fixes for creating mod from installed. Also modify display of merge status, and move path registration somewhere sensible. --- core/baselines.py | 9 +++--- core/lnp.py | 3 +- core/mods.py | 71 +++++++++++++++++++++++++---------------------- tkgui/mods.py | 10 +++++-- 4 files changed, 52 insertions(+), 41 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 1157d6f..0817b04 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -10,8 +10,6 @@ from . import update from .lnp import lnp -paths.register('baselines', paths.get('lnp'), 'baselines') - def find_vanilla_raws(version=None): """Finds vanilla raws for the requested version. If required, unzip a DF release to create the folder in LNP/Baselines/. @@ -99,9 +97,10 @@ def simplify_pack(pack, folder): if not folder=='mods': os.makedirs(os.path.join(pack, 'data', 'init')) os.makedirs(os.path.join(pack, 'data', 'art')) - dir_util.copy_tree( - os.path.join(tmp, 'data', 'art'), - os.path.join(pack, 'data', 'art')) + if os.path.exists(os.path.join(tmp, 'data', 'art')): + dir_util.copy_tree( + os.path.join(tmp, 'data', 'art'), + os.path.join(pack, 'data', 'art')) for filename in ('colors.txt', 'init.txt', 'd_init.txt', 'overrides.txt'): if os.path.isfile(os.path.join(tmp, 'data', 'init', filename)): diff --git a/core/lnp.py b/core/lnp.py index 8d4756b..3b6d084 100644 --- a/core/lnp.py +++ b/core/lnp.py @@ -52,7 +52,8 @@ def __init__(self): 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') self.df_info = None self.folders = [] diff --git a/core/mods.py b/core/mods.py index 651d2ec..6cd5742 100644 --- a/core/mods.py +++ b/core/mods.py @@ -4,8 +4,6 @@ from . import paths from . import baselines -paths.register('mods', paths.get('lnp'), 'mods') - def read_mods(): """Returns a list of mod packs""" # should go in tkgui/mods.py later @@ -37,14 +35,14 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): The lines of the previously merged file. Returns: - Merged lines if no changes in mod and gen files overlap. - Empty string otherwise. + tuple(status, lines); status is 0/'ok' or 2/'overlap merged' """ + status = 0 # special cases - where merging is not required because two are equal if vanilla_text == gen_text: - return mod_text + return 0, mod_text if vanilla_text == mod_text: - return gen_text + return 0, gen_text # returns list of 5-tuples describing how to turn vanilla into mod or gen lines # each specifies an operation, and start+end line nums for each change @@ -96,10 +94,20 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): van_gen_ops.pop(0) # if the changes would overlap, we can't handle that yet else: - # we'll revisit this later, to allow overlapping merges - # (important for graphics pack inclusion) - # probably have to return (status, lines) tuple. - return '' + # this is the rare over-write merge. + # changes status, use caution + status = 2 + # append the shorter block to new genned lines + if mod_i2 < gen_i2: + output_file_temp += mod_text[cur_v:mod_i2] + cur_v = mod_i2 + van_mod_ops.pop(0) + else: + output_file_temp += mod_text[cur_v:gen_i2] + cur_v = gen_i2 + van_gen_ops.pop(0) + if mod_i2 == gen_i2 : + van_mod_ops.pop(0) # clean up trailing opcodes, to avoid dropping the end of the file if van_mod_ops: mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] @@ -107,7 +115,7 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): if van_gen_ops: gen_tag, gen_i1, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] output_file_temp += gen_text[gen_j1:gen_j2] - return output_file_temp + return status, output_file_temp def get_lines_from_file(filename): """Get lines from a file, managing encoding. @@ -131,14 +139,11 @@ def do_merge_files(mod_file_name, van_file_name, gen_file_name): if os.path.isfile(gen_file_name): gen_lines = get_lines_from_file(gen_file_name) - gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) - if gen_lines: - gen_file = open(gen_file_name,"w") - for line in gen_lines: - gen_file.write(line) - return True - else: - return False + status, gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) + gen_file = open(gen_file_name,"w") + for line in gen_lines: + gen_file.write(line) + return status def merge_a_mod(mod): '''Merges the specified mod, and returns status (0|1|2|3) like an exit code. @@ -151,7 +156,7 @@ def merge_a_mod(mod): if not os.path.isdir(mod_raw_folder): return 2 status = merge_raw_folders(mod_raw_folder, vanilla_raw_folder) - if status < 2: + if status < 3: with open(os.path.join(mixed_raw_folder, 'installed_raws.txt'), 'a') as log: log.write(mod + '\n') return status @@ -163,17 +168,17 @@ def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): for item in file_tuple[2]: file = os.path.join(file_tuple[0], item) file = os.path.relpath(file, mod_raw_folder) - if os.path.isfile(os.path.join(vanilla_raw_folder, file)): - if not do_merge_files(os.path.join(mod_raw_folder, file), - os.path.join(vanilla_raw_folder, file), - os.path.join(mixed_raw_folder, file)): - return 3 - elif os.path.isfile(os.path.join(mixed_raw_folder, file)): - return 3 + if not file.endswith('.txt'): + continue + if (os.path.isfile(os.path.join(vanilla_raw_folder, file)) or + os.path.isfile(os.path.join(mixed_raw_folder, file))): + status = max(do_merge_files(os.path.join(mod_raw_folder, file), + os.path.join(vanilla_raw_folder, file), + os.path.join(mixed_raw_folder, file)), + status) else: shutil.copy(os.path.join(mod_raw_folder, file), os.path.join(mixed_raw_folder, file)) - status = 1 return status def clear_temp(): @@ -196,13 +201,14 @@ def make_mod_from_installed_raws(name): * If `installed_raws.txt` is not present, compare to vanilla * Otherwise, rebuild as much as possible then compare installed and rebuilt * Harder than I first thought... but not impossible''' + if os.path.isdir(os.path.join(paths.get('mods'), name)): + return False if get_installed_mods_from_log(): clear_temp() for mod in get_installed_mods_from_log(): merge_a_mod(mod) reconstruction = os.path.join(paths.get('baselines'), 'temp2', 'raw') - shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), - os.path.join(paths.get('baselines'), 'temp2', 'raw')) + shutil.copytree(mixed_raw_folder, reconstruction) else: reconstruction = baselines.find_vanilla_raws() @@ -219,9 +225,8 @@ def make_mod_from_installed_raws(name): if os.path.isdir(os.path.join(paths.get('baselines'), 'temp')): shutil.copytree(os.path.join(paths.get('baselines'), 'temp'), os.path.join(paths.get('mods'), name)) - else: - # No unique mods, or graphics packs, were installed - pass + return True + return False def get_installed_mods_from_log(): '''Return best possible mod load order to recreate installed with available''' diff --git a/tkgui/mods.py b/tkgui/mods.py index 159e425..3409dda 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -132,7 +132,13 @@ def move_down(self): def create_from_installed(self): m = simpledialog.askstring("Create Mod", "New mod name:") if m is not None and m != '': - mods.make_mod_from_installed_raws(m) + if mods.make_mod_from_installed_raws(m): + messagebox.showinfo('Mod extracted', + 'Your custom mod was extracted as '+m+'.') + else: + messagebox.showinfo('Error', + 'There is already a mod with that name, ' + 'or only pre-existing mods were found.') self.read_data() def add_to_installed(self): @@ -165,7 +171,7 @@ def perform_merge(self): for i, _ in enumerate(self.installed_list.get(0, END)): self.installed_list.itemconfig(i, bg='white') status = 3 - colors = ['green', 'yellow', 'orange', 'red'] + colors = ['limegreen', 'yellow', 'orange', 'red'] for i, mod in enumerate(self.installed_list.get(0, END)): status = mods.merge_a_mod(mod) self.installed_list.itemconfig(i, bg=colors[status]) From f06b9d50c7295c015415af03534a0168a2cd92e9 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Tue, 18 Nov 2014 15:29:28 +1100 Subject: [PATCH 15/29] Baseline-download now works properly, fixed mods being simplified on startup, added mod install confirmation message. Those were the biggest problems left. It'll try to download the correct download in a non-blocking thread (and only one thread per version). The mods.simplify_mods() function was being called directly when the button was created; adding a method within the class to call it prevents this. --- core/baselines.py | 1 - core/update.py | 16 ++++++++-------- tkgui/mods.py | 23 ++++++++++++++++++----- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 0817b04..7d70b64 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -26,7 +26,6 @@ def find_vanilla_raws(version=None): None if no version was available. """ # TODO: handle other DF versions; esp. small pack and non-SDL releases - # and non-zip files? Folder size minimisation? zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) for item in zipped: version = os.path.basename(item)[0:8] diff --git a/core/update.py b/core/update.py index 6ff36c2..e60da4d 100644 --- a/core/update.py +++ b/core/update.py @@ -3,8 +3,7 @@ """Update handling.""" from __future__ import print_function, unicode_literals, absolute_import -import sys, re, time, os -from threading import Thread +import sys, re, time, os, threading try: # Python 2 # pylint:disable=import-error @@ -28,7 +27,7 @@ def check_update(): if lnp.userconfig.get_number('updateDays') == -1: return if lnp.userconfig.get_number('nextUpdate') < time.time(): - t = Thread(target=perform_update_check) + t = threading.Thread(target=perform_update_check) t.daemon = True t.start() @@ -79,15 +78,16 @@ def download_df_version_to_baselines(version='invalid_string'): if not re.match('df_\d\d_\d\d', version): return None filename = version + '_win.zip' - # thread seems to block return, which nullifies time saving... - t = Thread(target=download_df_zip_from_bay12(filename)) - t.daemon = True - t.start() + if not 'download_'+version in (t.name for t in threading.enumerate()): + t = threading.Thread(target=download_df_zip_from_bay12, + args=(filename,), name='download_'+version) + t.daemon = True + t.start() return True def download_df_zip_from_bay12(filename): """Downloads a zipped version of DF from Bay12 Games. - 'filename' is yhe full name of the file to retrieve, + 'filename' is the full name of the file to retrieve, eg 'df_40_07_win.zip' """ url = 'http://www.bay12games.com/dwarves/' + filename req = Request(url, headers={'User-Agent':'PyLNP'}) diff --git a/tkgui/mods.py b/tkgui/mods.py index 3409dda..9415c5e 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -74,10 +74,10 @@ def create_controls(self): f = controls.create_control_group(self, None, True) controls.create_trigger_button( f, 'Simplify Mods', 'Simplify Mods', - mods.simplify_mods()).grid( + self.simplify_mods).grid( row=0, column=0, sticky="nsew") controls.create_trigger_button( - f, 'Install Mods', 'Copy "installed" mods to DF folder. ' + f, 'Install Mods', 'Copy merged mods to DF folder. ' 'WARNING: do not combine with graphics. May be unstable.', self.install_mods).grid(row=0, column=1, sticky="nsew") controls.create_trigger_button( @@ -180,6 +180,19 @@ def perform_merge(self): def install_mods(self): """Replaces /raw with the contents LNP/Baselines/temp/raw""" - shutil.rmtree(os.path.join(paths.get('df'), 'raw')) - shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), - os.path.join(paths.get('df'), 'raw')) + if messagebox.askokcancel( + message='Your graphics and raws will be changed.\n\n' + 'The mod merging function is still in beta. This could ' + 'break new worlds, or even cause crashes.\n\n' + '', + title='Are you sure?'): + shutil.rmtree(os.path.join(paths.get('df'), 'raw')) + shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), + os.path.join(paths.get('df'), 'raw')) + messagebox.showinfo( + 'Mods installed', + 'The selected mods were installed.\nGenerate a new world to ' + 'start playing with them!') + + def simplify_mods(self): + mods.simplify_mods() From cf9415d0638557c5b55112b23b30e94abab924ca Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Tue, 18 Nov 2014 16:09:53 +1100 Subject: [PATCH 16/29] Update readme; don't install graphics on saves with a list of installed mods. --- core/graphics.py | 7 ++++--- readme.rst | 26 +++++++++++++++++++++----- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/core/graphics.py b/core/graphics.py index fd7ad0c..e922504 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -229,9 +229,10 @@ def simplify_pack(pack): def savegames_to_update(): """Returns a list of savegames that will be updated.""" - return [ - o for o in glob.glob(os.path.join(paths.get('save'), '*')) - if os.path.isdir(o) and not o.endswith('current')] + saves = [o for o in glob.glob(os.path.join(paths.get('save'), '*')) + if os.path.isdir(o) and not o.endswith('current')] + return [s for s in saves if not os.path.isfile(os.path.join( + s, 'raw', 'installed_raws.txt'))] def update_savegames(): """Update save games with current raws.""" diff --git a/readme.rst b/readme.rst index bba8530..1eb904e 100644 --- a/readme.rst +++ b/readme.rst @@ -278,7 +278,7 @@ PyLNP expects to see the following directory structure:: Extras Graphics Keybinds - Tilesets + Mods Utilities PyLNP itself may be placed anywhere, so long as it is somewhere inside the base folder. It can be placed directly in the base folder, in a subfolder, in a subfolder of a subfolder, etc. The base folder is determined by checking the its own directory; if it cannot find a Dwarf Fortress folder, it will try the parent folder, and continue in this manner until it finds a suitable folder; that folder is considered the base folder. @@ -297,6 +297,12 @@ PyLNP.user ---------- This file, found in the base folder, contains user settings such as window width and height. It should not be distributed if you make a pack. +Baselines +--------- +This folder contains full unmodified raws for various versions of DF, and the settings and images relevant to graphics packs. These are used to rebuild the reduced raws used by graphics packs and mods, and should not be modified or removed - any new graphics or mod install would break. Extra tilesets added to a /data/art/ folder will be available to all graphics packs (useful for TwbT text options). + +Add versions by downloading the windows SDL edition of that version and placing it in the folder (eg "df_40_15_win.zip"). + Colors ------ This folder contains color schemes. As of DF 0.31.04, these are stored as data/init/colors.txt in the Dwarf Fortress folder; in 0.31.03 and below, they are contained in data/init/init.txt. @@ -323,11 +329,11 @@ If this version of PyLNP has not yet been run on the selected DF installation, a Graphics -------- -This folder contains graphics packs, consisting of data and raw folders. +This folder contains graphics packs, consisting of data and raw folders. Any raws identical to vanilla files will be discarded; when installing a graphics pack the remaining files will be copied over a set of vanilla raws and the combination installed. -Tilesets --------- -This folder contains tilesets; individual image files that the user can use for the FONT and GRAPHICS_FONT settings (and their fullscreen counterparts). +Mods +---- +This folder contains mods, consisting of a modified raw folder. They use the same reduced format for raws as graphics packs. Keybinds -------- @@ -384,3 +390,13 @@ If DFHack is detected in the Dwarf Fortress folder, a DFHack tab is added to the This tab includes a list where preconfigured hacks can be turned on or off. See the respective section in the description of PyLNP.json for information on how to configure these hacks. All active hacks are written to a file named ``PyLNP_dfhack_onload.init`` in the Dwarf Fortress folder. This file must be loaded by your standard ``onload.init`` file to take effect. + +Mods +==== +If mods are present in LNP/Mods/, a mods tab is added to the launcher. + +Multiple mods can be merged, in the order shown in the 'installed' pane. Those shown in green merged OK; in yellow with minor issues. Orange signifies an overlapping merge or other serious issue, and red could not be merged. Once you are happy with the combination, you can install them to the DF folder and generate a new world to start playing. + +Note that even an all-green combination might be broken in subtle (or non-subtle) ways. Mods are not currently compatible with graphics! Never update graphics on savegames with installed mods - they will break. + +For mod authors: note that the reduced raw format is equivalent to copying over a vanilla install - missing files are taken to be vanilla. Modifying existing files instead of adding new files decreases the chance of producing conflicting raws without a merge conflict. From 440a0db2ee26dfc56acb44d6b07828cdfde0bbd2 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 19 Nov 2014 09:15:18 +1100 Subject: [PATCH 17/29] Tilesets folder is back, small graphics code fixes The user-facing function of the tilesets folder is almost the same - the contents are added to the graphics customisation tab. On the back end, an installed graphics pack will contain tilesets from vanilla DF (via baselines), the graphics pack, and additional tilesets from the tilesets folder if they exist. This means less duplication and a more relevant list. The tilesets folder can therefore be used for any images in /data/art which are shared across packs - it's particularly good for TwbT images. --- core/graphics.py | 8 ++++---- core/lnp.py | 1 + readme.rst | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/core/graphics.py b/core/graphics.py index e922504..c1d75a6 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -83,6 +83,8 @@ def install_graphics(pack): dir_util.copy_tree( os.path.join(gfx_dir, 'data', 'art'), os.path.join(paths.get('data'), 'art')) + for tiles in glob.glob(os.path.join(paths.get('tilesets'), '*')): + shutil.copy(tiles, os.path.join(paths.get('data'), 'art')) patch_inits(gfx_dir) @@ -111,16 +113,14 @@ def install_graphics(pack): dir_util.remove_tree(gfx_dir) return False else: + df.load_params() if os.path.isdir(gfx_dir): dir_util.remove_tree(gfx_dir) return True - else: - if os.path.isdir(gfx_dir): - dir_util.remove_tree(gfx_dir) - return None if os.path.isdir(gfx_dir): dir_util.remove_tree(gfx_dir) df.load_params() + return None def validate_pack(pack): """Checks for presence of all required files for a pack install.""" diff --git a/core/lnp.py b/core/lnp.py index 3b6d084..8bd44f0 100644 --- a/core/lnp.py +++ b/core/lnp.py @@ -52,6 +52,7 @@ def __init__(self): 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') diff --git a/readme.rst b/readme.rst index 1eb904e..4139863 100644 --- a/readme.rst +++ b/readme.rst @@ -331,6 +331,10 @@ Graphics -------- This folder contains graphics packs, consisting of data and raw folders. Any raws identical to vanilla files will be discarded; when installing a graphics pack the remaining files will be copied over a set of vanilla raws and the combination installed. +Tilesets +-------- +This folder contains tilesets; individual image files that the user can use for the FONT and GRAPHICS_FONT settings (and their fullscreen counterparts). Tilesets can be installed through the graphics customisation tab, as they are added to each graphics pack as the pack is installed. + Mods ---- This folder contains mods, consisting of a modified raw folder. They use the same reduced format for raws as graphics packs. From b8e3f482a8b2d066ac77c96dbf21e751d944ac60 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Thu, 20 Nov 2014 23:03:56 +1100 Subject: [PATCH 18/29] Tiny fixes to re-adding of tilesets folder. --- core/graphics.py | 2 +- readme.rst | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/graphics.py b/core/graphics.py index c1d75a6..5f834bc 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -254,7 +254,7 @@ def update_savegames(): def open_tilesets(): """Opens the tilesets folder.""" - open_folder(os.path.join(paths.get('data'), 'art')) + open_folder(paths.get('tilesets') def read_tilesets(): """Returns a list of tileset files.""" diff --git a/readme.rst b/readme.rst index 4139863..f47d45c 100644 --- a/readme.rst +++ b/readme.rst @@ -279,6 +279,7 @@ PyLNP expects to see the following directory structure:: Graphics Keybinds Mods + Tilesets Utilities PyLNP itself may be placed anywhere, so long as it is somewhere inside the base folder. It can be placed directly in the base folder, in a subfolder, in a subfolder of a subfolder, etc. The base folder is determined by checking the its own directory; if it cannot find a Dwarf Fortress folder, it will try the parent folder, and continue in this manner until it finds a suitable folder; that folder is considered the base folder. From 2283c49294538bb7770ecb1ca88898afa1a60bb6 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Fri, 21 Nov 2014 10:52:30 +1100 Subject: [PATCH 19/29] Heaps of fixes per PyLint analysis. Mostly style issues, some bugs, fair few good habits learned. I'm going to get PyLint... as soon as I work out how to install it on Windows. I *think* this fixes all the issues raised, but there were enough that a couple might have slipped past... --- core/baselines.py | 26 ++++----- core/graphics.py | 8 +-- core/mods.py | 136 +++++++++++++++++++++++----------------------- core/update.py | 2 +- readme.rst | 2 +- tkgui/mods.py | 21 +++---- 6 files changed, 97 insertions(+), 98 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 7d70b64..866510d 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -3,7 +3,7 @@ """Advanced raw and data folder management, for mods or graphics packs.""" from __future__ import print_function, unicode_literals, absolute_import -import os, shutil, filecmp, sys, glob, tempfile, re, zipfile +import os, shutil, filecmp, sys, glob, tempfile, zipfile import distutils.dir_util as dir_util from . import paths @@ -29,14 +29,14 @@ def find_vanilla_raws(version=None): zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) for item in zipped: version = os.path.basename(item)[0:8] - file = os.path.join(paths.get('baselines'), version) - if not os.path.isdir(file): - zipfile.ZipFile(item).extractall(file) + f = os.path.join(paths.get('baselines'), version) + if not os.path.isdir(f): + zipfile.ZipFile(item).extractall(f) simplify_pack(version, 'baselines') os.remove(item) available = [os.path.basename(item) for item in glob.glob(os.path.join( - paths.get('baselines'), 'df_??_?*')) if os.path.isdir(item)] + paths.get('baselines'), 'df_??_?*')) if os.path.isdir(item)] if version == None: version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') if lnp.df_info.source == "init detection": @@ -93,7 +93,7 @@ def simplify_pack(pack, folder): os.path.join(tmp, 'raw', 'objects'), os.path.join(pack, 'raw', 'objects')) - if not folder=='mods': + if not folder == 'mods': os.makedirs(os.path.join(pack, 'data', 'init')) os.makedirs(os.path.join(pack, 'data', 'art')) if os.path.exists(os.path.join(tmp, 'data', 'art')): @@ -129,19 +129,19 @@ def remove_vanilla_raws_from_pack(pack, folder): raw_folder = os.path.join(paths.get(folder), pack, 'raw') vanilla_raw_folder = find_vanilla_raws() for root, dirs, files in os.walk(raw_folder): - for file in files: - file = os.path.join(root, file) + for f in files: + f = os.path.join(root, f) # silently clean up so empty dirs can be removed silently_kill = ('Thumbs.db', 'installed_mods.txt') if any(file.endswith(k) for k in silently_kill): os.remove(file) continue - file = os.path.relpath(file, raw_folder) + f = os.path.relpath(file, raw_folder) # if there's an identical vanilla file, remove the mod file - if os.path.isfile(os.path.join(vanilla_raw_folder, file)): - if filecmp.cmp(os.path.join(vanilla_raw_folder, file), - os.path.join(raw_folder, file)): - os.remove(os.path.join(raw_folder, file)) + if os.path.isfile(os.path.join(vanilla_raw_folder, f)): + if filecmp.cmp(os.path.join(vanilla_raw_folder, f), + os.path.join(raw_folder, f)): + os.remove(os.path.join(raw_folder, f)) def remove_empty_dirs(pack, folder): """Removes empty subdirs in a mods or graphics pack. diff --git a/core/graphics.py b/core/graphics.py index 5f834bc..207107d 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -231,8 +231,8 @@ def savegames_to_update(): """Returns a list of savegames that will be updated.""" saves = [o for o in glob.glob(os.path.join(paths.get('save'), '*')) if os.path.isdir(o) and not o.endswith('current')] - return [s for s in saves if not os.path.isfile(os.path.join( - s, 'raw', 'installed_raws.txt'))] + return [s for s in saves if not + os.path.isfile(os.path.join(s, 'raw', 'installed_raws.txt'))] def update_savegames(): """Update save games with current raws.""" @@ -254,7 +254,7 @@ def update_savegames(): def open_tilesets(): """Opens the tilesets folder.""" - open_folder(paths.get('tilesets') + open_folder(paths.get('tilesets')) def read_tilesets(): """Returns a list of tileset files.""" @@ -282,6 +282,6 @@ def install_tilesets(font, graphicsfont): df.set_option('FULLFONT', font) if (lnp.settings.version_has_option('GRAPHICS_FONT') and graphicsfont is not None and os.path.isfile( - os.path.join(paths.get('data'), 'art', graphicsfont))): + os.path.join(paths.get('data'), 'art', graphicsfont))): df.set_option('GRAPHICS_FONT', graphicsfont) df.set_option('GRAPHICS_FULLFONT', graphicsfont) diff --git a/core/mods.py b/core/mods.py index 6cd5742..8698a91 100644 --- a/core/mods.py +++ b/core/mods.py @@ -1,8 +1,12 @@ -import os, shutil, filecmp, glob, tempfile -import difflib, sys, time +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""""" +from __future__ import print_function, unicode_literals, absolute_import -from . import paths -from . import baselines +import os, shutil, glob, difflib, sys +from io import open + +from . import paths, baselines def read_mods(): """Returns a list of mod packs""" @@ -22,9 +26,9 @@ def simplify_pack(pack): baselines.remove_vanilla_raws_from_pack(pack, 'mods') baselines.remove_empty_dirs(pack, 'mods') -def do_merge_seq (mod_text, vanilla_text, gen_text): - """Merges sequences of lines. Returns empty string if a line changed by the mod - has been changed by a previous mod, or merged lines otherwise. +def do_merge_seq(mod_text, vanilla_text, gen_text): + """Merges sequences of lines. Returns empty string if a line changed by + the mod has been changed by a previous mod, or merged lines otherwise. Params: mod_text @@ -44,10 +48,10 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): if vanilla_text == mod_text: return 0, gen_text - # returns list of 5-tuples describing how to turn vanilla into mod or gen lines - # each specifies an operation, and start+end line nums for each change - # we then compose a text from these by concatenation, - # returning false if the mod changes lines which have already been changed. + # Returns list of 5-tuples describing how to turn vanilla into mod or gen + # lines. Each specifies an operation, and start+end lines for each change. + # We then compose a text from these by concatenation, returning + # false if the mod changes lines which have already been changed. van_mod_match = difflib.SequenceMatcher(None, vanilla_text, mod_text) van_gen_match = difflib.SequenceMatcher(None, vanilla_text, gen_text) van_mod_ops = van_mod_match.get_opcodes() @@ -64,7 +68,7 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): # if the mod is vanilla for these lines if mod_tag == 'equal': # if the gen lines are also vanilla - if gen_tag == 'equal' : + if gen_tag == 'equal': # append the shorter block to new genned lines if mod_i2 < gen_i2: output_file_temp += vanilla_text[cur_v:mod_i2] @@ -74,14 +78,14 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): output_file_temp += vanilla_text[cur_v:gen_i2] cur_v = gen_i2 van_gen_ops.pop(0) - if mod_i2 == gen_i2 : + if mod_i2 == gen_i2: van_mod_ops.pop(0) # otherwise append current genned lines else: output_file_temp += gen_text[gen_j1:gen_j2] cur_v = gen_i2 van_gen_ops.pop(0) - if mod_i2 == gen_i2 : + if mod_i2 == gen_i2: van_mod_ops.pop(0) # if mod has changes from vanilla else: @@ -90,7 +94,7 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): output_file_temp += mod_text[mod_j1:mod_j2] cur_v = mod_i2 van_mod_ops.pop(0) - if mod_i2 == gen_i2 : + if mod_i2 == gen_i2: van_gen_ops.pop(0) # if the changes would overlap, we can't handle that yet else: @@ -106,7 +110,7 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): output_file_temp += mod_text[cur_v:gen_i2] cur_v = gen_i2 van_gen_ops.pop(0) - if mod_i2 == gen_i2 : + if mod_i2 == gen_i2: van_mod_ops.pop(0) # clean up trailing opcodes, to avoid dropping the end of the file if van_mod_ops: @@ -117,90 +121,83 @@ def do_merge_seq (mod_text, vanilla_text, gen_text): output_file_temp += gen_text[gen_j1:gen_j2] return status, output_file_temp -def get_lines_from_file(filename): - """Get lines from a file, managing encoding. - Trying to merge files with diferent encoding causes errors; - a strict encoding often merges but at worst produces known issues. - Unicode handling changed a lot between versions... - TODO: test and improve this function! - """ - if sys.version_info[0] == 3: # Python 3.4 works for me - return open(filename, encoding='utf-8', errors='ignore').readlines() - else: # Forget handling encoding and hope for the best - return open(filename).readlines() - def do_merge_files(mod_file_name, van_file_name, gen_file_name): - ''' calls merge sequence on the files, and returns true if they could be (and were) merged - or false if the merge was conflicting (and thus skipped). - ''' - van_lines = get_lines_from_file(van_file_name) - mod_lines = get_lines_from_file(mod_file_name) + """Calls merge sequence on the files, and returns true if they could be + (and were) merged or false if the merge was conflicting (and thus skipped). + """ + van_lines = open(van_file_name, mode='r', encoding='cp437', + errors='replace').readlines() + mod_lines = open(mod_file_name, mode='r', encoding='cp437', + errors='replace').readlines() gen_lines = [] if os.path.isfile(gen_file_name): - gen_lines = get_lines_from_file(gen_file_name) + gen_lines = open(gen_file_name, mode='r', encoding='cp437', + errors='replace').readlines() status, gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) - gen_file = open(gen_file_name,"w") + gen_file = open(gen_file_name, "w") for line in gen_lines: gen_file.write(line) return status def merge_a_mod(mod): - '''Merges the specified mod, and returns status (0|1|2|3) like an exit code. + """Merges the specified mod, and returns an exit code 0-3. 0: Merge was successful, all well 1: Potential compatibility issues, no merge problems 2: Non-fatal error, overlapping lines or non-existent mod etc - 3: Fatal error, respond by rebuilding to previous mod''' + 3: Fatal error, respond by rebuilding to previous mod + """ mod_raw_folder = os.path.join(paths.get('mods'), mod, 'raw') if not os.path.isdir(mod_raw_folder): return 2 - status = merge_raw_folders(mod_raw_folder, vanilla_raw_folder) + status = merge_raw_folders(mod_raw_folder, baselines.find_vanilla_raws()) if status < 3: - with open(os.path.join(mixed_raw_folder, 'installed_raws.txt'), 'a') as log: + with open(os.path.join(paths.get('baselines'), 'temp', 'raw', + 'installed_raws.txt'), 'a') as log: log.write(mod + '\n') return status def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): - '''Merge the specified folders, output going in LNP/Baselines/temp/raw''' + """Merge the specified folders, output going in LNP/Baselines/temp/raw""" + mixed_raw_folder = os.path.join(paths.get('baselines'), 'temp', 'raw') status = 0 for file_tuple in os.walk(mod_raw_folder): for item in file_tuple[2]: - file = os.path.join(file_tuple[0], item) - file = os.path.relpath(file, mod_raw_folder) - if not file.endswith('.txt'): + f = os.path.join(file_tuple[0], item) + f = os.path.relpath(f, mod_raw_folder) + if not f.endswith('.txt'): continue - if (os.path.isfile(os.path.join(vanilla_raw_folder, file)) or - os.path.isfile(os.path.join(mixed_raw_folder, file))): - status = max(do_merge_files(os.path.join(mod_raw_folder, file), - os.path.join(vanilla_raw_folder, file), - os.path.join(mixed_raw_folder, file)), + if (os.path.isfile(os.path.join(vanilla_raw_folder, f)) or + os.path.isfile(os.path.join(mixed_raw_folder, f))): + status = max(do_merge_files(os.path.join(mod_raw_folder, f), + os.path.join(vanilla_raw_folder, f), + os.path.join(mixed_raw_folder, f)), status) else: - shutil.copy(os.path.join(mod_raw_folder, file), - os.path.join(mixed_raw_folder, file)) + shutil.copy(os.path.join(mod_raw_folder, f), + os.path.join(mixed_raw_folder, f)) return status def clear_temp(): + """Resets the folder in which raws are mixed.""" if os.path.exists(os.path.join(paths.get('baselines'), 'temp')): shutil.rmtree(os.path.join(paths.get('baselines'), 'temp')) - shutil.copytree(vanilla_raw_folder, mixed_raw_folder) - with open(os.path.join(mixed_raw_folder, 'installed_raws.txt'), 'w') as log: + shutil.copytree(baselines.find_vanilla_raws(), + os.path.join(paths.get('baselines'), 'temp', 'raw')) + with open(os.path.join(paths.get('baselines'), 'temp', 'raw', + 'installed_raws.txt'), 'w') as log: log.write('# List of raws merged by PyLNP:\n' + - os.path.dirname(vanilla_raw_folder)[-8:] + '\n') - -def init_paths(): - global vanilla_raw_folder, mixed_raw_folder - vanilla_raw_folder = baselines.find_vanilla_raws() - mixed_raw_folder = os.path.join(paths.get('baselines'), 'temp', 'raw') - clear_temp() + os.path.basename(baselines.find_vanilla_raws()) + '\n') def make_mod_from_installed_raws(name): - '''Capture whatever unavailable mods a user currently has installed as a mod called $name. + """Capture whatever unavailable mods a user currently has installed + as a mod called $name. * If `installed_raws.txt` is not present, compare to vanilla - * Otherwise, rebuild as much as possible then compare installed and rebuilt - * Harder than I first thought... but not impossible''' + * Otherwise, rebuild as much as possible then compare to installed + * Harder than I first thought... but not impossible. + """ if os.path.isdir(os.path.join(paths.get('mods'), name)): return False if get_installed_mods_from_log(): @@ -208,7 +205,8 @@ def make_mod_from_installed_raws(name): for mod in get_installed_mods_from_log(): merge_a_mod(mod) reconstruction = os.path.join(paths.get('baselines'), 'temp2', 'raw') - shutil.copytree(mixed_raw_folder, reconstruction) + shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), + reconstruction) else: reconstruction = baselines.find_vanilla_raws() @@ -229,15 +227,15 @@ def make_mod_from_installed_raws(name): return False def get_installed_mods_from_log(): - '''Return best possible mod load order to recreate installed with available''' - logged = read_installation_log(os.path.join(paths.get('df'), 'raw', 'installed_raws.txt')) - # return list overlap - like set intersection, but ordered + """Return best mod load order to recreate installed with available.""" + logged = read_installation_log(os.path.join(paths.get('df'), + 'raw', 'installed_raws.txt')) return [mod for mod in logged if mod in read_mods()] -def read_installation_log(file): - '''Read an 'installed_raws.txt' and return it's full contents.''' +def read_installation_log(fl): + """Read an 'installed_raws.txt' and return it's full contents.""" try: - with open(file) as f: + with open(fl) as f: file_contents = list(f.readlines()) except IOError: return [] diff --git a/core/update.py b/core/update.py index e60da4d..8c45e69 100644 --- a/core/update.py +++ b/core/update.py @@ -75,7 +75,7 @@ def download_df_version_to_baselines(version='invalid_string'): False if the download did not start None if the version string was invalid """ - if not re.match('df_\d\d_\d\d', version): + if not re.match(r'df_\d\d_\d\d', version): return None filename = version + '_win.zip' if not 'download_'+version in (t.name for t in threading.enumerate()): diff --git a/readme.rst b/readme.rst index f47d45c..e78e525 100644 --- a/readme.rst +++ b/readme.rst @@ -338,7 +338,7 @@ This folder contains tilesets; individual image files that the user can use for Mods ---- -This folder contains mods, consisting of a modified raw folder. They use the same reduced format for raws as graphics packs. +This folder contains mods for Dwarf Fortress, in the form of changes to the defining raws (which define the content DF uses). Mods use the same reduced format for raws as graphics packs. Keybinds -------- diff --git a/tkgui/mods.py b/tkgui/mods.py index 9415c5e..908500b 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -29,7 +29,7 @@ def create_variables(self): self.available = Variable() def read_data(self): - mods.init_paths() + mods.clear_temp() available = mods.read_mods() installed = mods.get_installed_mods_from_log() available = [m for m in available if m not in installed] @@ -130,15 +130,16 @@ def move_down(self): self.perform_merge() def create_from_installed(self): + """Extracts a mod from the currently installed raws.""" m = simpledialog.askstring("Create Mod", "New mod name:") if m is not None and m != '': if mods.make_mod_from_installed_raws(m): messagebox.showinfo('Mod extracted', - 'Your custom mod was extracted as '+m+'.') + 'Your custom mod was extracted as ' + m) else: messagebox.showinfo('Error', - 'There is already a mod with that name, ' - 'or only pre-existing mods were found.') + 'There is already a mod with that name, ' + 'or only pre-existing mods were found.') self.read_data() def add_to_installed(self): @@ -181,11 +182,10 @@ def perform_merge(self): def install_mods(self): """Replaces /raw with the contents LNP/Baselines/temp/raw""" if messagebox.askokcancel( - message='Your graphics and raws will be changed.\n\n' - 'The mod merging function is still in beta. This could ' - 'break new worlds, or even cause crashes.\n\n' - '', - title='Are you sure?'): + message = ('Your graphics and raws will be changed.\n\n' + 'The mod merging function is still in beta. This ' + 'could break new worlds, or even cause crashes.\n\n'), + title='Are you sure?'): shutil.rmtree(os.path.join(paths.get('df'), 'raw')) shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), os.path.join(paths.get('df'), 'raw')) @@ -193,6 +193,7 @@ def install_mods(self): 'Mods installed', 'The selected mods were installed.\nGenerate a new world to ' 'start playing with them!') - + def simplify_mods(self): + """Simplify mods; runs on startup if called directly by button.""" mods.simplify_mods() From 856cb29c86b388f10603c5e3a611783480bf4c90 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Fri, 21 Nov 2014 13:26:04 +1100 Subject: [PATCH 20/29] More fixes! core.mods.do_merge_seq is going to continue creating PyLint noise - it's a big and fairly complicated function but I can't see a good way to break it up. I've tried to keep it much more heavily commented to compensate; that and the difflib docs should make it easy enough to follow. --- core/baselines.py | 22 +++++++++++----------- core/graphics.py | 3 +-- core/mods.py | 39 +++++++++++++++++---------------------- tkgui/mods.py | 27 ++++++++++++++++++--------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 866510d..39a1b8f 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -10,7 +10,7 @@ from . import update from .lnp import lnp -def find_vanilla_raws(version=None): +def find_vanilla_raws(): """Finds vanilla raws for the requested version. If required, unzip a DF release to create the folder in LNP/Baselines/. @@ -37,12 +37,12 @@ def find_vanilla_raws(version=None): available = [os.path.basename(item) for item in glob.glob(os.path.join( paths.get('baselines'), 'df_??_?*')) if os.path.isdir(item)] - if version == None: - version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') - if lnp.df_info.source == "init detection": - # WARNING: likley to be much too early in this case - # User should restore 'release notes.txt' - pass + + version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') + if lnp.df_info.source == "init detection": + # WARNING: likley to be much too early in this case + # User should restore 'release notes.txt' + pass if version not in available: update.download_df_version_to_baselines(version) version = available[-1] @@ -128,15 +128,15 @@ def remove_vanilla_raws_from_pack(pack, folder): """ raw_folder = os.path.join(paths.get(folder), pack, 'raw') vanilla_raw_folder = find_vanilla_raws() - for root, dirs, files in os.walk(raw_folder): + for root, _, files in os.walk(raw_folder): for f in files: f = os.path.join(root, f) # silently clean up so empty dirs can be removed silently_kill = ('Thumbs.db', 'installed_mods.txt') - if any(file.endswith(k) for k in silently_kill): + if any(f.endswith(k) for k in silently_kill): os.remove(file) continue - f = os.path.relpath(file, raw_folder) + f = os.path.relpath(f, raw_folder) # if there's an identical vanilla file, remove the mod file if os.path.isfile(os.path.join(vanilla_raw_folder, f)): if filecmp.cmp(os.path.join(vanilla_raw_folder, f), @@ -153,7 +153,7 @@ def remove_empty_dirs(pack, folder): The parent folder of the pack (either 'mods' or 'graphics') """ pack = os.path.join(paths.get(folder), pack) - for n in range(3): + for _ in range(3): # only catches the lowest level each iteration for root, dirs, files in os.walk(pack): if not dirs and not files: diff --git a/core/graphics.py b/core/graphics.py index 207107d..c4548cf 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -46,8 +46,7 @@ def read_graphics(): return tuple(result) def install_graphics(pack): - """ - Installs the graphics pack located in LNP/Graphics/. + """Installs the graphics pack located in LNP/Graphics/. Params: pack diff --git a/core/mods.py b/core/mods.py index 8698a91..3eb8770 100644 --- a/core/mods.py +++ b/core/mods.py @@ -3,14 +3,14 @@ """""" from __future__ import print_function, unicode_literals, absolute_import -import os, shutil, glob, difflib, sys +import os, shutil, glob +from difflib import SequenceMatcher from io import open from . import paths, baselines def read_mods(): """Returns a list of mod packs""" - # should go in tkgui/mods.py later return [os.path.basename(o) for o in glob.glob(os.path.join(paths.get('mods'), '*')) if os.path.isdir(o)] @@ -48,23 +48,19 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): if vanilla_text == mod_text: return 0, gen_text - # Returns list of 5-tuples describing how to turn vanilla into mod or gen + # Get a list of 5-tuples describing how to turn vanilla into mod or gen # lines. Each specifies an operation, and start+end lines for each change. - # We then compose a text from these by concatenation, returning - # false if the mod changes lines which have already been changed. - van_mod_match = difflib.SequenceMatcher(None, vanilla_text, mod_text) - van_gen_match = difflib.SequenceMatcher(None, vanilla_text, gen_text) - van_mod_ops = van_mod_match.get_opcodes() - van_gen_ops = van_gen_match.get_opcodes() - - output_file_temp = [] - # holds the line we're up to, effectively truncates blocks which were + van_mod_ops = SequenceMatcher(None, vanilla_text, mod_text).get_opcodes() + van_gen_ops = SequenceMatcher(None, vanilla_text, gen_text).get_opcodes() + + # cur_v holds the line we're up to, effectively truncates blocks which were # partially covered in the previous iteration. - cur_v = 0 + output_file_temp, cur_v = [], 0 + while van_mod_ops and van_gen_ops: # get names from the next set of opcodes - mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] - gen_tag, gen_i1, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] + mod_tag, _, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] + gen_tag, _, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] # if the mod is vanilla for these lines if mod_tag == 'equal': # if the gen lines are also vanilla @@ -96,10 +92,8 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): van_mod_ops.pop(0) if mod_i2 == gen_i2: van_gen_ops.pop(0) - # if the changes would overlap, we can't handle that yet else: - # this is the rare over-write merge. - # changes status, use caution + # An over-write merge. Changes status to warn the user. status = 2 # append the shorter block to new genned lines if mod_i2 < gen_i2: @@ -113,6 +107,7 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): if mod_i2 == gen_i2: van_mod_ops.pop(0) # clean up trailing opcodes, to avoid dropping the end of the file + # TODO: check these are insertions, handle non-insertions appropriately. if van_mod_ops: mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] output_file_temp += mod_text[mod_j1:mod_j2] @@ -169,13 +164,13 @@ def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): if not f.endswith('.txt'): continue if (os.path.isfile(os.path.join(vanilla_raw_folder, f)) or - os.path.isfile(os.path.join(mixed_raw_folder, f))): + os.path.isfile(os.path.join(mixed_raw_folder, f))): status = max(do_merge_files(os.path.join(mod_raw_folder, f), os.path.join(vanilla_raw_folder, f), os.path.join(mixed_raw_folder, f)), status) else: - shutil.copy(os.path.join(mod_raw_folder, f), + shutil.copy(os.path.join(mod_raw_folder, f), os.path.join(mixed_raw_folder, f)) return status @@ -187,7 +182,7 @@ def clear_temp(): os.path.join(paths.get('baselines'), 'temp', 'raw')) with open(os.path.join(paths.get('baselines'), 'temp', 'raw', 'installed_raws.txt'), 'w') as log: - log.write('# List of raws merged by PyLNP:\n' + + log.write('# List of raws merged by PyLNP:\n' + os.path.basename(baselines.find_vanilla_raws()) + '\n') def make_mod_from_installed_raws(name): @@ -221,7 +216,7 @@ def make_mod_from_installed_raws(name): shutil.rmtree(os.path.join(paths.get('baselines'), 'temp2')) if os.path.isdir(os.path.join(paths.get('baselines'), 'temp')): - shutil.copytree(os.path.join(paths.get('baselines'), 'temp'), + shutil.copytree(os.path.join(paths.get('baselines'), 'temp'), os.path.join(paths.get('mods'), name)) return True return False diff --git a/tkgui/mods.py b/tkgui/mods.py index 908500b..1053383 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -15,11 +15,13 @@ # pylint:disable=import-error from tkinter import * from tkinter.ttk import * + import tkinter.messagebox as messagebox import tkinter.simpledialog as simpledialog else: # pylint:disable=import-error from Tkinter import * from ttk import * + import tkMessageBox as messagebox import tkSimpleDialog as simpledialog class ModsTab(Tab): @@ -88,6 +90,7 @@ def create_controls(self): f.grid(row=3, column=0, sticky="ew") def move_up(self): + """Moves the selected item/s up in the merge order and re-merges.""" if len(self.installed_list.curselection()) == 0: return selection = [int(i) for i in self.installed_list.curselection()] @@ -109,6 +112,7 @@ def move_up(self): self.perform_merge() def move_down(self): + """Moves the selected item/s down in the merge order and re-merges.""" if len(self.installed_list.curselection()) == 0: return selection = [int(i) for i in self.installed_list.curselection()] @@ -137,12 +141,13 @@ def create_from_installed(self): messagebox.showinfo('Mod extracted', 'Your custom mod was extracted as ' + m) else: - messagebox.showinfo('Error', - 'There is already a mod with that name, ' - 'or only pre-existing mods were found.') + messagebox.showinfo( + 'Error', ('There is already a mod with that name, ' + 'or only pre-existing mods were found.')) self.read_data() def add_to_installed(self): + """Move selected mod/s from available to merged list and re-merge.""" if len(self.available_list.curselection()) == 0: return for i in self.available_list.curselection(): @@ -152,6 +157,7 @@ def add_to_installed(self): self.perform_merge() def remove_from_installed(self): + """Move selected mod/s from merged to available list and re-merge.""" if len(self.installed_list.curselection()) == 0: return for i in self.installed_list.curselection()[::-1]: @@ -167,6 +173,7 @@ def remove_from_installed(self): self.perform_merge() def perform_merge(self): + """Merge the selected mods, with background color for user feedback.""" mods.clear_temp() # Set status to unknown before merging for i, _ in enumerate(self.installed_list.get(0, END)): @@ -179,13 +186,14 @@ def perform_merge(self): if status == 3: return - def install_mods(self): + @staticmethod + def install_mods(): """Replaces /raw with the contents LNP/Baselines/temp/raw""" if messagebox.askokcancel( - message = ('Your graphics and raws will be changed.\n\n' - 'The mod merging function is still in beta. This ' - 'could break new worlds, or even cause crashes.\n\n'), - title='Are you sure?'): + message=('Your graphics and raws will be changed.\n\n' + 'The mod merging function is still in beta. This ' + 'could break new worlds, or even cause crashes.\n\n'), + title='Are you sure?'): shutil.rmtree(os.path.join(paths.get('df'), 'raw')) shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), os.path.join(paths.get('df'), 'raw')) @@ -194,6 +202,7 @@ def install_mods(self): 'The selected mods were installed.\nGenerate a new world to ' 'start playing with them!') - def simplify_mods(self): + @staticmethod + def simplify_mods(): """Simplify mods; runs on startup if called directly by button.""" mods.simplify_mods() From 09f805b661f9ecca343500055e7ba1e7cf76cb3f Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Sun, 23 Nov 2014 14:47:47 +1100 Subject: [PATCH 21/29] Mods logic fixes, better comments. --- core/baselines.py | 4 ++-- core/mods.py | 34 ++++++++++++++++++++-------------- core/update.py | 8 ++++++-- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 39a1b8f..3399a78 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -25,7 +25,7 @@ def find_vanilla_raws(): If requested version unavailable, path to latest version None if no version was available. """ - # TODO: handle other DF versions; esp. small pack and non-SDL releases + # TODO: also transparently extract *nix releases (*.tar.bz2) zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) for item in zipped: version = os.path.basename(item)[0:8] @@ -134,7 +134,7 @@ def remove_vanilla_raws_from_pack(pack, folder): # silently clean up so empty dirs can be removed silently_kill = ('Thumbs.db', 'installed_mods.txt') if any(f.endswith(k) for k in silently_kill): - os.remove(file) + os.remove(f) continue f = os.path.relpath(f, raw_folder) # if there's an identical vanilla file, remove the mod file diff --git a/core/mods.py b/core/mods.py index 3eb8770..97e55bd 100644 --- a/core/mods.py +++ b/core/mods.py @@ -26,6 +26,12 @@ def simplify_pack(pack): baselines.remove_vanilla_raws_from_pack(pack, 'mods') baselines.remove_empty_dirs(pack, 'mods') +def install_mods(): + """Deletes installed raw folder, and copies over installed raws.""" + shutil.rmtree(os.path.join(paths.get('df'), 'raw')) + shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), + os.path.join(paths.get('df'), 'raw')) + def do_merge_seq(mod_text, vanilla_text, gen_text): """Merges sequences of lines. Returns empty string if a line changed by the mod has been changed by a previous mod, or merged lines otherwise. @@ -56,7 +62,7 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): # cur_v holds the line we're up to, effectively truncates blocks which were # partially covered in the previous iteration. output_file_temp, cur_v = [], 0 - + while van_mod_ops and van_gen_ops: # get names from the next set of opcodes mod_tag, _, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] @@ -107,12 +113,11 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): if mod_i2 == gen_i2: van_mod_ops.pop(0) # clean up trailing opcodes, to avoid dropping the end of the file - # TODO: check these are insertions, handle non-insertions appropriately. - if van_mod_ops: - mod_tag, mod_i1, mod_i2, mod_j1, mod_j2 = van_mod_ops[0] + while van_mod_ops: + mod_tag, _, mod_i2, mod_j1, mod_j2 = van_mod_ops.pop(0) output_file_temp += mod_text[mod_j1:mod_j2] - if van_gen_ops: - gen_tag, gen_i1, gen_i2, gen_j1, gen_j2 = van_gen_ops[0] + while van_gen_ops: + gen_tag, _, gen_i2, gen_j1, gen_j2 = van_gen_ops.pop(0) output_file_temp += gen_text[gen_j1:gen_j2] return status, output_file_temp @@ -124,15 +129,16 @@ def do_merge_files(mod_file_name, van_file_name, gen_file_name): errors='replace').readlines() mod_lines = open(mod_file_name, mode='r', encoding='cp437', errors='replace').readlines() - gen_lines = [] - if os.path.isfile(gen_file_name): - gen_lines = open(gen_file_name, mode='r', encoding='cp437', - errors='replace').readlines() + gen_lines = open(gen_file_name, mode='r', encoding='cp437', + errors='replace').readlines() status, gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) gen_file = open(gen_file_name, "w") for line in gen_lines: - gen_file.write(line) + try: + gen_file.write(line) + except UnicodeEncodeError: + return 3 # invalid character for DF encoding return status def merge_a_mod(mod): @@ -163,7 +169,7 @@ def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): f = os.path.relpath(f, mod_raw_folder) if not f.endswith('.txt'): continue - if (os.path.isfile(os.path.join(vanilla_raw_folder, f)) or + if (os.path.isfile(os.path.join(vanilla_raw_folder, f)) and os.path.isfile(os.path.join(mixed_raw_folder, f))): status = max(do_merge_files(os.path.join(mod_raw_folder, f), os.path.join(vanilla_raw_folder, f), @@ -227,10 +233,10 @@ def get_installed_mods_from_log(): 'raw', 'installed_raws.txt')) return [mod for mod in logged if mod in read_mods()] -def read_installation_log(fl): +def read_installation_log(log): """Read an 'installed_raws.txt' and return it's full contents.""" try: - with open(fl) as f: + with open(log) as f: file_contents = list(f.readlines()) except IOError: return [] diff --git a/core/update.py b/core/update.py index 8c45e69..2c4560f 100644 --- a/core/update.py +++ b/core/update.py @@ -72,18 +72,22 @@ def download_df_version_to_baselines(version='invalid_string'): Returns: True if the download was started (in a thread, for availability later) - False if the download did not start + False if the download did not start (eg because of another thread) None if the version string was invalid """ if not re.match(r'df_\d\d_\d\d', version): return None + # Windows is always OK; we discard OS-specific files and .zips are easier. + # TODO: Download the small version when it exists, to save bandwidth. filename = version + '_win.zip' if not 'download_'+version in (t.name for t in threading.enumerate()): t = threading.Thread(target=download_df_zip_from_bay12, args=(filename,), name='download_'+version) t.daemon = True t.start() - return True + return True + else: + return False def download_df_zip_from_bay12(filename): """Downloads a zipped version of DF from Bay12 Games. From dc48f8e7f3ff0eb60610b74ff9bdd292ee612e63 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Sun, 23 Nov 2014 15:03:29 +1100 Subject: [PATCH 22/29] Use moved install function, instead of duplicating it. --- tkgui/mods.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tkgui/mods.py b/tkgui/mods.py index 1053383..f025b1f 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -194,9 +194,7 @@ def install_mods(): 'The mod merging function is still in beta. This ' 'could break new worlds, or even cause crashes.\n\n'), title='Are you sure?'): - shutil.rmtree(os.path.join(paths.get('df'), 'raw')) - shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), - os.path.join(paths.get('df'), 'raw')) + mods.install_mods() messagebox.showinfo( 'Mods installed', 'The selected mods were installed.\nGenerate a new world to ' From a462d5d46371f707f24dad72efeabd0c03b3aca2 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Sun, 23 Nov 2014 22:14:36 +1100 Subject: [PATCH 23/29] Improve download function; now works for any DF version. Though a specific version can't be specified now, instead always get the current version (small if possible). New DFInstall classmethod get_archive_name to provide this filename. Fix base recorded in installed_raws.txt (version instead of 'raw'). Remove unused imports from tkgui/mods.py --- core/baselines.py | 46 ++++++++++++++++++++++------------------------ core/df.py | 17 +++++++++++++++++ core/mods.py | 4 ++-- core/update.py | 21 ++++++++------------- tkgui/mods.py | 5 ++--- 5 files changed, 51 insertions(+), 42 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 3399a78..894489c 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -11,44 +11,42 @@ from .lnp import lnp def find_vanilla_raws(): - """Finds vanilla raws for the requested version. - If required, unzip a DF release to create the folder in LNP/Baselines/. - - Params: - version - String indicating version in format 'df_40_15' - None returns the latest available raws. + """Finds vanilla raws for the current version. + Starts by unzipping any DF releases in baselines and preprocessing them. Returns: The path to the requested vanilla 'raw' folder eg: 'LNP/Baselines/df_40_15/raw' - If requested version unavailable, path to latest version + If requested version unavailable, path to latest available version None if no version was available. """ - # TODO: also transparently extract *nix releases (*.tar.bz2) - zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) - for item in zipped: - version = os.path.basename(item)[0:8] - f = os.path.join(paths.get('baselines'), version) - if not os.path.isdir(f): - zipfile.ZipFile(item).extractall(f) - simplify_pack(version, 'baselines') - os.remove(item) - + prepare_baselines() available = [os.path.basename(item) for item in glob.glob(os.path.join( - paths.get('baselines'), 'df_??_?*')) if os.path.isdir(item)] - + paths.get('baselines'), 'df_??_??')) if os.path.isdir(item)] + if not available: + return None version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') if lnp.df_info.source == "init detection": - # WARNING: likley to be much too early in this case - # User should restore 'release notes.txt' + # WARNING: probably the wrong version! Restore 'release notes.txt'. pass if version not in available: - update.download_df_version_to_baselines(version) + update.download_df_baseline() version = available[-1] - return os.path.join(paths.get('baselines'), version, 'raw') +def prepare_baselines(): + """Unzip any DF releases found, and discard non-universial files.""" + zipped = glob.glob(os.path.join(paths.get('baselines'), 'df_??_?*.zip')) + for item in zipped: + version = os.path.basename(item) + for s in ['_win', '_legacy', '_s', '.zip']: + version = version.replace(s, '') + f = os.path.join(paths.get('baselines'), version) + if not os.path.isdir(f): + zipfile.ZipFile(item).extractall(f) + simplify_pack(version, 'baselines') + os.remove(item) + def simplify_pack(pack, folder): """Removes unnecessary files from LNP//. Necessary files means: diff --git a/core/df.py b/core/df.py index 87f6033..74bc261 100644 --- a/core/df.py +++ b/core/df.py @@ -205,6 +205,23 @@ def detect_variations(self): result.append('legacy') return result + def get_archive_name(self): + """Return the filename of the download for this version. + Always windows, for comparison of raws in baselines. + Prefer small and SDL releases when available.""" + base = 'df_' + self.version[2:].replace('.', '_') + if self.version >= '0.31.13': + return base + '_win_s.zip' + if self.version >= '0.31.05': + return base + '_legacy_s.zip' + if self.version == '0.31.04': + return base + '_legacy.zip' + if self.version == '0.31.01': + return base + '.zip' + if self.version >= '0.21.104.19b': + return base + '_s.zip' + return base + '.zip' + @total_ordering class Version(object): """Container for a version number for easy comparisons.""" diff --git a/core/mods.py b/core/mods.py index 97e55bd..8c74d2b 100644 --- a/core/mods.py +++ b/core/mods.py @@ -189,7 +189,8 @@ def clear_temp(): with open(os.path.join(paths.get('baselines'), 'temp', 'raw', 'installed_raws.txt'), 'w') as log: log.write('# List of raws merged by PyLNP:\n' + - os.path.basename(baselines.find_vanilla_raws()) + '\n') + os.path.basename( + os.path.dirname(baselines.find_vanilla_raws())) + '\n') def make_mod_from_installed_raws(name): """Capture whatever unavailable mods a user currently has installed @@ -197,7 +198,6 @@ def make_mod_from_installed_raws(name): * If `installed_raws.txt` is not present, compare to vanilla * Otherwise, rebuild as much as possible then compare to installed - * Harder than I first thought... but not impossible. """ if os.path.isdir(os.path.join(paths.get('mods'), name)): return False diff --git a/core/update.py b/core/update.py index 2c4560f..f2f9ed7 100644 --- a/core/update.py +++ b/core/update.py @@ -14,6 +14,7 @@ from urllib.error import URLError from .lnp import lnp +from .df import DFInstall from . import launcher, paths def updates_configured(): @@ -62,27 +63,21 @@ def start_update(): """Launches a webbrowser to the specified update URL.""" launcher.open_url(lnp.config.get_string('updates/downloadURL')) -def download_df_version_to_baselines(version='invalid_string'): - """Download the specified version of DF from Bay12 Games - to serve as a baseline, in LNP/Baselines/ - - Params: - version - The requested version of DF +def download_df_baseline(): + """Download the current version of DF from Bay12 Games to serve as a + baseline, in LNP/Baselines/ Returns: True if the download was started (in a thread, for availability later) False if the download did not start (eg because of another thread) None if the version string was invalid """ - if not re.match(r'df_\d\d_\d\d', version): + if not re.match(r'df_\d\d_\d\d\w*', version): return None - # Windows is always OK; we discard OS-specific files and .zips are easier. - # TODO: Download the small version when it exists, to save bandwidth. - filename = version + '_win.zip' - if not 'download_'+version in (t.name for t in threading.enumerate()): + filename = DFInstall.get_archive_name() + if not 'download_' + version in (t.name for t in threading.enumerate()): t = threading.Thread(target=download_df_zip_from_bay12, - args=(filename,), name='download_'+version) + args=(filename,), name='download_' + version) t.daemon = True t.start() return True diff --git a/tkgui/mods.py b/tkgui/mods.py index f025b1f..bbca7d5 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -4,12 +4,11 @@ """Mods tab for the TKinter GUI.""" from __future__ import print_function, unicode_literals, absolute_import +import sys + from . import controls from .tab import Tab -import sys, os, shutil - from core import mods -from core import paths if sys.version_info[0] == 3: # Alternate import names # pylint:disable=import-error From 1f1e781cf4d8b865433718f4aa034eb8b75f0f5f Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Mon, 24 Nov 2014 08:48:18 +1100 Subject: [PATCH 24/29] Clean up download function (remove references to old argument) --- core/update.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/core/update.py b/core/update.py index f2f9ed7..1b8ff3b 100644 --- a/core/update.py +++ b/core/update.py @@ -70,10 +70,7 @@ def download_df_baseline(): Returns: True if the download was started (in a thread, for availability later) False if the download did not start (eg because of another thread) - None if the version string was invalid """ - if not re.match(r'df_\d\d_\d\d\w*', version): - return None filename = DFInstall.get_archive_name() if not 'download_' + version in (t.name for t in threading.enumerate()): t = threading.Thread(target=download_df_zip_from_bay12, @@ -81,8 +78,7 @@ def download_df_baseline(): t.daemon = True t.start() return True - else: - return False + return False def download_df_zip_from_bay12(filename): """Downloads a zipped version of DF from Bay12 Games. From 8dafcad6a9d86e23217cfa2321e3e326ab1e14d9 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Mon, 24 Nov 2014 23:30:59 +1100 Subject: [PATCH 25/29] Remove old variable from updates. --- core/update.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/update.py b/core/update.py index 1b8ff3b..e519e3c 100644 --- a/core/update.py +++ b/core/update.py @@ -72,9 +72,9 @@ def download_df_baseline(): False if the download did not start (eg because of another thread) """ filename = DFInstall.get_archive_name() - if not 'download_' + version in (t.name for t in threading.enumerate()): + if not 'download_' + filename in (t.name for t in threading.enumerate()): t = threading.Thread(target=download_df_zip_from_bay12, - args=(filename,), name='download_' + version) + args=(filename,), name='download_' + filename) t.daemon = True t.start() return True From 85d293118cdb286ed8fc4ec0b1c6c4024ec79905 Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Tue, 25 Nov 2014 13:42:52 +1100 Subject: [PATCH 26/29] Use new download function; improve handling of missing baselines. Not ready for release until a user warning is hooked in on the tkgui side - at present the functions will return None appropriately, but this goes nowhere. Affected functions: mods.clear_temp(), graphics.install_graphics() --- core/baselines.py | 25 ++++++++++--------------- core/graphics.py | 5 ++++- core/mods.py | 9 ++++++++- core/update.py | 30 +++++------------------------- 4 files changed, 27 insertions(+), 42 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 894489c..c014876 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -15,24 +15,19 @@ def find_vanilla_raws(): Starts by unzipping any DF releases in baselines and preprocessing them. Returns: - The path to the requested vanilla 'raw' folder - eg: 'LNP/Baselines/df_40_15/raw' - If requested version unavailable, path to latest available version - None if no version was available. + Path to the vanilla 'raw' folder, eg 'LNP/Baselines/df_40_15/raw' + False if baseline not available (and start download) + None if version detection is not accurate """ - prepare_baselines() - available = [os.path.basename(item) for item in glob.glob(os.path.join( - paths.get('baselines'), 'df_??_??')) if os.path.isdir(item)] - if not available: - return None - version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') if lnp.df_info.source == "init detection": # WARNING: probably the wrong version! Restore 'release notes.txt'. - pass - if version not in available: - update.download_df_baseline() - version = available[-1] - return os.path.join(paths.get('baselines'), version, 'raw') + return None + prepare_baselines() + version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') + if os.path.isdir(os.path.join(paths.get('baselines'), version, 'raw')): + return os.path.join(paths.get('baselines'), version, 'raw') + update.download_df_baseline() + return False def prepare_baselines(): """Unzip any DF releases found, and discard non-universial files.""" diff --git a/core/graphics.py b/core/graphics.py index c4548cf..42984c4 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -55,8 +55,11 @@ def install_graphics(pack): Returns: True if successful, False if an exception occured - None if required files are missing (raw/graphics, data/init) + None if baseline vanilla raws are missing """ + if not baselines.find_vanilla_raws(): + # TODO: add user warning re: missing baseline, download + return None gfx_dir = tempfile.mkdtemp() dir_util.copy_tree(baselines.find_vanilla_raws(), gfx_dir) dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), gfx_dir) diff --git a/core/mods.py b/core/mods.py index 8c74d2b..137be8c 100644 --- a/core/mods.py +++ b/core/mods.py @@ -131,7 +131,6 @@ def do_merge_files(mod_file_name, van_file_name, gen_file_name): errors='replace').readlines() gen_lines = open(gen_file_name, mode='r', encoding='cp437', errors='replace').readlines() - status, gen_lines = do_merge_seq(mod_lines, van_lines, gen_lines) gen_file = open(gen_file_name, "w") for line in gen_lines: @@ -149,6 +148,8 @@ def merge_a_mod(mod): 2: Non-fatal error, overlapping lines or non-existent mod etc 3: Fatal error, respond by rebuilding to previous mod """ + if not baselines.find_vanilla_raws(): + return 3 # no baseline; caught properly earlier mod_raw_folder = os.path.join(paths.get('mods'), mod, 'raw') if not os.path.isdir(mod_raw_folder): return 2 @@ -182,6 +183,9 @@ def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): def clear_temp(): """Resets the folder in which raws are mixed.""" + if not baselines.find_vanilla_raws(): + # TODO: add user warning re: missing baseline, download + return None if os.path.exists(os.path.join(paths.get('baselines'), 'temp')): shutil.rmtree(os.path.join(paths.get('baselines'), 'temp')) shutil.copytree(baselines.find_vanilla_raws(), @@ -210,6 +214,9 @@ def make_mod_from_installed_raws(name): reconstruction) else: reconstruction = baselines.find_vanilla_raws() + if not reconstruction: + # TODO: add user warning re: missing baseline, download + return None clear_temp() merge_raw_folders(reconstruction, os.path.join(paths.get('df'), 'raw')) diff --git a/core/update.py b/core/update.py index e519e3c..b5e2e74 100644 --- a/core/update.py +++ b/core/update.py @@ -15,7 +15,7 @@ from .lnp import lnp from .df import DFInstall -from . import launcher, paths +from . import launcher, paths, download def updates_configured(): """Returns True if update checking have been configured.""" @@ -65,28 +65,8 @@ def start_update(): def download_df_baseline(): """Download the current version of DF from Bay12 Games to serve as a - baseline, in LNP/Baselines/ - - Returns: - True if the download was started (in a thread, for availability later) - False if the download did not start (eg because of another thread) - """ - filename = DFInstall.get_archive_name() - if not 'download_' + filename in (t.name for t in threading.enumerate()): - t = threading.Thread(target=download_df_zip_from_bay12, - args=(filename,), name='download_' + filename) - t.daemon = True - t.start() - return True - return False - -def download_df_zip_from_bay12(filename): - """Downloads a zipped version of DF from Bay12 Games. - 'filename' is the full name of the file to retrieve, - eg 'df_40_07_win.zip' """ + baseline, in LNP/Baselines/""" + filename = lnp.df_info.get_archive_name() url = 'http://www.bay12games.com/dwarves/' + filename - req = Request(url, headers={'User-Agent':'PyLNP'}) - archive = urlopen(req, timeout=3).read() - with open(os.path.join(paths.get('baselines'), filename), 'wb') as f: - f.write(archive) - f.close() + target = os.path.join(paths.get('baselines'), filename) + download.download('baselines', url, target) From 55c66bcefc6c6b719717c89814f22e64e337fb6b Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Wed, 26 Nov 2014 12:36:15 +1100 Subject: [PATCH 27/29] Fix docstrings, fix import style, minor refactor graphics install, correct graphics pack verification. --- .hgignore | 1 + core/baselines.py | 3 +- core/graphics.py | 106 ++++++++++++++++++++-------------------------- core/mods.py | 7 ++- 4 files changed, 52 insertions(+), 65 deletions(-) diff --git a/.hgignore b/.hgignore index 84b0f78..b1b142c 100644 --- a/.hgignore +++ b/.hgignore @@ -10,3 +10,4 @@ stdout.txt Dwarf Fortress * LNP __pycache__ +*.bat diff --git a/core/baselines.py b/core/baselines.py index c014876..77a064e 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -6,8 +6,7 @@ import os, shutil, filecmp, sys, glob, tempfile, zipfile import distutils.dir_util as dir_util -from . import paths -from . import update +from . import paths, update from .lnp import lnp def find_vanilla_raws(): diff --git a/core/graphics.py b/core/graphics.py index 42984c4..2695e90 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -15,8 +15,7 @@ def open_graphics(): open_folder(paths.get('graphics')) def current_pack(): - """ - Returns the currently installed graphics pack. + """Returns the currently installed graphics pack. If the pack cannot be identified, returns "FONT/GRAPHICS_FONT". """ packs = read_graphics() @@ -57,6 +56,7 @@ def install_graphics(pack): False if an exception occured None if baseline vanilla raws are missing """ + retval = None if not baselines.find_vanilla_raws(): # TODO: add user warning re: missing baseline, download return None @@ -64,72 +64,63 @@ def install_graphics(pack): dir_util.copy_tree(baselines.find_vanilla_raws(), gfx_dir) dir_util.copy_tree(os.path.join(paths.get('graphics'), pack), gfx_dir) - 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(paths.get('df'), 'raw', 'graphics')): - dir_util.remove_tree( - os.path.join(paths.get('df'), 'raw', 'graphics')) + try: + # Delete old graphics + if os.path.isdir(os.path.join(paths.get('df'), 'raw', 'graphics')): + dir_util.remove_tree( + os.path.join(paths.get('df'), 'raw', 'graphics')) - # Copy new raws - dir_util.copy_tree( - os.path.join(gfx_dir, 'raw'), - os.path.join(paths.get('df'), 'raw')) + # Copy new raws + dir_util.copy_tree( + os.path.join(gfx_dir, 'raw'), + os.path.join(paths.get('df'), 'raw')) - #Copy art - if os.path.isdir(os.path.join(paths.get('data'), 'art')): - dir_util.remove_tree( - os.path.join(paths.get('data'), 'art')) - dir_util.copy_tree( - os.path.join(gfx_dir, 'data', 'art'), + #Copy art + if os.path.isdir(os.path.join(paths.get('data'), 'art')): + dir_util.remove_tree( os.path.join(paths.get('data'), 'art')) - for tiles in glob.glob(os.path.join(paths.get('tilesets'), '*')): - shutil.copy(tiles, os.path.join(paths.get('data'), 'art')) - - patch_inits(gfx_dir) + dir_util.copy_tree( + os.path.join(gfx_dir, 'data', 'art'), + os.path.join(paths.get('data'), 'art')) + for tiles in glob.glob(os.path.join(paths.get('tilesets'), '*')): + shutil.copy(tiles, os.path.join(paths.get('data'), 'art')) - # Install colorscheme - if lnp.df_info.version >= '0.31.04': - colors.load_colors(os.path.join( - gfx_dir, 'data', 'init', 'colors.txt')) - else: - colors.load_colors(os.path.join( - gfx_dir, 'data', 'init', 'init.txt')) + patch_inits(gfx_dir) - # TwbT overrides - try: - os.remove(os.path.join(paths.get('init'), 'overrides.txt')) - except: - pass - try: - shutil.copyfile( - os.path.join(gfx_dir, 'data', 'init', 'overrides.txt'), - os.path.join(paths.get('init'), 'overrides.txt')) - except: - pass - except Exception: - sys.excepthook(*sys.exc_info()) - if os.path.isdir(gfx_dir): - dir_util.remove_tree(gfx_dir) - return False + # Install colorscheme + if lnp.df_info.version >= '0.31.04': + colors.load_colors(os.path.join( + gfx_dir, 'data', 'init', 'colors.txt')) else: - df.load_params() - if os.path.isdir(gfx_dir): - dir_util.remove_tree(gfx_dir) - return True + colors.load_colors(os.path.join( + gfx_dir, 'data', 'init', 'init.txt')) + + # TwbT overrides + try: + os.remove(os.path.join(paths.get('init'), 'overrides.txt')) + except: + pass + try: + shutil.copyfile( + os.path.join(gfx_dir, 'data', 'init', 'overrides.txt'), + os.path.join(paths.get('init'), 'overrides.txt')) + except: + pass + except Exception: + sys.excepthook(*sys.exc_info()) + retval = False + else: + retval = True if os.path.isdir(gfx_dir): dir_util.remove_tree(gfx_dir) df.load_params() - return None + return retval def validate_pack(pack): """Checks for presence of all required files for a pack install.""" result = True gfx_dir = os.path.join(paths.get('graphics'), pack) result &= os.path.isdir(gfx_dir) - result &= os.path.isdir(os.path.join(gfx_dir, 'raw', 'graphics')) result &= os.path.isdir(os.path.join(gfx_dir, 'data', 'init')) result &= os.path.isdir(os.path.join(gfx_dir, 'data', 'art')) result &= os.path.isfile(os.path.join(gfx_dir, 'data', 'init', 'init.txt')) @@ -141,10 +132,8 @@ def validate_pack(pack): return result def patch_inits(gfx_dir): - """ - Installs init files from a graphics pack by selectively changing - specific fields. All settings outside of the mentioned fields are - preserved. + """Installs init files from a graphics pack by selectively changing + specific fields. All settings but the mentioned fields are preserved. """ d_init_fields = [ 'WOUND_COLOR_NONE', 'WOUND_COLOR_MINOR', @@ -274,8 +263,7 @@ def current_tilesets(): return (lnp.settings.FONT, None) def install_tilesets(font, graphicsfont): - """ - Installs the provided tilesets as [FULL]FONT and GRAPHICS_[FULL]FONT. + """Installs the provided tilesets as [FULL]FONT and GRAPHICS_[FULL]FONT. To skip either option, use None as the parameter. """ if font is not None and os.path.isfile( diff --git a/core/mods.py b/core/mods.py index 137be8c..2ddbe6a 100644 --- a/core/mods.py +++ b/core/mods.py @@ -1,6 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -"""""" +"""Mod Pack management and merging tools.""" from __future__ import print_function, unicode_literals, absolute_import import os, shutil, glob @@ -33,8 +33,7 @@ def install_mods(): os.path.join(paths.get('df'), 'raw')) def do_merge_seq(mod_text, vanilla_text, gen_text): - """Merges sequences of lines. Returns empty string if a line changed by - the mod has been changed by a previous mod, or merged lines otherwise. + """Merges sequences of lines. Params: mod_text @@ -42,7 +41,7 @@ def do_merge_seq(mod_text, vanilla_text, gen_text): vanilla_text The lines of the corresponding vanilla file. gen_text - The lines of the previously merged file. + The lines of the previously merged file or files. Returns: tuple(status, lines); status is 0/'ok' or 2/'overlap merged' From 52e6b8ff73c2fdc54edd251348260a09fbdb6dbb Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Mon, 8 Dec 2014 20:01:39 +1100 Subject: [PATCH 28/29] Use new paths function in mods-specific files. Improve some tooltips on mods tab; there will be some later polishing anyway. --- core/baselines.py | 13 ++++++------- core/graphics.py | 26 +++++++++++++------------- core/mods.py | 41 +++++++++++++++++++---------------------- tkgui/mods.py | 6 +++--- 4 files changed, 41 insertions(+), 45 deletions(-) diff --git a/core/baselines.py b/core/baselines.py index 77a064e..6e7f995 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -23,8 +23,8 @@ def find_vanilla_raws(): return None prepare_baselines() version = 'df_' + str(lnp.df_info.version)[2:].replace('.', '_') - if os.path.isdir(os.path.join(paths.get('baselines'), version, 'raw')): - return os.path.join(paths.get('baselines'), version, 'raw') + if os.path.isdir(paths.get('baselines', version, 'raw')): + return paths.get('baselines', version, 'raw') update.download_df_baseline() return False @@ -35,7 +35,7 @@ def prepare_baselines(): version = os.path.basename(item) for s in ['_win', '_legacy', '_s', '.zip']: version = version.replace(s, '') - f = os.path.join(paths.get('baselines'), version) + f = paths.get('baselines', version) if not os.path.isdir(f): zipfile.ZipFile(item).extractall(f) simplify_pack(version, 'baselines') @@ -62,7 +62,7 @@ def simplify_pack(pack, folder): valid_dirs = ('graphics', 'mods', 'baselines') if not folder in valid_dirs: return False - pack = os.path.join(paths.get(folder), pack) + pack = paths.get(folder, pack) files_before = sum(len(f) for (_, _, f) in os.walk(pack)) if files_before == 0: return None @@ -118,7 +118,7 @@ def remove_vanilla_raws_from_pack(pack, folder): folder The parent folder of the pack (either 'mods' or 'graphics') """ - raw_folder = os.path.join(paths.get(folder), pack, 'raw') + raw_folder = paths.get(folder, pack, 'raw') vanilla_raw_folder = find_vanilla_raws() for root, _, files in os.walk(raw_folder): for f in files: @@ -144,9 +144,8 @@ def remove_empty_dirs(pack, folder): folder The parent folder of the pack (either 'mods' or 'graphics') """ - pack = os.path.join(paths.get(folder), pack) for _ in range(3): # only catches the lowest level each iteration - for root, dirs, files in os.walk(pack): + for root, dirs, files in os.walk(paths.get(folder, pack)): if not dirs and not files: os.rmdir(root) diff --git a/core/graphics.py b/core/graphics.py index 3dfb77d..b9ac8dd 100644 --- a/core/graphics.py +++ b/core/graphics.py @@ -65,20 +65,20 @@ def install_graphics(pack): try: # Delete old graphics - if os.path.isdir(paths.get('df', 'raw', 'graphics'): - dir_util.remove_tree(paths.get('df', 'raw', 'graphics') + if os.path.isdir(paths.get('df', 'raw', 'graphics')): + dir_util.remove_tree(paths.get('df', 'raw', 'graphics')) # Copy new raws dir_util.copy_tree(os.path.join(gfx_dir, 'raw'), - paths.get('df', 'raw') + paths.get('df', 'raw')) #Copy art if os.path.isdir(os.path.join(paths.get('data'), 'art')): - dir_util.remove_tree(paths.get('data', 'art') + dir_util.remove_tree(paths.get('data', 'art')) dir_util.copy_tree(os.path.join(gfx_dir, 'data', 'art'), - paths.get('data', 'art') - for tiles in glob.glob(paths.get('tilesets', '*'): - shutil.copy(tiles, paths.get('data', 'art') + paths.get('data', 'art')) + for tiles in glob.glob(paths.get('tilesets', '*')): + shutil.copy(tiles, paths.get('data', 'art')) patch_inits(gfx_dir) @@ -92,13 +92,13 @@ def install_graphics(pack): # TwbT overrides try: - os.remove(paths.get('init', 'overrides.txt') + os.remove(paths.get('init', 'overrides.txt')) except: pass try: shutil.copyfile( os.path.join(gfx_dir, 'data', 'init', 'overrides.txt'), - paths.get('init', 'overrides.txt') + paths.get('init', 'overrides.txt')) except: pass except Exception: @@ -244,9 +244,9 @@ def open_tilesets(): def read_tilesets(): """Returns a list of tileset files.""" - files = glob.glob(paths.get('data', 'art', '*.bmp') + files = glob.glob(paths.get('data', 'art', '*.bmp')) if 'legacy' not in lnp.df_info.variations: - files += glob.glob(paths.get('data', 'art', '*.png') + files += glob.glob(paths.get('data', 'art', '*.png')) return tuple([os.path.basename(o) for o in files if not ( o.endswith('mouse.png') or o.endswith('mouse.bmp') or o.endswith('shadows.png'))]) @@ -261,11 +261,11 @@ def install_tilesets(font, graphicsfont): """Installs the provided tilesets as [FULL]FONT and GRAPHICS_[FULL]FONT. To skip either option, use None as the parameter. """ - if font is not None and os.path.isfile(paths.get('data', 'art', font): + if font is not None and os.path.isfile(paths.get('data', 'art', font)): df.set_option('FONT', font) df.set_option('FULLFONT', font) if (lnp.settings.version_has_option('GRAPHICS_FONT') and graphicsfont is not None and os.path.isfile( - paths.get('data', 'art', graphicsfont)): + paths.get('data', 'art', graphicsfont))): df.set_option('GRAPHICS_FONT', graphicsfont) df.set_option('GRAPHICS_FULLFONT', graphicsfont) diff --git a/core/mods.py b/core/mods.py index 2ddbe6a..1a42080 100644 --- a/core/mods.py +++ b/core/mods.py @@ -12,7 +12,7 @@ def read_mods(): """Returns a list of mod packs""" return [os.path.basename(o) for o in - glob.glob(os.path.join(paths.get('mods'), '*')) + glob.glob(paths.get('mods', '*')) if os.path.isdir(o)] def simplify_mods(): @@ -149,19 +149,19 @@ def merge_a_mod(mod): """ if not baselines.find_vanilla_raws(): return 3 # no baseline; caught properly earlier - mod_raw_folder = os.path.join(paths.get('mods'), mod, 'raw') + mod_raw_folder = paths.get('mods', mod, 'raw') if not os.path.isdir(mod_raw_folder): return 2 status = merge_raw_folders(mod_raw_folder, baselines.find_vanilla_raws()) if status < 3: - with open(os.path.join(paths.get('baselines'), 'temp', 'raw', - 'installed_raws.txt'), 'a') as log: + with open(paths.get('baselines', 'temp', 'raw', 'installed_raws.txt'), + 'a') as log: log.write(mod + '\n') return status def merge_raw_folders(mod_raw_folder, vanilla_raw_folder): """Merge the specified folders, output going in LNP/Baselines/temp/raw""" - mixed_raw_folder = os.path.join(paths.get('baselines'), 'temp', 'raw') + mixed_raw_folder = paths.get('baselines', 'temp', 'raw') status = 0 for file_tuple in os.walk(mod_raw_folder): for item in file_tuple[2]: @@ -185,12 +185,12 @@ def clear_temp(): if not baselines.find_vanilla_raws(): # TODO: add user warning re: missing baseline, download return None - if os.path.exists(os.path.join(paths.get('baselines'), 'temp')): - shutil.rmtree(os.path.join(paths.get('baselines'), 'temp')) + if os.path.exists(paths.get('baselines', 'temp')): + shutil.rmtree(paths.get('baselines', 'temp')) shutil.copytree(baselines.find_vanilla_raws(), - os.path.join(paths.get('baselines'), 'temp', 'raw')) - with open(os.path.join(paths.get('baselines'), 'temp', 'raw', - 'installed_raws.txt'), 'w') as log: + paths.get('baselines', 'temp', 'raw')) + with open(paths.get('baselines', 'temp', 'raw', 'installed_raws.txt'), + 'w') as log: log.write('# List of raws merged by PyLNP:\n' + os.path.basename( os.path.dirname(baselines.find_vanilla_raws())) + '\n') @@ -202,15 +202,14 @@ def make_mod_from_installed_raws(name): * If `installed_raws.txt` is not present, compare to vanilla * Otherwise, rebuild as much as possible then compare to installed """ - if os.path.isdir(os.path.join(paths.get('mods'), name)): + if os.path.isdir(paths.get('mods', name)): return False if get_installed_mods_from_log(): clear_temp() for mod in get_installed_mods_from_log(): merge_a_mod(mod) - reconstruction = os.path.join(paths.get('baselines'), 'temp2', 'raw') - shutil.copytree(os.path.join(paths.get('baselines'), 'temp', 'raw'), - reconstruction) + reconstruction = paths.get('baselines', 'temp2', 'raw') + shutil.copytree(paths.get('baselines', 'temp', 'raw'), reconstruction) else: reconstruction = baselines.find_vanilla_raws() if not reconstruction: @@ -218,25 +217,23 @@ def make_mod_from_installed_raws(name): return None clear_temp() - merge_raw_folders(reconstruction, os.path.join(paths.get('df'), 'raw')) + merge_raw_folders(reconstruction, paths.get('df', 'raw')) baselines.simplify_pack('temp', 'baselines') baselines.remove_vanilla_raws_from_pack('temp', 'baselines') baselines.remove_empty_dirs('temp', 'baselines') - if os.path.isdir(os.path.join(paths.get('baselines'), 'temp2')): - shutil.rmtree(os.path.join(paths.get('baselines'), 'temp2')) + if os.path.isdir(paths.get('baselines', 'temp2')): + shutil.rmtree(paths.get('baselines', 'temp2')) - if os.path.isdir(os.path.join(paths.get('baselines'), 'temp')): - shutil.copytree(os.path.join(paths.get('baselines'), 'temp'), - os.path.join(paths.get('mods'), name)) + if os.path.isdir(paths.get('baselines', 'temp')): + shutil.copytree(paths.get('baselines', 'temp'), paths.get('mods', name)) return True return False def get_installed_mods_from_log(): """Return best mod load order to recreate installed with available.""" - logged = read_installation_log(os.path.join(paths.get('df'), - 'raw', 'installed_raws.txt')) + logged = read_installation_log(paths.get('df', 'raw', 'installed_raws.txt')) return [mod for mod in logged if mod in read_mods()] def read_installation_log(log): diff --git a/tkgui/mods.py b/tkgui/mods.py index bbca7d5..b8b425b 100644 --- a/tkgui/mods.py +++ b/tkgui/mods.py @@ -74,12 +74,12 @@ def create_controls(self): f = controls.create_control_group(self, None, True) controls.create_trigger_button( - f, 'Simplify Mods', 'Simplify Mods', + f, 'Simplify Mods', 'Removes unnecessary and vanilla files.', self.simplify_mods).grid( row=0, column=0, sticky="nsew") controls.create_trigger_button( f, 'Install Mods', 'Copy merged mods to DF folder. ' - 'WARNING: do not combine with graphics. May be unstable.', + 'WARNING: do not combine with graphics. May cause problems.', self.install_mods).grid(row=0, column=1, sticky="nsew") controls.create_trigger_button( f, 'Create Mod from Installed', 'Creates a mod from unique changes ' @@ -189,7 +189,7 @@ def perform_merge(self): def install_mods(): """Replaces /raw with the contents LNP/Baselines/temp/raw""" if messagebox.askokcancel( - message=('Your graphics and raws will be changed.\n\n' + message=('Your graphics will be removed and raws changed.\n\n' 'The mod merging function is still in beta. This ' 'could break new worlds, or even cause crashes.\n\n'), title='Are you sure?'): From b5e3e6090d9ffff0795d50b4b85b7d58dd24afaa Mon Sep 17 00:00:00 2001 From: PeridexisErrant Date: Sat, 13 Dec 2014 09:46:22 +1100 Subject: [PATCH 29/29] Add baselines.set_auto_download(), fix string in df.get_archive_name --- core/baselines.py | 5 +++++ core/df.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/core/baselines.py b/core/baselines.py index 6e7f995..cf99731 100644 --- a/core/baselines.py +++ b/core/baselines.py @@ -41,6 +41,11 @@ def prepare_baselines(): simplify_pack(version, 'baselines') os.remove(item) +def set_auto_download(value): + """Sets the option for auto-download of baselines.""" + lnp.userconfig['downloadBaselines'] = value + lnp.userconfig.save_data() + def simplify_pack(pack, folder): """Removes unnecessary files from LNP//. Necessary files means: diff --git a/core/df.py b/core/df.py index 36542c1..2f003f0 100644 --- a/core/df.py +++ b/core/df.py @@ -209,7 +209,7 @@ def get_archive_name(self): """Return the filename of the download for this version. Always windows, for comparison of raws in baselines. Prefer small and SDL releases when available.""" - base = 'df_' + self.version[2:].replace('.', '_') + base = 'df_' + str(self.version)[2:].replace('.', '_') if self.version >= '0.31.13': return base + '_win_s.zip' if self.version >= '0.31.05':