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