From 8cc7f927ec4f2ea4e81c3e7bcbdf633c986f4907 Mon Sep 17 00:00:00 2001 From: Simon Lusenc Date: Wed, 23 Mar 2016 12:47:26 +0100 Subject: [PATCH] Release - 1.3 --- addon/io_scs_tools/__init__.py | 2 +- addon/io_scs_tools/exp/__init__.py | 22 +-- addon/io_scs_tools/exp/pip/curve.py | 3 + addon/io_scs_tools/exp/pip/exporter.py | 30 ++-- addon/io_scs_tools/exp/pix.py | 4 +- addon/io_scs_tools/imp/pia.py | 5 +- addon/io_scs_tools/imp/pix.py | 5 +- .../internals/connections/core.py | 7 +- .../io_scs_tools/internals/containers/mat.py | 7 +- .../io_scs_tools/internals/containers/pix.py | 13 +- .../io_scs_tools/internals/containers/sii.py | 7 +- .../io_scs_tools/internals/containers/tobj.py | 4 +- .../internals/persistent/initialization.py | 3 +- .../internals/preview_models/__init__.py | 6 +- addon/io_scs_tools/operators/scene.py | 132 ++++++++++++++--- addon/io_scs_tools/operators/wm.py | 14 +- addon/io_scs_tools/properties/material.py | 2 +- addon/io_scs_tools/ui/shared.py | 4 + addon/io_scs_tools/utils/material.py | 4 +- addon/io_scs_tools/utils/name.py | 5 +- addon/io_scs_tools/utils/object.py | 14 +- addon/io_scs_tools/utils/path.py | 25 +++- addon/io_scs_tools/utils/printout.py | 136 +++++++++++++++--- 23 files changed, 348 insertions(+), 106 deletions(-) diff --git a/addon/io_scs_tools/__init__.py b/addon/io_scs_tools/__init__.py index 1097951..7107fab 100644 --- a/addon/io_scs_tools/__init__.py +++ b/addon/io_scs_tools/__init__.py @@ -22,7 +22,7 @@ "name": "SCS Tools", "description": "Setup models, Import-Export SCS data format", "author": "Simon Lusenc (50keda), Milos Zajic (4museman)", - "version": (1, 2, "bfa9481"), + "version": (1, 2, "4a0a4dd"), "blender": (2, 75, 0), "location": "File > Import-Export", "wiki_url": "https://github.com/SCSSoftware/BlenderTools/wiki", diff --git a/addon/io_scs_tools/exp/__init__.py b/addon/io_scs_tools/exp/__init__.py index 3b0a93f..f53ac6a 100644 --- a/addon/io_scs_tools/exp/__init__.py +++ b/addon/io_scs_tools/exp/__init__.py @@ -71,16 +71,16 @@ def batch_export(operator_instance, init_obj_list, menu_filepath=None): # MAKE FINAL FILEPATH if menu_filepath: - filepath = menu_filepath + filepath = _path_utils.readable_norm(menu_filepath) filepath_message = "Export path selected in file browser:\n\t \"" + filepath + "\"" elif custom_filepath: - filepath = custom_filepath + filepath = _path_utils.readable_norm(custom_filepath) filepath_message = "Custom export path used for \"" + root_object.name + "\" is:\n\t \"" + filepath + "\"" else: - filepath = global_filepath + filepath = _path_utils.readable_norm(global_filepath) filepath_message = "Default export path used for \"" + root_object.name + "\":\n\t \"" + filepath + "\"" - scs_project_path = _get_scs_globals().scs_project_path + scs_project_path = _path_utils.readable_norm(_get_scs_globals().scs_project_path) if os.path.isdir(filepath) and _path_utils.startswith(filepath, scs_project_path) and scs_project_path != "": # EXPORT ENTRY POINT @@ -106,18 +106,22 @@ def batch_export(operator_instance, init_obj_list, menu_filepath=None): return {'CANCELLED'} if not lprint("\nI Export procces completed, summaries are printed below!", report_errors=True, report_warnings=True): - operator_instance.report({'INFO'}, "Export successfully completed!") + operator_instance.report({'INFO'}, "Export successfully completed, exported %s game object(s)!" % len(scs_game_objects_exported)) bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', abort=True) # abort 3d view reporting operator if len(scs_game_objects_exported) > 0: - print("\n\nEXPORTED GAME OBJECTS (" + str(len(scs_game_objects_exported)) + "):\n" + "=" * 26) + message = "EXPORTED GAME OBJECTS (" + str(len(scs_game_objects_exported)) + "):\n\t " + "=" * 26 + "\n\t " for scs_game_object_export_message in scs_game_objects_exported: - print(scs_game_object_export_message) + message += scs_game_object_export_message + "\n\t " + message += "=" * 26 + lprint("I " + message) if len(scs_game_objects_rejected) > 0: - print("\n\nREJECTED GAME OBJECTS (" + str(len(scs_game_objects_rejected)) + "):\n" + "=" * 26) + message = "REJECTED GAME OBJECTS (" + str(len(scs_game_objects_rejected)) + "):\n\t " + "=" * 26 + "\n\t " for scs_game_object_export_message in scs_game_objects_rejected: - print(scs_game_object_export_message) + message += scs_game_object_export_message + "\n\t " + message += "=" * 26 + lprint("I " + message) if len(scs_game_objects_exported) + len(scs_game_objects_rejected) == 0: message = "Nothing to export! Please set at least one 'SCS Root Object'." diff --git a/addon/io_scs_tools/exp/pip/curve.py b/addon/io_scs_tools/exp/pip/curve.py index 18523f2..5683496 100644 --- a/addon/io_scs_tools/exp/pip/curve.py +++ b/addon/io_scs_tools/exp/pip/curve.py @@ -85,6 +85,9 @@ def prepare_curves(curves_l): msg = msg[:-2] + "]" lprint(msg) + def __lt__(self, other): + return self.__index < other.get_index() + def __eq__(self, other): return self.__index == other.get_index() diff --git a/addon/io_scs_tools/exp/pip/exporter.py b/addon/io_scs_tools/exp/pip/exporter.py index bcacea3..66f6801 100644 --- a/addon/io_scs_tools/exp/pip/exporter.py +++ b/addon/io_scs_tools/exp/pip/exporter.py @@ -1,4 +1,5 @@ from os import path +from collections import OrderedDict from mathutils import Vector, Quaternion from io_scs_tools.consts import PrefabLocators as _PL_consts from io_scs_tools.exp.pip.curve import Curve @@ -27,13 +28,14 @@ def __sort_locators_by_type__(locator_list): :rtype: tuple[dict[str, bpy.types.Object]] """ - control_node_locs = {} - nav_point_locs = {} - sign_locs = {} - spawn_point_locs = {} - semaphore_locs = {} - map_point_locs = {} - trigger_point_locs = {} + # NOTE: doing all dictionaries ordered to have same export result on unchanged data + control_node_locs = OrderedDict() + nav_point_locs = OrderedDict() + sign_locs = OrderedDict() + spawn_point_locs = OrderedDict() + semaphore_locs = OrderedDict() + map_point_locs = OrderedDict() + trigger_point_locs = OrderedDict() for loc in locator_list: if loc.scs_props.locator_prefab_type == "Control Node": @@ -148,9 +150,9 @@ def execute(dirpath, filename, prefab_locator_list, offset_matrix, used_terrain_ pip_header = Header(2, filename) pip_global = Globall() - pip_nodes = {} + pip_nodes = OrderedDict() """:type: dict[int,Node]""" - pip_curves = {} + pip_curves = OrderedDict() """:type: dict[int, Curve]""" pip_signs = [] """:type: list[Sign]""" @@ -158,11 +160,11 @@ def execute(dirpath, filename, prefab_locator_list, offset_matrix, used_terrain_ """:type: list[SpawnPoint]""" pip_semaphores = [] """:type: list[Semaphore]""" - pip_map_points = {} + pip_map_points = OrderedDict() """:type: dict[str, MapPoint]""" - pip_trigger_points = {} + pip_trigger_points = OrderedDict() """:type: dict[str, TriggerPoint]""" - pip_intersections = [{}, {}, {}] + pip_intersections = [OrderedDict(), OrderedDict(), OrderedDict()] """:type: list[dict[str, list[Intersection]]]""" # nodes creation @@ -382,8 +384,8 @@ def execute(dirpath, filename, prefab_locator_list, offset_matrix, used_terrain_ TriggerPoint.prepare_trigger_points(pip_trigger_points.values()) # intersections creation - for c0_i, c0 in enumerate(pip_curves.values()): - for c1_i, c1 in enumerate(pip_curves.values()): + for c0_i, c0 in enumerate(sorted(pip_curves.values())): + for c1_i, c1 in enumerate(sorted(pip_curves.values())): if c1_i <= c0_i: # only search each pair of curves once continue diff --git a/addon/io_scs_tools/exp/pix.py b/addon/io_scs_tools/exp/pix.py index e1e12d7..b0f0dd0 100644 --- a/addon/io_scs_tools/exp/pix.py +++ b/addon/io_scs_tools/exp/pix.py @@ -115,6 +115,8 @@ def export(dirpath, root_object, game_object_list): context = bpy.context context.window.cursor_modal_set('WAIT') + lprint("I Export started for: %r on: %s", (root_object.name, time.strftime("%b %d, %Y at %H:%M:%S"))) + # TRANSITIONAL STRUCTURES terrain_points = TerrainPntsTrans() parts = PartsTrans() @@ -216,6 +218,6 @@ def export(dirpath, root_object, game_object_list): # FINAL FEEDBACK context.window.cursor_modal_restore() if export_success: - lprint('\nI Export completed in %.3f sec. Files were saved to folder:\n\t %r\n', (time.time() - t, dirpath)) + lprint("I Export completed for: %r in %.3f seconds.\n", (root_object.name, time.time() - t)) return True diff --git a/addon/io_scs_tools/imp/pia.py b/addon/io_scs_tools/imp/pia.py index 92a8f08..86bebd4 100644 --- a/addon/io_scs_tools/imp/pia.py +++ b/addon/io_scs_tools/imp/pia.py @@ -28,6 +28,7 @@ from io_scs_tools.utils.printout import lprint from io_scs_tools.utils import animation as _animation_utils from io_scs_tools.utils import convert as _convert_utils +from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils import get_scs_globals as _get_scs_globals @@ -212,7 +213,7 @@ def load(root_object, pia_files, armature, pis_filepath=None, bones=None): if os.path.isfile(pia_skeleton): bones = _pis.load(pia_skeleton, armature, get_only=True) else: - lprint("\nE The filepath %r doesn't exist!", (pia_skeleton.replace("\\", "/"),)) + lprint("\nE The filepath %r doesn't exist!", (_path_utils.readable_norm(pia_skeleton),)) else: lprint(str("E Animation doesn't match the skeleton. Animation won't be loaded!\n\t " @@ -222,7 +223,7 @@ def load(root_object, pia_files, armature, pis_filepath=None, bones=None): lprint('I ++ "%s" IMPORTING animation data...', (os.path.basename(pia_filepath),)) pia_container = _pix_container.get_data_from_file(pia_filepath, ind) if not pia_container: - lprint('\nE File "%s" is empty!', (pia_filepath.replace("\\", "/"),)) + lprint('\nE File "%s" is empty!', (_path_utils.readable_norm(pia_filepath),)) continue # TEST PRINTOUTS diff --git a/addon/io_scs_tools/imp/pix.py b/addon/io_scs_tools/imp/pix.py index 7064fd2..2e293a0 100644 --- a/addon/io_scs_tools/imp/pix.py +++ b/addon/io_scs_tools/imp/pix.py @@ -33,6 +33,7 @@ from io_scs_tools.utils import material as _material_utils from io_scs_tools.utils import name as _name_utils from io_scs_tools.utils import object as _object_utils +from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils.printout import lprint @@ -304,7 +305,7 @@ def load(context, filepath): if scs_globals.import_pim_file or scs_globals.import_pis_file: if filepath: if os.path.isfile(filepath): - lprint('\nD PIM filepath:\n %s', (filepath.replace("\\", "/"),)) + lprint('\nD PIM filepath:\n %s', (_path_utils.readable_norm(filepath),)) result, objects, locators, armature, skeleton, mats_info = _pim.load( context, filepath, @@ -312,7 +313,7 @@ def load(context, filepath): ) # print(' armature:\n%s\n skeleton:\n%s' % (str(armature), str(skeleton))) else: - lprint('\nI No file found at %r!' % (filepath.replace("\\", "/"),)) + lprint('\nI No file found at %r!' % (_path_utils.readable_norm(filepath),)) else: lprint('\nI No filepath provided!') diff --git a/addon/io_scs_tools/internals/connections/core.py b/addon/io_scs_tools/internals/connections/core.py index f55dc9e..810b428 100644 --- a/addon/io_scs_tools/internals/connections/core.py +++ b/addon/io_scs_tools/internals/connections/core.py @@ -20,6 +20,7 @@ import bpy import hashlib +from collections import OrderedDict from io_scs_tools.consts import ConnectionsStorage as _CS_consts from io_scs_tools.consts import PrefabLocators as _PL_consts from io_scs_tools.internals.connections import collector as _collector @@ -212,8 +213,8 @@ def gather_connections_upon_selected(data_block, loc_names): locators_refs = data[REFS][LOCATORS] conns_entries = data[REFS][CONNECTIONS][ENTRIES] - conns_to_draw = {} - for loc_name in loc_names: + conns_to_draw = OrderedDict() # use ordered one so export with unchanged data will be the same every time + for loc_name in sorted(loc_names): if loc_name in locators_refs: @@ -307,7 +308,7 @@ def get_connections(data_block, loc_name): locators_refs = data[REFS][LOCATORS] conns_entries = data[REFS][CONNECTIONS][ENTRIES] - connections = {} + connections = OrderedDict() # use ordered one so export with unchanged data will be the same every time if loc_name in locators_refs: loc_refs = locators_refs[loc_name] diff --git a/addon/io_scs_tools/internals/containers/mat.py b/addon/io_scs_tools/internals/containers/mat.py index 8e3c1e4..e1d1444 100644 --- a/addon/io_scs_tools/internals/containers/mat.py +++ b/addon/io_scs_tools/internals/containers/mat.py @@ -19,6 +19,7 @@ # Copyright (C) 2013-2014: SCS Software import os +from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils.printout import lprint from io_scs_tools.internals.containers.parsers import mat as _mat @@ -97,15 +98,15 @@ def get_data_from_file(filepath): if data_dict: if len(data_dict) < 1: - lprint('\nI MAT file "%s" is empty!', (str(filepath).replace("\\", "/"),)) + lprint('\nI MAT file "%s" is empty!', (_path_utils.readable_norm(filepath),)) return None container = MatContainer(data_dict, effect) else: - lprint('\nI MAT file "%s" is empty!', (str(filepath).replace("\\", "/"),)) + lprint('\nI MAT file "%s" is empty!', (_path_utils.readable_norm(filepath),)) return None else: - lprint('\nW Invalid MAT file path %r!', (str(filepath).replace("\\", "/"),)) + lprint('\nW Invalid MAT file path %r!', (_path_utils.readable_norm(filepath),)) else: lprint('\nI No MAT file path provided!') diff --git a/addon/io_scs_tools/internals/containers/pix.py b/addon/io_scs_tools/internals/containers/pix.py index 244a129..851b0dd 100644 --- a/addon/io_scs_tools/internals/containers/pix.py +++ b/addon/io_scs_tools/internals/containers/pix.py @@ -23,8 +23,9 @@ from mathutils import Vector from io_scs_tools.internals.containers.parsers import pix as _pix_parser from io_scs_tools.internals.containers.writers import pix as _pix_writer -from io_scs_tools.utils.printout import lprint from io_scs_tools.internals.structure import SectionData as _SectionData +from io_scs_tools.utils import path as _path_utils +from io_scs_tools.utils.printout import lprint def fast_check_for_pia_skeleton(pia_filepath, skeleton): @@ -197,7 +198,7 @@ def get_data_from_file(filepath, ind, print_info=False): # print(' filepath: "%s"\n' % filepath) container, state = _pix_parser.read_data(filepath, ind, print_info) if len(container) < 1: - lprint('\nE File "%s" is empty!', (str(filepath).replace("\\", "/"),)) + lprint('\nE File "%s" is empty!', (_path_utils.readable_norm(filepath),)) return None # print_container(container) # TEST PRINTOUTS @@ -221,10 +222,14 @@ def write_data_to_file(container, filepath, ind, print_info=False): :rtype: bool """ + # convert filepath in readable form so when file writting will be logged + # path will be properly readable even on windows. Without mixed back and forward slashes. + filepath = _path_utils.readable_norm(filepath) + result = _pix_writer.write_data(container, filepath, ind, print_info=print_info) if result != {'FINISHED'}: - lprint('E Unable to export data into file:\n\t "%s"\nFor details check printouts above.', (str(filepath).replace("\\", "/"),)) + lprint("E Unable to export data into file:\n\t %r\n\t For details check printouts above.", (filepath,)) return False else: - lprint('I File created!') + lprint("I File created!") return True diff --git a/addon/io_scs_tools/internals/containers/sii.py b/addon/io_scs_tools/internals/containers/sii.py index f1e211b..aa6b22b 100644 --- a/addon/io_scs_tools/internals/containers/sii.py +++ b/addon/io_scs_tools/internals/containers/sii.py @@ -19,6 +19,7 @@ # Copyright (C) 2013-2014: SCS Software import os +from io_scs_tools.utils import path as _path_utils from io_scs_tools.utils.printout import lprint from io_scs_tools.internals.containers.parsers import sii as _sii @@ -32,13 +33,13 @@ def get_data_from_file(filepath): container = _sii.parse_file(filepath) if container: if len(container) < 1: - lprint('D SII file "%s" is empty!', (str(filepath).replace("\\", "/"),)) + lprint('D SII file "%s" is empty!', (_path_utils.readable_norm(filepath),)) return None else: - lprint('D SII file "%s" is empty!', (str(filepath).replace("\\", "/"),)) + lprint('D SII file "%s" is empty!', (_path_utils.readable_norm(filepath),)) return None else: - lprint('W Invalid SII file path %r!', (str(filepath).replace("\\", "/"),)) + lprint('W Invalid SII file path %r!', (_path_utils.readable_norm(filepath),)) else: lprint('I No SII file path provided!') diff --git a/addon/io_scs_tools/internals/containers/tobj.py b/addon/io_scs_tools/internals/containers/tobj.py index 8b8954b..57ff8ad 100644 --- a/addon/io_scs_tools/internals/containers/tobj.py +++ b/addon/io_scs_tools/internals/containers/tobj.py @@ -283,7 +283,7 @@ def read_data_from_file(cls, filepath, skip_validation=False): return None if not (os.path.isfile(filepath) and filepath.lower().endswith(".tobj")): - lprint("W Invalid TOBJ file path %r!", (str(filepath).replace("\\", "/"),)) + lprint("W Invalid TOBJ file path %r!", (_path_utils.readable_norm(filepath),)) return None records = _tobj.parse_file(filepath) @@ -291,7 +291,7 @@ def read_data_from_file(cls, filepath, skip_validation=False): records_iter = iter(records) if records is None or records_len <= 0: - lprint("I TOBJ file %r is empty!", (os.path.normpath(filepath),)) + lprint("I TOBJ file %r is empty!", (_path_utils.readable_norm(filepath),)) return None container = cls() diff --git a/addon/io_scs_tools/internals/persistent/initialization.py b/addon/io_scs_tools/internals/persistent/initialization.py index db63bb4..a1ef3fa 100644 --- a/addon/io_scs_tools/internals/persistent/initialization.py +++ b/addon/io_scs_tools/internals/persistent/initialization.py @@ -21,6 +21,7 @@ import bpy import os from bpy.app.handlers import persistent +from io_scs_tools import get_tools_version from io_scs_tools.internals import preview_models as _preview_models from io_scs_tools.internals.callbacks import open_gl as _open_gl_callback from io_scs_tools.internals.containers import config as _config_container @@ -49,7 +50,7 @@ def initialise_scs_dict(scene): # SCREEN CHECK... if bpy.context.screen: - lprint("I Initialization of SCS scene") + lprint("I Initialization of SCS scene, BT version: " + get_tools_version()) # NOTE: covers: start-up, reload, enable/disable and it should be immediately removed # from handlers as soon as it's executed for the first time diff --git a/addon/io_scs_tools/internals/preview_models/__init__.py b/addon/io_scs_tools/internals/preview_models/__init__.py index 0abe105..771a4fa 100644 --- a/addon/io_scs_tools/internals/preview_models/__init__.py +++ b/addon/io_scs_tools/internals/preview_models/__init__.py @@ -119,10 +119,12 @@ def load(locator): if filepath.lower().endswith(".pim"): abs_filepath = _path_utils.get_abs_path(filepath, skip_mod_check=True) if not os.path.isfile(abs_filepath): - lprint("W Locator %r has invalid path to Preview Model PIM file: %r", (locator.name, abs_filepath.replace("\\", "/"))) + lprint("W Locator %r has invalid path to Preview Model PIM file: %r", + (locator.name, _path_utils.readable_norm(abs_filepath))) load_model = False else: - lprint("W Locator %r has invalid path to Preview Model PIM file: %r", (locator.name, filepath.replace("\\", "/"))) + lprint("W Locator %r has invalid path to Preview Model PIM file: %r", + (locator.name, _path_utils.readable_norm(filepath))) load_model = False else: load_model = False diff --git a/addon/io_scs_tools/operators/scene.py b/addon/io_scs_tools/operators/scene.py index 50d212c..952b3a1 100644 --- a/addon/io_scs_tools/operators/scene.py +++ b/addon/io_scs_tools/operators/scene.py @@ -20,9 +20,9 @@ import bpy import os -import platform import subprocess import shutil +from sys import platform from bpy.props import StringProperty, CollectionProperty, EnumProperty, IntProperty, BoolProperty from io_scs_tools.consts import ConvHlpr as _CONV_HLPR_consts from io_scs_tools.utils import object as _object_utils @@ -901,9 +901,7 @@ def execute(self, context): main_path = _get_scs_globals().conv_hlpr_converters_path - system_type = platform.system() - - if system_type == "Linux": + if platform == "linux": # Linux if os.system("command -v wineconsole") == 0: command = ["wineconsole " + os.path.join(main_path, "convert.cmd")] @@ -911,14 +909,33 @@ def execute(self, context): self.report({'ERROR'}, "Conversion aborted! Please install WINE, it's required to run conversion tools on Linux!") return {'CANCELLED'} - elif system_type == "Windows": + elif platform == "darwin": # Mac OS X + + # NOTE: we are assuming that user installed wine as we did through easiest way: + # downloading winebottler and then just drag&drop to applications + wineconsole_path = "/Applications/Wine.app/Contents/Resources/bin/wineconsole" + + if os.system("command -v wineconsole") == 0: + + command = ["wineconsole " + os.path.join(main_path, "convert.cmd")] + + elif os.path.isfile(wineconsole_path): + + command = [wineconsole_path + " " + os.path.join(main_path, "convert.cmd")] + + else: + self.report({'ERROR'}, "Conversion aborted! Please install at least Wine application from WineBottler, " + "it's required to run conversion tools on Mac OS X!") + return {'CANCELLED'} + + elif platform == "win32": # Windows command = 'cmd /C ""' + os.path.join(main_path, "convert.cmd") + '""' command = command.replace("\\", "/") else: - self.report({'ERROR'}, "Unsupported OS type! Make sure you are running either Linux or Windows!") + self.report({'ERROR'}, "Unsupported OS type! Make sure you are running either Mac OS X, Linux or Windows!") return {'CANCELLED'} # try to run conversion tools @@ -1002,21 +1019,72 @@ class FindGameModFolder(bpy.types.Operator): ) ) + @staticmethod + def ensure_mod_folder(game_home_path): + """Gets game home folder, creates "mod" directory inside if not yet present and returns full path to mod folder. + + :param game_home_path: path to game home folder + :type game_home_path: str + :return: full path to game mod folder + :rtype: str + """ + + game_mod_path = game_home_path + "/mod" + if not os.path.isdir(game_mod_path): + os.mkdir(game_mod_path) + + return os.path.normpath(game_mod_path) + + @staticmethod + def get_platform_depended_game_path(): + """Gets platform dependent game path as root directory for storing SCS games user data. + + :return: game path or empty string if OS is neither Windows, Linux or Mac OS X + :rtype: str + """ + + game_path = "" + + if platform == "linux": # Linux + + game_path = os.path.expanduser("~/.local/share") + + elif platform == "darwin": # Mac OS X + + game_path = os.path.expanduser("~/Library/Application Support") + + elif platform == "win32": # Windows + + try: + import winreg + + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders") as key: + + personal = winreg.QueryValueEx(key, "Personal") + game_path = str(personal[0]) + + except OSError as e: + import traceback + + lprint("E Error while looking for My Documents in registry: %r", type(e).__name__) + traceback.print_exc() + + return game_path + def execute(self, context): scs_globals = _get_scs_globals() - # try Windows like installation - mod_dir = os.path.expanduser("~/" + self.game + "/mod") - if os.path.isdir(mod_dir): - scs_globals.conv_hlpr_mod_destination = mod_dir - return {'FINISHED'} + possible_game_homes = ( + os.path.expanduser("~"), # Windows like installation on Linux or Mac OS X + self.get_platform_depended_game_path(), + ) - # try Linux Steam like installation - mod_dir = os.path.expanduser("~/.local/share/" + self.game + "/mod") - if os.path.isdir(mod_dir): - scs_globals.conv_hlpr_mod_destination = mod_dir - return {'FINISHED'} + for possible_game_home in possible_game_homes: + game_home_path = os.path.join(possible_game_home, self.game) + if os.path.isdir(game_home_path): + scs_globals.conv_hlpr_mod_destination = self.ensure_mod_folder(game_home_path) + return {'FINISHED'} self.report({'WARNING'}, "Could not find '" + self.game + "' mod folder") return {'CANCELLED'} @@ -1125,3 +1193,35 @@ def execute(self, context): self.report({'INFO'}, "Packing done, mod packed to: '%s'" % mod_filepath) return {'FINISHED'} + + +class Log: + """ + Wraper class for better navigation in file + """ + + class CopyLogToClipboard(bpy.types.Operator): + bl_label = "Copy BT Log To Clipboard" + bl_idname = "scene.scs_copy_log" + bl_description = "Copies whole Blender Tools log to clipboard (log was captured since Blender startup)." + + def execute(self, context): + from io_scs_tools.utils.printout import get_log + + text = bpy.data.texts.new("SCS BT Log") + + override = { + 'window': bpy.context.window, + 'region': None, + 'area': None, + 'edit_text': text, + } + bpy.ops.text.insert(override, text=get_log()) + bpy.ops.text.select_all(override) + bpy.ops.text.copy(override) + + text.user_clear() + bpy.data.texts.remove(text) + + self.report({'INFO'}, "Blender Tools log copied to clipboard!") + return {'FINISHED'} diff --git a/addon/io_scs_tools/operators/wm.py b/addon/io_scs_tools/operators/wm.py index a141d6c..af08e1e 100644 --- a/addon/io_scs_tools/operators/wm.py +++ b/addon/io_scs_tools/operators/wm.py @@ -21,7 +21,6 @@ import bpy from bpy.props import StringProperty, BoolProperty - from io_scs_tools.utils import view3d as _view3d_utils from io_scs_tools.utils import info as _info_utils @@ -172,14 +171,11 @@ def invoke(self, context, event): # split message by new lines for line in self.message.split("\n"): - # adapt number of spaces by message type - space_count = 22 if line.startswith("WARNING") else 18 - - # remove tabulator simulated new lines from warnings and errors, written like: "\n\t " - line = line.replace("\t ", " " * space_count) + # remove tabulator simulated new lines from warnings and errors, written like: "\n\t " + line = line.replace("\t ", " " * 4) - # make sure to get rid of any other tabulators and change them for space eg: "INFO\t-" - line = line.replace("\t", " ") + # remove tabulator simulated empty space before warning or error line of summaries e.g "\t > " + line = line.replace("\t ", "") Show3DViewReport.__static_message_l.append(line) @@ -193,7 +189,7 @@ def invoke(self, context, event): class ShowDeveloperErrors(bpy.types.Operator): bl_label = "" - bl_description = "Show errors from stack" + bl_description = "Show errors from stack. This was intended to be used only from batch import/export scripts." bl_idname = "wm.show_dev_error_messages" def execute(self, context): diff --git a/addon/io_scs_tools/properties/material.py b/addon/io_scs_tools/properties/material.py index 8c310c8..b70334a 100644 --- a/addon/io_scs_tools/properties/material.py +++ b/addon/io_scs_tools/properties/material.py @@ -126,7 +126,7 @@ def __update_shader_texture_tobj_file__(self, context, tex_type): lprint("", report_warnings=-1, report_errors=-1) lprint("E Settings in TOBJ file not saved; content is malformed or referencing none existing textures!\n\t " "Please check TOBJ's content in your favorite text editor; file path:\n\t %r", - (os.path.normpath(tobj_file),)) + (_path_utils.readable_norm(tobj_file),)) lprint("", report_warnings=1, report_errors=1) diff --git a/addon/io_scs_tools/ui/shared.py b/addon/io_scs_tools/ui/shared.py index 3d5435e..952023f 100644 --- a/addon/io_scs_tools/ui/shared.py +++ b/addon/io_scs_tools/ui/shared.py @@ -168,6 +168,10 @@ def draw_common_settings(layout, draw_config_storage_place=False): :type draw_config_storage_place: bool """ box4 = layout.box().column() + + row = box4.row(align=True) + row.operator("scene.scs_copy_log", icon="COPYDOWN") + row = box4.row(align=True) row.prop(_get_scs_globals(), 'dump_level', text="Log Level", icon='MOD_EXPLODE') diff --git a/addon/io_scs_tools/utils/material.py b/addon/io_scs_tools/utils/material.py index aeac854..b97088b 100644 --- a/addon/io_scs_tools/utils/material.py +++ b/addon/io_scs_tools/utils/material.py @@ -64,7 +64,7 @@ def get_texture(texture_path, texture_type, report_invalid=False): if abs_texture_filepath: lprint("W Texture can't be displayed as TOBJ file: %r is referencing non texture file:\n\t %r", - (texture_path, _path.normalize(abs_texture_filepath).replace("\\", "/"))) + (texture_path, _path.readable_norm(abs_texture_filepath))) else: @@ -152,7 +152,7 @@ def get_texture(texture_path, texture_type, report_invalid=False): lprint("", report_warnings=-1, report_errors=-1) lprint("W Texture can't be displayed as TOBJ file: %r is referencing non existing texture file:\n\t %r", - (texture_path, _path.normalize(abs_texture_filepath).replace("\\", "/"))) + (texture_path, _path.readable_norm(abs_texture_filepath))) if report_invalid: lprint("", report_warnings=1, report_errors=1) diff --git a/addon/io_scs_tools/utils/name.py b/addon/io_scs_tools/utils/name.py index 8477813..94953bf 100644 --- a/addon/io_scs_tools/utils/name.py +++ b/addon/io_scs_tools/utils/name.py @@ -65,11 +65,12 @@ def tokenize_name(name, default_name="default"): """ Takes a string and returns it as a valid token. :type name: str + :type default_name: str """ name = name.lower() # lower case # strip of Blender naming convention of double objects .XXX - if re.match(".+\.\d{3}", name): + if re.match(".+(\.\d{3})$", name): name = name[:-4] new_name = "" @@ -82,4 +83,4 @@ def tokenize_name(name, default_name="default"): elif len(new_name) == 0: new_name = default_name - return new_name \ No newline at end of file + return new_name diff --git a/addon/io_scs_tools/utils/object.py b/addon/io_scs_tools/utils/object.py index 1459ab5..bcdc3b7 100644 --- a/addon/io_scs_tools/utils/object.py +++ b/addon/io_scs_tools/utils/object.py @@ -180,16 +180,20 @@ def sort_out_game_objects_for_export(objects): # PRINTOUTS dump_level = int(_get_scs_globals().dump_level) if dump_level > 2: - lprint('\nD Rejected Objects:') + message = "D Rejected Objects:\n\t " for obj in rejected_objects: - print(' - %r rejected' % obj.name) + message += ' - %r rejected\n\t ' % obj.name + + lprint(message) if dump_level > 2: - lprint("\nD 'SCS Game Objects' to export:") + message = "D 'SCS Game Objects' to export:\n\t " for scs_root_object in game_objects_dict: - print(' - filename: %r' % scs_root_object.name) + message += ' - filename: %r\n\t ' % scs_root_object.name for scs_game_object in game_objects_dict[scs_root_object]: - print(' %r' % scs_game_object.name) + message += ' %r\n\t ' % scs_game_object.name + + lprint(message) return game_objects_dict diff --git a/addon/io_scs_tools/utils/path.py b/addon/io_scs_tools/utils/path.py index 5cbbcbe..b1beb1e 100644 --- a/addon/io_scs_tools/utils/path.py +++ b/addon/io_scs_tools/utils/path.py @@ -590,8 +590,8 @@ def startswith(path1, path2): :rtype: bool """ - norm_path1 = normalize(path1) - norm_path2 = normalize(path2) + norm_path1 = full_norm(path1) + norm_path2 = full_norm(path2) return norm_path1.startswith(norm_path2) @@ -608,13 +608,13 @@ def is_samepath(path1, path2): :rtype: bool """ - norm_path1 = normalize(path1) - norm_path2 = normalize(path2) + norm_path1 = full_norm(path1) + norm_path2 = full_norm(path2) return norm_path1 == norm_path2 -def normalize(path1): +def full_norm(path1): """Normalize path. It also takes into account windows drive letter which can be big or small. @@ -628,3 +628,18 @@ def normalize(path1): norm_path1 = os.path.normcase(norm_path1) return norm_path1 + + +def readable_norm(path): + """Normalize path in nice human readable form. + On windows it also converts backslashes to forward ones, to have cross platform output. + + :param path: path to normalize + :type path: str + :return: normalized path + :rtype: str + """ + norm_path = os.path.normpath(path) + norm_path = norm_path.replace("\\", "/") + + return norm_path diff --git a/addon/io_scs_tools/utils/printout.py b/addon/io_scs_tools/utils/printout.py index f4c7264..6e6c3ab 100644 --- a/addon/io_scs_tools/utils/printout.py +++ b/addon/io_scs_tools/utils/printout.py @@ -19,6 +19,75 @@ # Copyright (C) 2013-2014: SCS Software import bpy +import atexit +from tempfile import NamedTemporaryFile + + +class _FileLogger: + """File logging class wrapper. + + Class wrapping is needed manly for safety of log file removal + after Blender is shut down. + + Registering fuction for atexit module makes sure than, + file is deleted if Blender is closed normally. + + However file is not deleted if process is killed in Linux. + On Windows, on the other hand, file gets deleted even if Blender + is closed from Task Manager -> End Task/Process + """ + __log_file = None + + def __init__(self): + + self.__log_file = NamedTemporaryFile(mode="w+", suffix=".log.txt", delete=True) + + # instead of destructor we are using delete method, + # to close and consequentially delete log file + atexit.register(self.delete) + + def delete(self): + """Closes file and consiquentally deletes it as log file was created in that fashion. + """ + + # close file only if it's still exists in class variable + if self.__log_file is not None: + self.__log_file.close() + self.__log_file = None + + def write(self, msg_object): + """Writes message to the log file. + + :param msg_object: message to be written to file + :type msg_object: object + """ + + self.__log_file.write(msg_object) + + def flush(self): + """Flushes written content to file on disk.""" + + self.__log_file.flush() + + def get_log(self): + """Gets current content of temporary SCS BT log file, + which was created at startup and is having log of BT session. + + :return: current content of log file as string + :rtype: str + """ + + # firstly move to start of the file + self.__log_file.seek(0) + + log = "" + for line in self.__log_file.readlines(): + log += line.replace("\t ", "\t\t ") # replace for Blender text editor to be aligned the same as in console + + return log + + +file_logger = _FileLogger() dev_error_messages = [] error_messages = [] @@ -42,39 +111,44 @@ def lprint(string, values=(), report_errors=0, report_warnings=0): dump_level = int(_get_scs_globals().dump_level) - global error_messages, warning_messages prech = '' if string is not "": while string[0] in '\n\t': prech += string[0] string = string[1:] + + message = None if string[0] == 'E': message = str(prech + 'ERROR\t- ' + string[2:] % values) - print(message) error_messages.append(message.strip('\n')) # raise Exception('ERROR - ' + string[2:]) if string[0] == 'W': message = str(prech + 'WARNING\t- ' + string[2:] % values) warning_messages.append(message.strip('\n')) - if dump_level >= 1: - print(message) + if not dump_level >= 1: + message = None if dump_level >= 2: if string[0] == 'I': - print(prech + 'INFO\t- ' + string[2:] % values) + message = str(prech + 'INFO\t- ' + string[2:] % values) if dump_level >= 3: if string[0] == 'D': - print(prech + 'DEBUG\t- ' + string[2:] % values) + message = prech + 'DEBUG\t- ' + string[2:] % values if dump_level >= 4: if string[0] == 'S': - print(prech + string[2:] % values) + message = prech + string[2:] % values + + if message is not None: + print(message) + file_logger.write(message + "\n") + if string[0] not in 'EWIDS': print(prech + '!!! UNKNOWN MESSAGE SIGN !!! - "' + string + '"' % values) # CLEAR ERROR AND WARNING STACK IF REQUESTED if report_errors == -1: - error_messages = [] + error_messages.clear() if report_warnings == -1: - warning_messages = [] + warning_messages.clear() # ERROR AND WARNING REPORTS title = "" @@ -82,14 +156,19 @@ def lprint(string, values=(), report_errors=0, report_warnings=0): if report_errors == 1 and error_messages: # print error summary - print('\n\nERROR SUMMARY:\n================') - text += '\nERROR SUMMARY:\n================\n' + text += '\n\t ERROR SUMMARY:\n\t ================\n\t ' printed_messages = [] for message_i, message in enumerate(error_messages): + + message = message.replace("ERROR\t- ", "> ") + message = message.replace("\n\t ", "\n\t ") + + # print out only unique error messages if message not in printed_messages: printed_messages.append(message) - print(message) - text += message + "\n" + text += message + "\n\t " + + text += "================\n" # create dialog title and message title = "ERRORS" @@ -97,22 +176,26 @@ def lprint(string, values=(), report_errors=0, report_warnings=0): if dump_level == 5: dev_error_messages.extend(error_messages) - error_messages = [] + error_messages.clear() if report_warnings == 1 and warning_messages: if dump_level > 0: # print warning summary - print('\n\nWARNING SUMMARY:\n================') - text += '\nWARNING SUMMARY:\n================\n' + text += '\n\t WARNING SUMMARY:\n\t ================\n\t ' printed_messages = [] for message_i, message in enumerate(warning_messages): + + message = message.replace("WARNING\t- ", "> ") + message = message.replace("\n\t ", "\n\t ") + # print only unique messages if message not in printed_messages: printed_messages.append(message) - print(message) - text += message + "\n" + text += message + "\n\t " + + text += "================\n" # create dialog title and message if title != "": @@ -122,15 +205,30 @@ def lprint(string, values=(), report_errors=0, report_warnings=0): if dump_level == 5: dev_warning_messages.extend(warning_messages) - warning_messages = [] + warning_messages.clear() + + file_logger.flush() if title != "": + print(text) + file_logger.write(text + "\n") + file_logger.flush() bpy.ops.wm.show_3dview_report('INVOKE_DEFAULT', title=title, message=text) return True else: return False +def get_log(): + """Gets current content of temporary SCS BT log file, + which was created at startup and is having log of BT session. + + :return: current content of log file as string + :rtype: str + """ + return file_logger.get_log() + + def dev_lprint(): """Prints out whole stack of errors and warnings. Stack is cleared afterwards. """