diff --git a/korman/__init__.py b/korman/__init__.py
index 170c467..b55ce32 100644
--- a/korman/__init__.py
+++ b/korman/__init__.py
@@ -34,8 +34,8 @@ def register():
# This will auto-magically register all blender classes for us
bpy.utils.register_module(__name__)
- # We have to setup pointer props to our custom property groups ourselves,
- # so let's go ahead and do that now.
+ # Sigh... Blender isn't totally automated.
+ operators.register()
properties.register()
def unregister():
diff --git a/korman/exporter.py b/korman/exporter.py
index 0203a7b..73d6dee 100644
--- a/korman/exporter.py
+++ b/korman/exporter.py
@@ -14,46 +14,147 @@
# along with Korman. If not, see .
import bpy
+import os.path
from PyHSPlasma import *
-class PlasmaExporter(bpy.types.Operator):
- """Exports ages for Cyan Worlds' Plasma Engine"""
-
- bl_idname = "export.plasma_age"
- bl_label = "Export Age"
-
- # Export specific props
- pl_version = bpy.props.EnumProperty(
- name="Version",
- description="Version of the Plasma Engine to target",
- default="pots", # This should be changed when moul is easier to target!
- items=[
- ("abm", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game", 2),
- ("pots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack", 1),
- ("moul", "Myst Online: Uru Live (70.2)", "Targets the most recent online game", 0),
- # I see no reason to even offer Myst 5...
- ]
- )
-
- @classmethod
- def poll(cls, context):
- if context.object is not None:
- return context.scene.render.engine == "PLASMA_GAME"
-
- def execute(self, context):
- # TODO
- return {"FINISHED"}
-
- def invoke(self, context, event):
- # Called when a user hits "export" from the menu
- # We will prompt them for the export info, then call execute()
- context.window_manager.fileselect_add(self)
- return {"RUNNING_MODAL"}
-
-
-# Add the export operator to the Export menu :)
-def menu_cb(self, context):
- self.layout.operator_context = "INVOKE_DEFAULT"
- self.layout.operator(PlasmaExporter.bl_idname, text="Plasma Age (.age)")
-def register():
- bpy.types.INFO_MT_file_export.append(menu_cb)
+class ExportError(Exception):
+ def __init__(self, value="Undefined Export Error"):
+ self.value = value
+ def __str__(self):
+ return self.value
+
+class Exporter:
+ def __init__(self, op):
+ self._op = op # Blender operator
+
+ # This stuff doesn't need to be static
+ self._objects = []
+ self._pages = {}
+ self.texture_page = True
+
+ @property
+ def age_name(self):
+ return os.path.splitext(os.path.split(self._op.filepath)[1])[0]
+
+ def _create_page(self, name, id, builtin=False):
+ location = plLocation(self.version)
+ location.prefix = bpy.context.scene.world.plasma_age.seq_prefix
+ if builtin:
+ location.flags |= plLocation.kBuiltIn
+ location.page = id
+ self._pages[name] = location
+
+ info = plPageInfo()
+ info.age = self.age_name
+ info.page = name
+ info.location = location
+ self.mgr.AddPage(info)
+
+ if not builtin:
+ self._age_info.addPage((name, id, 0))
+ if self.version <= pvPots:
+ node = plSceneNode("%s_District_%s" % (self.age_name, name))
+ else:
+ node = plSceneNode("%s_%s" % (self.age_name, name))
+ self.mgr.AddObject(location, node)
+ return location
+
+ def get_textures_page(self, obj):
+ if self.pages["Textures"] is not None:
+ return self.pages["Textures"]
+ else:
+ return self.pages[obj.plasma_object.page]
+
+ @property
+ def version(self):
+ # I <3 Python
+ return globals()[self._op.version]
+
+ def run(self):
+ # Step 0: Init export resmgr and stuff
+ self.mgr = plResManager()
+ self.mgr.setVer(self.version)
+
+ # Step 1: Gather a list of objects that we need to export
+ # We should do this first so we can sanity check
+ # and give accurate progress reports
+ self._collect_objects()
+
+ # Step 2: Collect some age information
+ self._grab_age_info() # World Props -> plAgeInfo
+ for page in bpy.context.scene.world.plasma_age.pages:
+ self._create_page(page.name, page.seq_suffix)
+ self._sanity_check_pages()
+ self._generate_builtins() # Creates BuiltIn and Textures
+
+ # ... todo ...
+
+ # ... And finally... Write it all out!
+ self.mgr.WriteAge(self._op.filepath, self._age_info)
+ dir = os.path.split(self._op.filepath)[0]
+ for name, loc in self._pages.items():
+ page = self.mgr.FindPage(loc) # not cached because it's C++ owned
+ # I know that plAgeInfo has its own way of doing this, but we'd have
+ # to do some looping and stuff. This is easier.
+ if self.version <= pvMoul:
+ chapter = "_District_"
+ else:
+ chapter = "_"
+ f = os.path.join(dir, "%s%s%s.prp" % (self.age_name, chapter, name))
+ self.mgr.WritePage(f, page)
+
+ def _collect_objects(self):
+ for obj in bpy.data.objects:
+ if obj.plasma_object.export:
+ self._objects.append(obj)
+
+ def _grab_age_info(self):
+ self._age_info = bpy.context.scene.world.plasma_age.export()
+ self._age_info.name = self.age_name
+ self.mgr.AddAge(self._age_info)
+
+ def _sanity_check_pages(self):
+ """Ensure all objects are in valid pages and create the Default page if used"""
+ for obj in self._objects:
+ page = obj.plasma_object.page
+ if not page in self._pages and page == "":
+ # This object is in the default page... Init that.
+ for loc in self._pages.values():
+ if not loc.page:
+ self._pages[""] = loc
+ break
+ else:
+ # need to create default page
+ self._pages[""] = self._create_page("Default", 0)
+ else:
+ # oh dear...
+ raise ExportError("Object '%s' in undefined page '%s'" % (obj.name, page))
+
+ def _generate_builtins(self):
+ # Find the highest two available negative suffixes for BuiltIn and Textures
+ # This should generally always resolve to -2 and -1
+ suffixes = []; _s = -1
+ while len(suffixes) != 2:
+ for location in self._pages.values():
+ if location.page == _s:
+ break
+ else:
+ suffixes.append(_s)
+ _s -= 1
+
+ # Grunt work...
+ if self.version <= pvMoul:
+ builtin = self._create_page("BuiltIn", suffixes[1], True)
+ pfm = plPythonFileMod("VeryVerySpecialPythonFileMod")
+ pfm.filename = self.age_name
+ self.mgr.AddObject(builtin, pfm)
+ sdlhook = plSceneObject("AgeSDLHook")
+ sdlhook.addModifier(pfm.key)
+ self.mgr.AddObject(builtin, sdlhook)
+ self._pages["BuiltIn"] = builtin
+
+ if self._op.use_texture_page:
+ textures = self._create_page("Textures", suffixes[0], True)
+ self._pages["Textures"] = textures
+ else:
+ self._pages["Textures"] = None # probably easier than looping to find it
diff --git a/korman/operators/__init__.py b/korman/operators/__init__.py
index d44002a..0219f4e 100644
--- a/korman/operators/__init__.py
+++ b/korman/operators/__init__.py
@@ -13,4 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see .
-from .op_world import *
+from . import op_export as exporter
+from . import op_world as world
+
+def register():
+ exporter.register()
diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py
new file mode 100644
index 0000000..ec5477f
--- /dev/null
+++ b/korman/operators/op_export.py
@@ -0,0 +1,93 @@
+# 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
+import os, os.path
+from .. import exporter
+
+class ExportOperator(bpy.types.Operator):
+ """Exports ages for Cyan Worlds' Plasma Engine"""
+
+ bl_idname = "export.plasma_age"
+ bl_label = "Export Age"
+ bl_options = {"BLOCKING"}
+
+ # Export specific props
+ version = bpy.props.EnumProperty(
+ name="Version",
+ description="Version of the Plasma Engine to target",
+ default="pvPots", # This should be changed when moul is easier to target!
+ items=[
+ ("pvPrime", "Ages Beyond Myst (63.11)", "Targets the original Uru (Live) game", 2),
+ ("pvPots", "Path of the Shell (63.12)", "Targets the most recent offline expansion pack", 1),
+ ("pvMoul", "Myst Online: Uru Live (70)", "Targets the most recent online game", 0),
+ # I see no reason to even offer Myst 5...
+ ]
+ )
+ optimize = bpy.props.BoolProperty(name="Optimize Age",
+ description="Optimizes your age to run faster. This slows down export.")
+ use_texture_page = bpy.props.BoolProperty(name="Use Texture Page",
+ description="Exports all textures to a dedicated Textures page",
+ default=True)
+ filepath = bpy.props.StringProperty(subtype="FILE_PATH")
+
+ def _set_error(self, value):
+ self.has_reports = True
+ self.report = ({"ERROR"}, value)
+ error = property(fset=_set_error) # Can't use decorators here :(
+
+ @classmethod
+ def poll(cls, context):
+ if context.object is not None:
+ return context.scene.render.engine == "PLASMA_GAME"
+
+ def execute(self, context):
+ # Before we begin, do some basic sanity checking...
+ if self.filepath == "":
+ self.error = "No file specified"
+ return {"CANCELLED"}
+ else:
+ dir = os.path.split(self.filepath)[0]
+ if not os.path.exists(dir):
+ try:
+ os.mkdirs(dir)
+ except os.error:
+ self.error = "Failed to create export directory"
+ return {"CANCELLED"}
+
+ # Separate blender operator and actual export logic for my sanity
+ e = exporter.Exporter(self)
+ try:
+ e.run()
+ except exporter.ExportError as error:
+ self.error = str(error)
+ return {"CANCELLED"}
+ else:
+ return {"FINISHED"}
+
+ def invoke(self, context, event):
+ # Called when a user hits "export" from the menu
+ # We will prompt them for the export info, then call execute()
+ 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)")
+def register():
+ bpy.types.INFO_MT_file_export.append(menu_cb)
diff --git a/korman/properties/prop_world.py b/korman/properties/prop_world.py
index 98cf5ac..0ca84ce 100644
--- a/korman/properties/prop_world.py
+++ b/korman/properties/prop_world.py
@@ -15,6 +15,7 @@
import bpy
from bpy.props import *
+from PyHSPlasma import *
class PlasmaFni(bpy.types.PropertyGroup):
bl_idname = "world.plasma_fni"
@@ -56,8 +57,8 @@ class PlasmaPage(bpy.types.PropertyGroup):
self.last_name = self.name
return None
- # Empty page names not allowed!
- if self.name == "":
+ # There are some obviously bad page names
+ if self.name.lower() in ("", "builtin", "default", "textures"):
self.make_default_name(self.seq_suffix)
return None
@@ -111,4 +112,12 @@ class PlasmaAge(bpy.types.PropertyGroup):
type=PlasmaPage)
# Implementation details
- active_page_index = IntProperty(name="Active Page Index")
\ No newline at end of file
+ active_page_index = IntProperty(name="Active Page Index")
+
+ def export(self):
+ age = plAgeInfo()
+ age.dayLength = self.day_length
+ age.seqPrefix = self.seq_prefix
+
+ # Pages are added to the ResManager in the main exporter
+ return age