From e5b79bf6b5871c048c8c623de3f5570506db2c51 Mon Sep 17 00:00:00 2001 From: Adam Johnson Date: Sat, 5 Jan 2019 23:17:13 -0500 Subject: [PATCH] Add Python packing operator Implements the boilerplate code for compiling Python code in arbitrary python versions and packing the marshalled data into Cyan's Python.pak format. Since this is a lot of bp, a separate operator has been added to both test the resulting mayhem and provide age creators an easy way to export only their needed Python. The only python that is packed currently is the age sdl hook file, if any. In order for that part to happen, Python File nodes need to be upgraded from having a string path to actually using the new text_id field. --- korman/addon_prefs.py | 30 ++++- korman/exporter/__init__.py | 1 + korman/exporter/python.py | 173 ++++++++++++++++++++++++++ korman/korlib/__init__.py | 1 + korman/korlib/python.py | 200 ++++++++++++++++++++++++++++++ korman/nodes/node_python.py | 5 +- korman/operators/op_export.py | 116 ++++++++++++++--- korman/plasma_magic.py | 219 +++++++++++++++++++++++++++++++++ korman/properties/__init__.py | 2 + korman/properties/prop_text.py | 22 ++++ korman/ui/__init__.py | 1 + korman/ui/ui_text.py | 28 +++++ korman/ui/ui_world.py | 12 +- 13 files changed, 786 insertions(+), 24 deletions(-) create mode 100644 korman/exporter/python.py create mode 100644 korman/korlib/python.py create mode 100644 korman/plasma_magic.py create mode 100644 korman/properties/prop_text.py create mode 100644 korman/ui/ui_text.py diff --git a/korman/addon_prefs.py b/korman/addon_prefs.py index 94a3011..1155e57 100644 --- a/korman/addon_prefs.py +++ b/korman/addon_prefs.py @@ -39,11 +39,26 @@ class KormanAddonPreferences(bpy.types.AddonPreferences): games = CollectionProperty(type=PlasmaGame) active_game_index = IntProperty(options={"SKIP_SAVE"}) + python22_executable = StringProperty(name="Python 2.2", + description="Path to the Python 2.2 executable", + options=set(), + subtype="FILE_PATH") + python23_executable = StringProperty(name="Python 2.3", + description="Path to the Python 2.3 executable", + options=set(), + subtype="FILE_PATH") + python27_executable = StringProperty(name="Python 2.7", + description="Path to the Python 2.7 executable", + options=set(), + subtype="FILE_PATH") + def draw(self, context): layout = self.layout + split = layout.split() + main_col = split.column() - layout.label("Plasma Games:") - row = layout.row() + main_col.label("Plasma Games:") + row = main_col.row() row.template_list("PlasmaGameListRW", "games", self, "games", self, "active_game_index", rows=2) col = row.column(align=True) @@ -51,13 +66,20 @@ class KormanAddonPreferences(bpy.types.AddonPreferences): col.operator("world.plasma_game_remove", icon="ZOOMOUT", text="") col.operator("world.plasma_game_convert", icon="IMPORT", text="") + # Python Installs + main_col = split.column() + main_col.label("Python Executables:") + main_col.prop(self, "python22_executable") + main_col.prop(self, "python23_executable") + main_col.prop(self, "python27_executable") + # Game Properties active_game_index = self.active_game_index if bool(self.games) and active_game_index < len(self.games): active_game = self.games[active_game_index] - layout.separator() - box = layout.box() + layout.label("Game Configuration:") + box = layout.box().column() box.prop(active_game, "path", emboss=False) box.prop(active_game, "version") diff --git a/korman/exporter/__init__.py b/korman/exporter/__init__.py index 0ec72c5..e8652a8 100644 --- a/korman/exporter/__init__.py +++ b/korman/exporter/__init__.py @@ -18,4 +18,5 @@ from PyHSPlasma import * from .convert import * from .explosions import * +from .python import * from . import utils diff --git a/korman/exporter/python.py b/korman/exporter/python.py new file mode 100644 index 0000000..e263b9f --- /dev/null +++ b/korman/exporter/python.py @@ -0,0 +1,173 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from pathlib import Path +from PyHSPlasma import * + +from .explosions import ExportError +from . import logger +from .. import korlib +from ..plasma_magic import plasma_python_glue, very_very_special_python + +class PythonPackageExporter: + def __init__(self, filepath, version): + self._filepath = filepath + self._modules = {} + self._pfms = {} + self._version = version + + def _compyle(self, report): + report.progress_advance() + report.progress_range = len(self._modules) + len(self._pfms) + inc_progress = report.progress_increment + + age = bpy.context.scene.world.plasma_age + Text = bpy.types.Text + if self._version <= pvPots: + py_version = (2, 2) + else: + py_version = (2, 3) + py_code = [] + + for filename, source in self._pfms.items(): + if isinstance(source, Text): + if not source.plasma_text.package and age.python_method != "all": + inc_progress() + continue + code = source.as_string() + else: + code = source + + code = "{}\n\n{}\n".format(code, plasma_python_glue) + success, result = korlib.compyle(filename, code, py_version, report, indent=1) + if not success: + raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) + py_code.append((filename, result)) + inc_progress() + + for filename, source in self._modules.items(): + if isinstance(source, Text): + if not source.plasma_text.package and age.python_method != "all": + inc_progress() + continue + code = source.as_string() + else: + code = source + + # no glue needed here, ma! + success, result = korlib.compyle(filename, code, py_version, report, indent=1) + if not success: + raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) + py_code.append((filename, result)) + inc_progress() + + # man that was ugly... + return py_code + + def _ensure_age_sdl_hook(self, report): + age_props = bpy.context.scene.world.plasma_age + if age_props.age_sdl: + py_filename = "{}.py".format(age_props.age_name) + age_py = self._modules.get(py_filename) + if age_py is not None: + del self._modules[py_filename] + if age_py.plasma_text.package or age.python_method == "all": + self._pfms[py_filename] = age_py + else: + report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) + self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + else: + report.msg("Packing default AgeSDL Python", indent=1) + very_very_special_python.format(age_name=age_props.age_name) + self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) + + def _harvest_pfms(self, report): + objects = bpy.context.scene.objects + report.progress_advance() + report.progress_range = len(objects) + inc_progress = report.progress_increment + + for i in objects: + logic = i.plasma_modifiers.advanced_logic + if i.plasma_object.enabled and logic.enabled: + for j in logic.logic_groups: + tree_versions = (globals()[version] for version in j.version) + if self._version in tree_versions: + self._harvest_tree(j.node_tree) + inc_progress() + + def _harvest_modules(self, report): + texts = bpy.data.texts + report.progress_advance() + report.progress_range = len(texts) + inc_progress = report.progress_increment + + for i in texts: + if i.name.endswith(".py") and i.name not in self._pfms: + self._modules.setdefault(i.name, i) + inc_progress() + + def _harvest_tree(self, tree): + # Search the node tree for any python file nodes. Any that we find are PFMs + for i in tree.nodes: + if i.bl_idname == "PlasmaPythonFileNode": + if i.filename and i.text_id: + self._pfms.setdefault(i.filename, i.text_id) + + def run(self): + """Runs a stripped-down version of the Exporter that only handles Python files""" + age_props = bpy.context.scene.world.plasma_age + log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger + with korlib.ConsoleToggler(age_props.show_console), log(self._filepath) as report: + report.progress_add_step("Harvesting Plasma PythonFileMods") + report.progress_add_step("Harvesting Helper Python Modules") + report.progress_add_step("Compyling Python Code") + report.progress_add_step("Packing Compyled Code") + report.progress_start("PACKING PYTHON") + + # Harvest the Python code + self._harvest_pfms(report) + self._harvest_modules(report) + self._ensure_age_sdl_hook(report) + + # Compyle and package the Python + self._package_python(report) + + # DONE + report.progress_end() + + def _package_python(self, report): + py_code = self._compyle(report) + self._write_python_pak(py_code, report) + + def _write_python_pak(self, py_code, report): + report.progress_advance() + + if self._version == pvEoa: + enc = plEncryptedStream.kEncAes + elif self._version == pvMoul: + enc = None + else: + enc = plEncryptedStream.kEncXtea + + if enc is None: + stream = hsFileStream(self._version).open(self._filepath, fmCreate) + else: + stream = plEncryptedStream(self._version).open(self._filepath, fmCreate, enc) + try: + korlib.package_python(stream, py_code) + finally: + stream.close() diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 89006c0..3e03574 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -73,6 +73,7 @@ else: finally: from .console import ConsoleToggler + from .python import * from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY def _wave_chunks(stream): diff --git a/korman/korlib/python.py b/korman/korlib/python.py new file mode 100644 index 0000000..0a7e219 --- /dev/null +++ b/korman/korlib/python.py @@ -0,0 +1,200 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +from __future__ import generators # Python 2.2 +import marshal +import os.path +import sys + +_python_executables = {} + +class PythonNotAvailableError(Exception): + pass + + +def compyle(file_name, py_code, py_version, report=None, indent=0): + # NOTE: Should never run under Python 2.x + my_version = sys.version_info[:2] + assert my_version == (2, 7) or my_version[0] > 2 + + # Remember: Python 2.2 file, so no single line if statements... + idx = file_name.find('.') + if idx == -1: + module_name = file_name + else: + module_name = file_name[:idx] + + if report is not None: + report.msg("Compyling {}", file_name, indent=indent) + + if my_version != py_version: + import subprocess + + py_executable = _find_python(py_version) + args = (py_executable, __file__, module_name) + try: + py_code = py_code.encode("utf-8") + except UnicodeError: + if report is not None: + report.error("Could not encode '{}'", file_name, indent=indent+1) + return (False, "Could not encode file") + result = subprocess.run(args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + if result.returncode != 0: + try: + error = result.stdout.decode("utf-8").replace('\r\n', '\n') + except UnicodeError: + error = result.stdout + if report is not None: + report.error("Compylation Error in '{}'\n{}", file_name, error, indent=indent+1) + return (result.returncode == 0, result.stdout) + else: + raise NotImplementedError() + +def _compyle(module_name, py_code): + # Old python versions have major issues with Windows style newlines. + # Also, bad things happen if there is no newline at the end. + py_code += '\n' # sigh, this is slow on old Python... + py_code = py_code.replace('\r\n', '\n') + py_code = py_code.replace('\r', '\n') + code_object = compile(py_code, module_name, "exec") + + # The difference between us and the py_compile module is twofold: + # 1) py_compile compyles to a file. We might be exporting to memory, so that's + # not what we want. + # 2) py_compile saves a *pyc format file containing information such as compyle + # time and marshal format version. These items are not included in Cyan's + # Python.pak format. + # Therefore, we simply return the marshalled data as a string. + return marshal.dumps(code_object) + +def _find_python(py_version): + def find_executable(py_version): + # First, try to use Blender to find the Python executable + try: + import bpy + except ImportError: + pass + else: + userprefs = bpy.context.user_preferences.addons["korman"].preferences + py_executable = getattr(userprefs, "python{}{}_executable".format(*py_version), None) + if _verify_python(py_version, py_executable): + return py_executable + + # Second, try looking Python up in the registry. + try: + import winreg + except ImportError: + pass + else: + py_executable = _find_python_reg(winreg.HKEY_LOCAL_MACHINE, py_version) + if _verify_python(py_version, py_executable): + return py_executable + py_executable = _find_python_reg(winreg.HKEY_CURRENT_USER, py_version) + if _verify_python(py_version, py_executable): + return py_executable + + # I give up, you win. + return None + + py_executable = _python_executables.setdefault(py_version, find_executable(py_version)) + if py_executable: + return py_executable + else: + raise PythonNotAvailableError("{}.{}".format(*py_version)) + +def _find_python_reg(reg_key, py_version): + import winreg + subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version) + try: + python_dir = winreg.QueryValue(reg_key, subkey_name) + except FileNotFoundError: + return None + else: + return os.path.join(python_dir, "python.exe") + +def package_python(stream, pyc_objects): + # Python.pak format: + # uint32_t numFiles + # - safeStr filename + # - uint32_t offset + # ~~~~~ + # uint32_t filesz + # uint8_t data[filesz] + assert bool(pyc_objects) + + # `stream` might be a plEncryptedStream, which doesn't seek very well at all. + # Therefore, we will go ahead and calculate the size of the index block so + # there is no need to seek around to write offset values + base_offset = 4 # uint32_t numFiles + data_offset = 0 + pyc_info = [] # sad, but makes life easier... + for module_name, compyled_code in pyc_objects: + pyc_info.append((module_name, data_offset, compyled_code)) + + # index offset overall + base_offset += 2 # writeSafeStr length + # NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts + # the Python unicode/string object to UTF-8. Currently, this is true. + base_offset += len(module_name.encode("utf-8")) # writeSafeStr + base_offset += 4 + + # current file data offset + data_offset += 4 # uint32_t filesz + data_offset += len(compyled_code) + + stream.writeInt(len(pyc_info)) + for module_name, data_offset, compyled_code in pyc_info: + stream.writeSafeStr(module_name) + # offset of data == index size (base_offset) + offset to data blob (data_offset) + stream.writeInt(base_offset + data_offset) + for module_name, data_offset, compyled_code in pyc_info: + stream.writeInt(len(compyled_code)) + stream.write(compyled_code) + +def _verify_python(py_version, py_exe): + if not py_exe: + return False + + import subprocess + try: + args = (py_exe, "-V") + result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5) + except OSError: + return False + else: + output = result.stdout.decode() + try: + py_str, py_check = output[:6], output[7:10] + except IndexError: + return False + else: + if py_str != "Python": + return False + return "{}.{}".format(*py_version) == py_check + +if __name__ == "__main__": + # Python tries to be "helpful" on Windows by converting \n to \r\n. + # Therefore we must change the mode of stdout. + if sys.platform == "win32": + import os, msvcrt + msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) + + try: + module_name = sys.argv[1] + except IndexError: + module_name = "" + py_code_source = sys.stdin.read() + py_code_object = _compyle(module_name, py_code_source) + sys.stdout.write(py_code_object) diff --git a/korman/nodes/node_python.py b/korman/nodes/node_python.py index 60f3583..96c9dcf 100644 --- a/korman/nodes/node_python.py +++ b/korman/nodes/node_python.py @@ -186,10 +186,13 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node): self.inputs.clear() bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, python_path=self.filepath) - filename = StringProperty(name="File", + filename = StringProperty(name="File Name", description="Python Filename") filepath = StringProperty(update=_update_pyfile, options={"HIDDEN"}) + text_id = PointerProperty(name="Script File", + description="Script file datablock", + type=bpy.types.Text) attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index d246542..a1944d0 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -17,15 +17,34 @@ import bpy from bpy.props import * import cProfile from pathlib import Path +from PyHSPlasma import * import pstats from ..addon_prefs import game_versions from .. import exporter from ..helpers import UiHelper +from .. import korlib from ..properties.prop_world import PlasmaAge -from ..korlib import ConsoleToggler -class ExportOperator(bpy.types.Operator): +class ExportOperator: + def _get_default_path(self, context): + blend_filepath = context.blend_data.filepath + if not blend_filepath: + blend_filepath = context.scene.world.plasma_age.age_name + if not blend_filepath: + blend_filepath = "Korman" + return blend_filepath + + @property + def has_reports(self): + return hasattr(self.report) + + @classmethod + def poll(cls, context): + return context.scene.render.engine == "PLASMA_GAME" + + +class PlasmaAgeExportOperator(ExportOperator, bpy.types.Operator): """Exports ages for Cyan Worlds' Plasma Engine""" bl_idname = "export.plasma_age" @@ -72,6 +91,14 @@ class ExportOperator(bpy.types.Operator): ("perengine", "Export Supported EnvMaps", "Only environment maps supported by the selected game engine are exported")], "default": "dcm2dem"}), + "python_method": (EnumProperty, {"name": "Python", + "description": "Specifies how Python should be packed", + "items": [("none", "Pack Nothing", "Don't pack any Python files."), + ("as_requested", "Pack Requested Scripts", "Packs any script both linked as a Text file and requested for packaging."), + ("all", "Pack All Scripts", "Packs all Python files linked as a Text file.")], + "default": "as_requested", + "options": set()}), + "export_active": (BoolProperty, {"name": "INTERNAL: Export currently running", "default": False, "options": {"SKIP_SAVE"}}), @@ -101,7 +128,7 @@ class ExportOperator(bpy.types.Operator): layout.prop(age, "texcache_method", text="") layout.prop(age, "lighting_method") row = layout.row() - row.enabled = ConsoleToggler.is_platform_supported() + row.enabled = korlib.ConsoleToggler.is_platform_supported() row.prop(age, "show_console") layout.prop(age, "verbose") layout.prop(age, "profile_export") @@ -117,14 +144,6 @@ class ExportOperator(bpy.types.Operator): else: super().__setattr__(attr, value) - @property - def has_reports(self): - return hasattr(self.report) - - @classmethod - def poll(cls, context): - return context.scene.render.engine == "PLASMA_GAME" - def execute(self, context): # Before we begin, do some basic sanity checking... path = Path(self.filepath) @@ -173,12 +192,8 @@ class ExportOperator(bpy.types.Operator): # Called when a user hits "export" from the menu # We will prompt them for the export info, then call execute() if not self.filepath: - blend_filepath = context.blend_data.filepath - if not blend_filepath: - blend_filepath = context.scene.world.plasma_age.age_name - if not blend_filepath: - blend_filepath = "Korman" - self.filepath = str(Path(blend_filepath).with_suffix(".age")) + bfp = self._get_default_path(context) + self.filepath = str(Path(bfp).with_suffix(".age")) context.window_manager.fileselect_add(self) return {"RUNNING_MODAL"} @@ -196,11 +211,76 @@ class ExportOperator(bpy.types.Operator): setattr(PlasmaAge, name, prop(**age_options)) +class PlasmaPythonExportOperator(ExportOperator, bpy.types.Operator): + bl_idname = "export.plasma_pak" + bl_label = "Package Scripts" + bl_description = "Package Age Python scripts" + + filepath = StringProperty(subtype="FILE_PATH") + filter_glob = StringProperty(default="*.pak", options={'HIDDEN'}) + + version = EnumProperty(name="Version", + description="Plasma version to export this age for", + items=game_versions, + default="pvPots", + options=set()) + + def draw(self, context): + layout = self.layout + age = context.scene.world.plasma_age + + # The crazy mess we're doing with props on the fly means we have to explicitly draw them :( + row = layout.row() + row.alert = age.python_method == "none" + row.prop(age, "python_method") + layout.prop(self, "version") + row = layout.row() + row.enabled = korlib.ConsoleToggler.is_platform_supported() + row.prop(age, "show_console") + layout.prop(age, "verbose") + + def execute(self, context): + path = Path(self.filepath) + if not self.filepath: + self.report({"ERROR"}, "No file specified") + return {"CANCELLED"} + else: + if not path.exists: + try: + path.mkdir(parents=True) + except OSError: + self.report({"ERROR"}, "Failed to create export directory") + return {"CANCELLED"} + path.touch() + + # Bonus Fun: Implement Profile-mode here (later...) + e = exporter.PythonPackageExporter(filepath=self.filepath, + version=globals()[self.version]) + try: + e.run() + except exporter.ExportError as error: + self.report({"ERROR"}, str(error)) + return {"CANCELLED"} + except korlib.PythonNotAvailableError as error: + self.report({"ERROR"}, "Python Version {} not found".format(error)) + return {"CANCELLED"} + else: + return {"FINISHED"} + + def invoke(self, context, event): + if not self.filepath: + bfp = self._get_default_path(context) + self.filepath = str(Path(bfp).with_suffix(".pak")) + context.window_manager.fileselect_add(self) + return {"RUNNING_MODAL"} + + # Add the export operator to the Export menu :) def menu_cb(self, context): if context.scene.render.engine == "PLASMA_GAME": self.layout.operator_context = "INVOKE_DEFAULT" - self.layout.operator(ExportOperator.bl_idname, text="Plasma Age (.age)") + self.layout.operator(PlasmaAgeExportOperator.bl_idname, text="Plasma Age (.age)") + self.layout.operator(PlasmaPythonExportOperator.bl_idname, text="Plasma Scripts (.pak)") def register(): diff --git a/korman/plasma_magic.py b/korman/plasma_magic.py new file mode 100644 index 0000000..b67d438 --- /dev/null +++ b/korman/plasma_magic.py @@ -0,0 +1,219 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +very_very_special_python = """ +from Plasma import * +from PlasmaTypes import * + +globals()["{age_name}"] = type("{age_name}", (ptResponder,), dict()) +""" + +very_very_special_sdl = """ +#============================================================== +# This VeryVerySpecial SDL File was automatically generated +# by Korman. Have a nice day! +# +# READ: When modifying an SDL record, do *not* modify the +# existing record. You must copy and paste a new version +# below the current one and make your changes there. +#============================================================== + +STATEDESC {age_name} +{{ + VERSION 0 +}} + +""" + +# Copypasta (with small fixes for str.format) of glue.py from CWE's moul-scripts +plasma_python_glue = """ +glue_cl = None +glue_inst = None +glue_params = None +glue_paramKeys = None +try: + x = glue_verbose +except NameError: + glue_verbose = 0 +def glue_getClass(): + global glue_cl + if glue_cl == None: + try: + cl = globals()[glue_name] + if issubclass(cl,ptModifier): + glue_cl = cl + else: + if glue_verbose: + print "Class %s is not derived from modifier" % (cl.__name__) + except: + if glue_verbose: + try: + print "Could not find class %s" % (glue_name) + except NameError: + print "Filename/classname not set!" + return glue_cl +def glue_getInst(): + global glue_inst + if type(glue_inst) == type(None): + cl = glue_getClass() + if cl != None: + glue_inst = cl() + return glue_inst +def glue_delInst(): + global glue_inst + global glue_cl + global glue_params + global glue_paramKeys + if type(glue_inst) != type(None): + del glue_inst + glue_cl = None + glue_params = None + glue_paramKeys = None +def glue_getVersion(): + inst = glue_getInst() + ver = inst.version + glue_delInst() + return ver +def glue_findAndAddAttribs(obj, glue_params): + if isinstance(obj,ptAttribute): + if glue_params.has_key(obj.id): + if glue_verbose: + print "WARNING: Duplicate attribute ids!" + print "%s has id %d which is already defined in %s" % (obj.name, obj.id, glue_params[obj.id].name) + else: + glue_params[obj.id] = obj + elif type(obj) == type([]): + for o in obj: + glue_findAndAddAttribs(o, glue_params) + elif type(obj) == type(dict()): + for o in obj.values(): + glue_findAndAddAttribs(o, glue_params) + elif type(obj) == type( () ): + for o in obj: + glue_findAndAddAttribs(o, glue_params) + +def glue_getParamDict(): + global glue_params + global glue_paramKeys + if type(glue_params) == type(None): + glue_params = dict() + gd = globals() + for obj in gd.values(): + glue_findAndAddAttribs(obj, glue_params) + # rebuild the parameter sorted key list + glue_paramKeys = glue_params.keys() + glue_paramKeys.sort() + glue_paramKeys.reverse() + return glue_params +def glue_getClassName(): + cl = glue_getClass() + if cl != None: + return cl.__name__ + if glue_verbose: + print "Class not found in %s.py" % (glue_name) + return None +def glue_getBlockID(): + inst = glue_getInst() + if inst != None: + return inst.id + if glue_verbose: + print "Instance could not be created in %s.py" % (glue_name) + return None +def glue_getNumParams(): + pd = glue_getParamDict() + if pd != None: + return len(pd) + if glue_verbose: + print "No attributes found in %s.py" % (glue_name) + return 0 +def glue_getParam(number): + global glue_paramKeys + pd = glue_getParamDict() + if pd != None: + # see if there is a paramKey list + if type(glue_paramKeys) == type([]): + if number >= 0 and number < len(glue_paramKeys): + return pd[glue_paramKeys[number]].getdef() + else: + print "glue_getParam: Error! %d out of range of attribute list" % (number) + else: + pl = pd.values() + if number >= 0 and number < len(pl): + return pl[number].getdef() + else: + if glue_verbose: + print "glue_getParam: Error! %d out of range of attribute list" % (number) + if glue_verbose: + print "GLUE: Attribute list error" + return None +def glue_setParam(id,value): + pd = glue_getParamDict() + if pd != None: + if pd.has_key(id): + try: + pd[id].__setvalue__(value) + except AttributeError: + if isinstance(pd[id],ptAttributeList): + try: + if type(pd[id].value) != type([]): + pd[id].value = [] + except AttributeError: + pd[id].value = [] + pd[id].value.append(value) + else: + pd[id].value = value + else: + if glue_verbose: + print "setParam: can't find id=",id + else: + print "setParma: Something terribly has gone wrong. Head for the cover." +def glue_isNamedAttribute(id): + pd = glue_getParamDict() + if pd != None: + try: + if isinstance(pd[id],ptAttribNamedActivator): + return 1 + if isinstance(pd[id],ptAttribNamedResponder): + return 2 + except KeyError: + if glue_verbose: + print "Could not find id=%d attribute" % (id) + return 0 +def glue_isMultiModifier(): + inst = glue_getInst() + if isinstance(inst,ptMultiModifier): + return 1 + return 0 +def glue_getVisInfo(number): + global glue_paramKeys + pd = glue_getParamDict() + if pd != None: + # see if there is a paramKey list + if type(glue_paramKeys) == type([]): + if number >= 0 and number < len(glue_paramKeys): + return pd[glue_paramKeys[number]].getVisInfo() + else: + print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) + else: + pl = pd.values() + if number >= 0 and number < len(pl): + return pl[number].getVisInfo() + else: + if glue_verbose: + print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) + if glue_verbose: + print "GLUE: Attribute list error" + return None +""" diff --git a/korman/properties/__init__.py b/korman/properties/__init__.py index d8ea5e6..741a519 100644 --- a/korman/properties/__init__.py +++ b/korman/properties/__init__.py @@ -21,6 +21,7 @@ from .prop_lamp import * from . import modifiers from .prop_object import * from .prop_scene import * +from .prop_text import * from .prop_texture import * from .prop_world import * @@ -32,6 +33,7 @@ def register(): bpy.types.Object.plasma_net = bpy.props.PointerProperty(type=PlasmaNet) bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) bpy.types.Scene.plasma_scene = bpy.props.PointerProperty(type=PlasmaScene) + bpy.types.Text.plasma_text = bpy.props.PointerProperty(type=PlasmaText) bpy.types.Texture.plasma_layer = bpy.props.PointerProperty(type=PlasmaLayer) bpy.types.World.plasma_age = bpy.props.PointerProperty(type=PlasmaAge) bpy.types.World.plasma_fni = bpy.props.PointerProperty(type=PlasmaFni) diff --git a/korman/properties/prop_text.py b/korman/properties/prop_text.py new file mode 100644 index 0000000..3746cf7 --- /dev/null +++ b/korman/properties/prop_text.py @@ -0,0 +1,22 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy +from bpy.props import * + +class PlasmaText(bpy.types.PropertyGroup): + package = BoolProperty(name="Export", + description="Package this file in the age export", + options=set()) diff --git a/korman/ui/__init__.py b/korman/ui/__init__.py index 3369bb7..a292060 100644 --- a/korman/ui/__init__.py +++ b/korman/ui/__init__.py @@ -21,6 +21,7 @@ from .ui_menus import * from .ui_modifiers import * from .ui_object import * from .ui_render_layer import * +from .ui_text import * from .ui_texture import * from .ui_toolbox import * from .ui_world import * diff --git a/korman/ui/ui_text.py b/korman/ui/ui_text.py new file mode 100644 index 0000000..4d3fd12 --- /dev/null +++ b/korman/ui/ui_text.py @@ -0,0 +1,28 @@ +# This file is part of Korman. +# +# Korman is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Korman is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Korman. If not, see . + +import bpy + +class PlasmaTextEditorHeader(bpy.types.Header): + bl_space_type = "TEXT_EDITOR" + + def draw(self, context): + layout, text = self.layout, context.space_data.text + + if text is not None: + is_py = text.name.endswith(".py") + row = layout.row(align=True) + row.enabled = is_py + row.prop(text.plasma_text, "package") diff --git a/korman/ui/ui_world.py b/korman/ui/ui_world.py index a02d388..938f0f3 100644 --- a/korman/ui/ui_world.py +++ b/korman/ui/ui_world.py @@ -53,21 +53,30 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel): layout.separator() row = layout.row(align=True) + legal_game = bool(age.age_name.strip()) and active_game is not None row.operator_context = "EXEC_DEFAULT" - row.enabled = bool(age.age_name.strip()) and active_game is not None + row.enabled = legal_game op = row.operator("export.plasma_age", icon="EXPORT") if active_game is not None: op.dat_only = False op.filepath = str((Path(active_game.path) / "dat" / age.age_name).with_suffix(".age")) op.version = active_game.version row = row.row(align=True) + row.enabled = legal_game row.operator_context = "INVOKE_DEFAULT" op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age") if active_game is not None: op.dat_only = False op.filepath = "{}.zip".format(age.age_name) op.version = active_game.version + row = row.row(align=True) + row.operator_context = "EXEC_DEFAULT" + row.enabled = legal_game and active_game.version != "pvMoul" + op = row.operator("export.plasma_pak", icon="FILE_SCRIPT") + if active_game is not None: + op.filepath = str((Path(active_game.path) / "Python" / age.age_name).with_suffix(".pak")) + op.version = active_game.version class PlasmaGameListRO(bpy.types.UIList): @@ -151,6 +160,7 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel): layout.separator() layout.prop(age, "envmap_method") layout.prop(age, "lighting_method") + layout.prop(age, "python_method") layout.prop(age, "texcache_method")