Browse Source

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.
pull/128/head
Adam Johnson 6 years ago
parent
commit
e5b79bf6b5
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 30
      korman/addon_prefs.py
  2. 1
      korman/exporter/__init__.py
  3. 173
      korman/exporter/python.py
  4. 1
      korman/korlib/__init__.py
  5. 200
      korman/korlib/python.py
  6. 5
      korman/nodes/node_python.py
  7. 116
      korman/operators/op_export.py
  8. 219
      korman/plasma_magic.py
  9. 2
      korman/properties/__init__.py
  10. 22
      korman/properties/prop_text.py
  11. 1
      korman/ui/__init__.py
  12. 28
      korman/ui/ui_text.py
  13. 12
      korman/ui/ui_world.py

30
korman/addon_prefs.py

@ -39,11 +39,26 @@ class KormanAddonPreferences(bpy.types.AddonPreferences):
games = CollectionProperty(type=PlasmaGame) games = CollectionProperty(type=PlasmaGame)
active_game_index = IntProperty(options={"SKIP_SAVE"}) 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): def draw(self, context):
layout = self.layout layout = self.layout
split = layout.split()
main_col = split.column()
layout.label("Plasma Games:") main_col.label("Plasma Games:")
row = layout.row() row = main_col.row()
row.template_list("PlasmaGameListRW", "games", self, "games", self, row.template_list("PlasmaGameListRW", "games", self, "games", self,
"active_game_index", rows=2) "active_game_index", rows=2)
col = row.column(align=True) 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_remove", icon="ZOOMOUT", text="")
col.operator("world.plasma_game_convert", icon="IMPORT", 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 # Game Properties
active_game_index = self.active_game_index active_game_index = self.active_game_index
if bool(self.games) and active_game_index < len(self.games): if bool(self.games) and active_game_index < len(self.games):
active_game = self.games[active_game_index] active_game = self.games[active_game_index]
layout.separator() layout.label("Game Configuration:")
box = layout.box() box = layout.box().column()
box.prop(active_game, "path", emboss=False) box.prop(active_game, "path", emboss=False)
box.prop(active_game, "version") box.prop(active_game, "version")

1
korman/exporter/__init__.py

@ -18,4 +18,5 @@ from PyHSPlasma import *
from .convert import * from .convert import *
from .explosions import * from .explosions import *
from .python import *
from . import utils from . import utils

173
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 <http://www.gnu.org/licenses/>.
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()

1
korman/korlib/__init__.py

@ -73,6 +73,7 @@ else:
finally: finally:
from .console import ConsoleToggler from .console import ConsoleToggler
from .python import *
from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY
def _wave_chunks(stream): def _wave_chunks(stream):

200
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 <http://www.gnu.org/licenses/>.
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 = "<string>"
py_code_source = sys.stdin.read()
py_code_object = _compyle(module_name, py_code_source)
sys.stdout.write(py_code_object)

5
korman/nodes/node_python.py

@ -186,10 +186,13 @@ class PlasmaPythonFileNode(PlasmaNodeBase, bpy.types.Node):
self.inputs.clear() self.inputs.clear()
bpy.ops.node.plasma_attributes_to_node(node_path=self.node_path, python_path=self.filepath) 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") description="Python Filename")
filepath = StringProperty(update=_update_pyfile, filepath = StringProperty(update=_update_pyfile,
options={"HIDDEN"}) options={"HIDDEN"})
text_id = PointerProperty(name="Script File",
description="Script file datablock",
type=bpy.types.Text)
attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"}) attributes = CollectionProperty(type=PlasmaAttribute, options={"HIDDEN"})
no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) no_update = BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"})

116
korman/operators/op_export.py

@ -17,15 +17,34 @@ import bpy
from bpy.props import * from bpy.props import *
import cProfile import cProfile
from pathlib import Path from pathlib import Path
from PyHSPlasma import *
import pstats import pstats
from ..addon_prefs import game_versions from ..addon_prefs import game_versions
from .. import exporter from .. import exporter
from ..helpers import UiHelper from ..helpers import UiHelper
from .. import korlib
from ..properties.prop_world import PlasmaAge 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""" """Exports ages for Cyan Worlds' Plasma Engine"""
bl_idname = "export.plasma_age" 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")], ("perengine", "Export Supported EnvMaps", "Only environment maps supported by the selected game engine are exported")],
"default": "dcm2dem"}), "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", "export_active": (BoolProperty, {"name": "INTERNAL: Export currently running",
"default": False, "default": False,
"options": {"SKIP_SAVE"}}), "options": {"SKIP_SAVE"}}),
@ -101,7 +128,7 @@ class ExportOperator(bpy.types.Operator):
layout.prop(age, "texcache_method", text="") layout.prop(age, "texcache_method", text="")
layout.prop(age, "lighting_method") layout.prop(age, "lighting_method")
row = layout.row() row = layout.row()
row.enabled = ConsoleToggler.is_platform_supported() row.enabled = korlib.ConsoleToggler.is_platform_supported()
row.prop(age, "show_console") row.prop(age, "show_console")
layout.prop(age, "verbose") layout.prop(age, "verbose")
layout.prop(age, "profile_export") layout.prop(age, "profile_export")
@ -117,14 +144,6 @@ class ExportOperator(bpy.types.Operator):
else: else:
super().__setattr__(attr, value) 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): def execute(self, context):
# Before we begin, do some basic sanity checking... # Before we begin, do some basic sanity checking...
path = Path(self.filepath) path = Path(self.filepath)
@ -173,12 +192,8 @@ class ExportOperator(bpy.types.Operator):
# Called when a user hits "export" from the menu # Called when a user hits "export" from the menu
# We will prompt them for the export info, then call execute() # We will prompt them for the export info, then call execute()
if not self.filepath: if not self.filepath:
blend_filepath = context.blend_data.filepath bfp = self._get_default_path(context)
if not blend_filepath: self.filepath = str(Path(bfp).with_suffix(".age"))
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"))
context.window_manager.fileselect_add(self) context.window_manager.fileselect_add(self)
return {"RUNNING_MODAL"} return {"RUNNING_MODAL"}
@ -196,11 +211,76 @@ class ExportOperator(bpy.types.Operator):
setattr(PlasmaAge, name, prop(**age_options)) 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 :) # Add the export operator to the Export menu :)
def menu_cb(self, context): def menu_cb(self, context):
if context.scene.render.engine == "PLASMA_GAME": if context.scene.render.engine == "PLASMA_GAME":
self.layout.operator_context = "INVOKE_DEFAULT" 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(): def register():

219
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 <http://www.gnu.org/licenses/>.
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
"""

2
korman/properties/__init__.py

@ -21,6 +21,7 @@ from .prop_lamp import *
from . import modifiers from . import modifiers
from .prop_object import * from .prop_object import *
from .prop_scene import * from .prop_scene import *
from .prop_text import *
from .prop_texture import * from .prop_texture import *
from .prop_world 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_net = bpy.props.PointerProperty(type=PlasmaNet)
bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject) bpy.types.Object.plasma_object = bpy.props.PointerProperty(type=PlasmaObject)
bpy.types.Scene.plasma_scene = bpy.props.PointerProperty(type=PlasmaScene) 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.Texture.plasma_layer = bpy.props.PointerProperty(type=PlasmaLayer)
bpy.types.World.plasma_age = bpy.props.PointerProperty(type=PlasmaAge) bpy.types.World.plasma_age = bpy.props.PointerProperty(type=PlasmaAge)
bpy.types.World.plasma_fni = bpy.props.PointerProperty(type=PlasmaFni) bpy.types.World.plasma_fni = bpy.props.PointerProperty(type=PlasmaFni)

22
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 <http://www.gnu.org/licenses/>.
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())

1
korman/ui/__init__.py

@ -21,6 +21,7 @@ from .ui_menus import *
from .ui_modifiers import * from .ui_modifiers import *
from .ui_object import * from .ui_object import *
from .ui_render_layer import * from .ui_render_layer import *
from .ui_text import *
from .ui_texture import * from .ui_texture import *
from .ui_toolbox import * from .ui_toolbox import *
from .ui_world import * from .ui_world import *

28
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 <http://www.gnu.org/licenses/>.
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")

12
korman/ui/ui_world.py

@ -53,21 +53,30 @@ class PlasmaGamePanel(AgeButtonsPanel, bpy.types.Panel):
layout.separator() layout.separator()
row = layout.row(align=True) row = layout.row(align=True)
legal_game = bool(age.age_name.strip()) and active_game is not None
row.operator_context = "EXEC_DEFAULT" 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") op = row.operator("export.plasma_age", icon="EXPORT")
if active_game is not None: if active_game is not None:
op.dat_only = False op.dat_only = False
op.filepath = str((Path(active_game.path) / "dat" / age.age_name).with_suffix(".age")) op.filepath = str((Path(active_game.path) / "dat" / age.age_name).with_suffix(".age"))
op.version = active_game.version op.version = active_game.version
row = row.row(align=True) row = row.row(align=True)
row.enabled = legal_game
row.operator_context = "INVOKE_DEFAULT" row.operator_context = "INVOKE_DEFAULT"
op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age") op = row.operator("export.plasma_age", icon="PACKAGE", text="Package Age")
if active_game is not None: if active_game is not None:
op.dat_only = False op.dat_only = False
op.filepath = "{}.zip".format(age.age_name) op.filepath = "{}.zip".format(age.age_name)
op.version = active_game.version 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): class PlasmaGameListRO(bpy.types.UIList):
@ -151,6 +160,7 @@ class PlasmaAgePanel(AgeButtonsPanel, bpy.types.Panel):
layout.separator() layout.separator()
layout.prop(age, "envmap_method") layout.prop(age, "envmap_method")
layout.prop(age, "lighting_method") layout.prop(age, "lighting_method")
layout.prop(age, "python_method")
layout.prop(age, "texcache_method") layout.prop(age, "texcache_method")

Loading…
Cancel
Save