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")