diff --git a/korman/exporter.py b/korman/exporter.py deleted file mode 100644 index 2ef91ba..0000000 --- a/korman/exporter.py +++ /dev/null @@ -1,222 +0,0 @@ -# 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.path -from PyHSPlasma import * - -class ExportError(Exception): - def __init__(self, value="Undefined Export Error"): - super(Exception, self).__init__(value) - - -class Exporter: - def __init__(self, op): - self._op = op # Blender operator - - # This stuff doesn't need to be static - self._nodes = {} - self._objects = [] - self._pages = {} - - def add_object(self, pl, bl=None, loc=None): - """Automates adding a converted Blender object to our Plasma Resource Manager""" - assert (bl or loc) - if loc: - location = loc - else: - location = self._pages[bl.plasma_object.page] - self.mgr.AddObject(location, pl) - node = self._nodes[location] - if node: # All objects must be in the scene node - if isinstance(pl, plSceneObject): - f = node.addSceneObject - else: - f = node.addPoolObject - f(pl.key) - - - @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._nodes[location] = node - self.mgr.AddObject(location, node) - else: - self._nodes[location] = None - 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 - - # Step 3: Export all the things! - self._export_scene_objects() - - # Step 4: FINALLY. Let's write the PRPs and crap. - self.mgr.WriteAge(self._op.filepath, self._age_info) - self._write_fni() - self._write_pages() - - def _collect_objects(self): - for obj in bpy.data.objects: - if obj.plasma_object.enabled: - self._objects.append(obj) - - def _grab_age_info(self): - age = bpy.context.scene.world.plasma_age - self._age_info = plAgeInfo() - self._age_info.dayLength = age.day_length - self._age_info.lingerTime = 180 # this is fairly standard - self._age_info.name = self.age_name - self._age_info.seqPrefix = age.seq_prefix - self._age_info.startDateTime = age.start_time - 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 page in self._pages: - # good. keep trying. - continue - elif 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 and self._op.save_state: - builtin = self._create_page("BuiltIn", suffixes[1], True) - pfm = plPythonFileMod("VeryVerySpecialPythonFileMod") - pfm.filename = self.age_name - self.mgr.AddObject(builtin, pfm) # add_object has lots of overhead - 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 - - def _export_scene_objects(self): - for bl_obj in self._objects: - # Normally, we'd pass off to the property for export logic, but we need to - # do meshes here, so let's stay local until it's modifier time - so = plSceneObject(bl_obj.name) - self.add_object(pl=so, bl=bl_obj) - # TODO: export mesh - # TODO: export plasma modifiers - - def _write_fni(self): - if self.version <= pvMoul: - enc = plEncryptedStream.kEncXtea - else: - enc = plEncryptedStream.kEncAES - fname = os.path.join(os.path.split(self._op.filepath)[0], "%s.fni" % self.age_name) - stream = plEncryptedStream() - stream.open(fname, fmWrite, enc) - - # Write out some stuff - fni = bpy.context.scene.world.plasma_fni - stream.writeLine("Graphics.Renderer.Fog.SetClearColor %f %f %f" % tuple(fni.clear_color)) - if fni.fog_method != "none": - stream.writeLine("Graphics.Renderer.Fog.SetDefColor %f %f %f" % tuple(fni.fog_color)) - if fni.fog_method == "linear": - stream.writeLine("Graphics.Renderer.Fog.SetDefLinear %f %f %f" % (fni.fog_start, fni.fog_end, fni.fog_density)) - elif fni.fog_method == "exp2": - stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 %f %f" % (fni.fog_end, fni.fog_density)) - stream.writeLine("Graphics.Renderer.Setyon %f" % fni.yon) - stream.close() - - def _write_pages(self): - 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) diff --git a/korman/exporter/__init__.py b/korman/exporter/__init__.py new file mode 100644 index 0000000..8623197 --- /dev/null +++ b/korman/exporter/__init__.py @@ -0,0 +1,21 @@ +# 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.path +from PyHSPlasma import * + +from .convert import * +from .explosions import * diff --git a/korman/exporter/convert.py b/korman/exporter/convert.py new file mode 100644 index 0000000..789cce2 --- /dev/null +++ b/korman/exporter/convert.py @@ -0,0 +1,80 @@ +# 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.path +from PyHSPlasma import * + +from . import explosions +from . import manager + +class Exporter: + # These are objects that we need to export as plSceneObjects + _objects = [] + + def __init__(self, op): + self._op = op # Blender export operator + + @property + def age_name(self): + return os.path.splitext(os.path.split(self._op.filepath)[1])[0] + + def run(self): + # Step 0: Init export resmgr and stuff + self.mgr = manager.ExportManager(globals()[self._op.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: Create the age info and the pages + self._export_age_info() + + # Step 2.9: Ensure that all Plasma Objects are in a valid page + # This creates the default page if it is used + self.mgr.sanity_check_object_pages(self.age_name, self._objects) + + # Step 3: Export all the things! + self._export_scene_objects() + + # Step 4: FINALLY. Let's write the PRPs and crap. + self.mgr.save_age(self._op.filepath) + + def _collect_objects(self): + for obj in bpy.data.objects: + if obj.plasma_object.enabled: + self._objects.append(obj) + + def _export_age_info(self): + # Make life slightly easier... + age_info = bpy.context.scene.world.plasma_age + age_name = self.age_name + mgr = self.mgr + + # Generate the plAgeInfo + mgr.AddAge(age_info.export(self)) + + # Create all the pages we need + for page in age_info.pages: + mgr.create_page(age_name, page.name, page.seq_suffix) + mgr.create_builtins(age_name, self._op.use_texture_page) + + def _export_scene_objects(self): + for bl_obj in self._objects: + # Naive export all Plasma Objects. They will be responsible for calling back into the + # exporter to find/create drawable meshes, materials, etc. Not sure if this design will + # work well, but we're going to go with it for now. + bl_obj.plasma_object.export(self, bl_obj) \ No newline at end of file diff --git a/korman/exporter/explosions.py b/korman/exporter/explosions.py new file mode 100644 index 0000000..0198c0a --- /dev/null +++ b/korman/exporter/explosions.py @@ -0,0 +1,35 @@ +# 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 . + +class ExportError(Exception): + def __init__(self, value="Undefined Export Error"): + super(Exception, self).__init__(value) + + +class UndefinedPageError(ExportError): + mistakes = {} + + def __init__(self): + super(ExportError, self).__init__("You have objects in pages that do not exist!") + + def add(self, page, obj): + if page not in self.mistakes: + self.mistakes[page] = [obj,] + else: + self.mistakes[page].append(obj) + + def raise_if_error(self): + if self.mistakes: + raise self diff --git a/korman/exporter/manager.py b/korman/exporter/manager.py new file mode 100644 index 0000000..c992f67 --- /dev/null +++ b/korman/exporter/manager.py @@ -0,0 +1,191 @@ +# 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.path +from PyHSPlasma import * + +from . import explosions + +class ExportManager: + """Friendly resource-managing helper class.""" + + _nodes = {} + _pages = {} + + def __init__(self, version): + self.mgr = plResManager() + self.mgr.setVer(version) + + # cheap inheritance + for i in dir(self.mgr): + if not hasattr(self, i): + setattr(self, i, getattr(self.mgr, i)) + + def AddAge(self, info): + # There's only ever one age being exported, so hook the call and hang onto the plAgeInfo + # We'll save from our reference later. Forget grabbing this from C++ + self._age_info = info + self.mgr.AddAge(info) + + def add_object(self, pl, name=None, bl=None, loc=None): + """Automates adding a converted Blender object to our Plasma Resource Manager""" + assert (bl or loc) + if loc: + location = loc + else: + location = self._pages[bl.plasma_object.page] + + # pl can be a class or an instance. + # This is one of those "sanity" things to ensure we don't suddenly + # passing around the key of an uninitialized object. + if isinstance(pl, type(object)): + assert name or bl + if name is None: + name = bl.name + pl = pl(name) + + self.mgr.AddObject(location, pl) + node = self._nodes[location] + if node: # All objects must be in the scene node + if isinstance(pl, plSceneObject): + node.addSceneObject(pl.key) + else: + # FIXME: determine which types belong here... + # Probably anything that's not a plModifier or a plBitmap... + # Remember that the idea is that Plasma needs to deliver refs to load the age. + # It's harmless to have too many refs here (though the file size will be big, heh) + node.addPoolObject(pl.key) + return pl # we may have created it... + + def create_builtins(self, age, textures): + # BuiltIn.prp + # FIXME: Only gen this if the artist wants it + builtin = self.create_page(age, "BuiltIn", -1, True) + pfm = self.add_object(plPythonFileMod, name="VeryVerySpecialPythonFileMod", loc=builtin) + sdl = self.add_object(plSceneObject, name="AgeSDLHook", loc=builtin) + sdl.addModifier(pfm.key) + + # Textures.prp + if textures: + self.create_page(age, "Textures", -2, True) + + def create_page(self, age, name, id, builtin=False): + location = plLocation(self.mgr.getVer()) + 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 = age + info.page = name + info.location = location + self.mgr.AddPage(info) + + if not builtin: + self._age_info.addPage((name, id, 0)) + if self.getVer() <= pvPots: + node = plSceneNode("{}_District_{}".format(age, name)) + else: + node = plSceneNode("{}_{}".format(age, name)) + self._nodes[location] = node + self.mgr.AddObject(location, node) + else: + self._nodes[location] = None + return location + + def find_key(self, bl_obj, index): + """Given a blender Object and a pCre index, find an exported plKey""" + location = self._pages[bl_obj.plasma_object.page] + + # NOTE: may need to replace with a python side dict for faster lookups + # evaluate when exporter has been fleshed out + for key in self.mgr.getKeys(location, index): + if bl_obj.name == key.name: + return key + return None + + def get_textures_page(self, obj): + """Returns the page that Blender Object obj's textures should be exported to""" + # The point of this is to account for per-page textures... + if "Textures" in self._pages: + return self._pages["Textures"] + else: + return self._pages[obj.plasma_object.page] + + def sanity_check_object_pages(self, age, objects): + """Ensure all objects are in valid pages and create the Default page if used""" + + error = explosions.UndefinedPageError() + for obj in objects: + page = obj.plasma_object.page + if page in self._pages: + # good. keep trying. + continue + elif page == "": + # This object is in the default page... Init that. + for loc in self._pages.values(): + if not loc.page: + self.mgr._pages[""] = loc + break + else: + # need to create default page + self._pages[""] = self.create_page("Default", 0) + else: + error.add(page, obj.name) + error.raise_if_error() + + def save_age(self, path): + relpath, ageFile = os.path.split(path) + ageName = os.path.splitext(ageFile)[0] + + self.mgr.WriteAge(path, self._age_info) + self._write_fni(relpath, ageName) + self._write_pages(relpath, ageName) + + def _write_fni(self, path, ageName): + if self.mgr.getVer() <= pvMoul: + enc = plEncryptedStream.kEncXtea + else: + enc = plEncryptedStream.kEncAES + fname = os.path.join(path, "{}.fni".format(ageName)) + stream = plEncryptedStream() + stream.open(fname, fmWrite, enc) + + # Write out some stuff + fni = bpy.context.scene.world.plasma_fni + stream.writeLine("Graphics.Renderer.Fog.SetClearColor %f %f %f" % tuple(fni.clear_color)) + if fni.fog_method != "none": + stream.writeLine("Graphics.Renderer.Fog.SetDefColor %f %f %f" % tuple(fni.fog_color)) + if fni.fog_method == "linear": + stream.writeLine("Graphics.Renderer.Fog.SetDefLinear %f %f %f" % (fni.fog_start, fni.fog_end, fni.fog_density)) + elif fni.fog_method == "exp2": + stream.writeLine("Graphics.Renderer.Fog.SetDefExp2 %f %f" % (fni.fog_end, fni.fog_density)) + stream.writeLine("Graphics.Renderer.Setyon %f" % fni.yon) + stream.close() + + def _write_pages(self, path, ageName): + 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.mgr.getVer() <= pvMoul: + chapter = "_District_" + else: + chapter = "_" + f = os.path.join(path, "{}{}{}.prp".format(ageName, chapter, name)) + self.mgr.WritePage(f, page) diff --git a/korman/operators/op_export.py b/korman/operators/op_export.py index 069c0bd..f41d0ba 100644 --- a/korman/operators/op_export.py +++ b/korman/operators/op_export.py @@ -57,7 +57,7 @@ class ExportOperator(bpy.types.Operator): def execute(self, context): # Before we begin, do some basic sanity checking... - if self.filepath == "": + if not self.filepath: self.error = "No file specified" return {"CANCELLED"} else: diff --git a/korman/operators/op_world.py b/korman/operators/op_world.py index 0b427b1..f8052f6 100644 --- a/korman/operators/op_world.py +++ b/korman/operators/op_world.py @@ -35,7 +35,7 @@ class PageAddOperator(AgeOperator, bpy.types.Operator): suffixes = {p.seq_suffix for p in age.pages} if suffixes: test = set(range(min(suffixes), max(suffixes))) - missing = test - set(suffixes) + missing = test - suffixes try: suffix = missing.pop() except KeyError: diff --git a/korman/properties/prop_object.py b/korman/properties/prop_object.py index 4efcd6c..aa31d86 100644 --- a/korman/properties/prop_object.py +++ b/korman/properties/prop_object.py @@ -15,6 +15,7 @@ import bpy from bpy.props import * +from PyHSPlasma import * class PlasmaObject(bpy.types.PropertyGroup): def _enabled(self, context): @@ -38,6 +39,15 @@ class PlasmaObject(bpy.types.PropertyGroup): o.plasma_object.page = page.name break + def export(self, exporter, bl_obj): + """Plasma Object Export""" + + # This is where the magic happens... + if self.enabled: + # TODO: Something more useful than a blank object. + exporter.mgr.add_object(plSceneObject, bl=bl_obj) + + enabled = BoolProperty(name="Export", description="Export this as a discrete object", default=False, diff --git a/korman/properties/prop_world.py b/korman/properties/prop_world.py index 0400b63..baf4785 100644 --- a/korman/properties/prop_world.py +++ b/korman/properties/prop_world.py @@ -116,6 +116,15 @@ class PlasmaPage(bpy.types.PropertyGroup): class PlasmaAge(bpy.types.PropertyGroup): + def export(self, exporter): + _age_info = plAgeInfo() + _age_info.dayLength = self.day_length + _age_info.lingerTime = 180 # this is fairly standard + _age_info.name = exporter.age_name + _age_info.seqPrefix = self.seq_prefix + _age_info.startDateTime = self.start_time + return _age_info + day_length = FloatProperty(name="Day Length", description="Length of a day (in hours) on this age", default=30.230000, diff --git a/korman/render.py b/korman/render.py index 7e2c9ee..5cf02a3 100644 --- a/korman/render.py +++ b/korman/render.py @@ -22,9 +22,11 @@ class PlasmaRenderEngine(bpy.types.RenderEngine): pass + # Explicitly whitelist compatible Blender panels... from bl_ui import properties_material properties_material.MATERIAL_PT_context_material.COMPAT_ENGINES.add("PLASMA_GAME") +properties_material.MATERIAL_PT_options.COMPAT_ENGINES.add("PLASMA_GAME") properties_material.MATERIAL_PT_preview.COMPAT_ENGINES.add("PLASMA_GAME") del properties_material