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