Browse Source

Update LogicWiz modifiers for deferred logic export.

In a previous changeset, the Advanced Logic modifier was changed to
defer exporting logic until the very end of the export process. This
means that many nodes are designed with the assumption that all
non-logic PRP objects are fully exported by the time they are exported.
LogicWiz modifiers, however, violated this assumption by exporting
during the main export() phase.

So, this adds a new `pre_export()` phase for all modifiers that lets
them generate logic trees and even entirely new Plasma Objects safely.
Futher, old LogicWiz modifiers have been tweaked to not leak junked
objects if the export fails in the middle of those modifiers.
pull/273/head
Adam Johnson 3 years ago
parent
commit
b36218aa57
Signed by: Hoikas
GPG Key ID: 0B6515D6FF6F271E
  1. 64
      korman/exporter/convert.py
  2. 5
      korman/exporter/utils.py
  3. 2
      korman/operators/op_modifier.py
  4. 17
      korman/properties/modifiers/avatar.py
  5. 39
      korman/properties/modifiers/base.py
  6. 104
      korman/properties/modifiers/gui.py
  7. 2
      korman/properties/modifiers/logic.py
  8. 4
      korman/properties/modifiers/region.py

64
korman/exporter/convert.py

@ -15,8 +15,12 @@
import bpy import bpy
from collections import defaultdict
from contextlib import ExitStack
import functools
from pathlib import Path from pathlib import Path
from ..helpers import TemporaryObject
from ..korlib import ConsoleToggler from ..korlib import ConsoleToggler
from PyHSPlasma import * from PyHSPlasma import *
@ -41,12 +45,12 @@ class Exporter:
self._op = op # Blender export operator self._op = op # Blender export operator
self._objects = [] self._objects = []
self.actors = set() self.actors = set()
self.want_node_trees = {} self.want_node_trees = defaultdict(set)
self.exported_nodes = {} self.exported_nodes = {}
def run(self): def run(self):
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report: with ConsoleToggler(self._op.show_console), log(self._op.filepath) as self.report, ExitStack() as self.exit_stack:
# Step 0: Init export resmgr and stuff # Step 0: Init export resmgr and stuff
self.mgr = manager.ExportManager(self) self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self) self.mesh = mesh.MeshConverter(self)
@ -64,6 +68,7 @@ class Exporter:
self.mesh.add_progress_presteps(self.report) self.mesh.add_progress_presteps(self.report)
self.report.progress_add_step("Collecting Objects") self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Verify Competence") self.report.progress_add_step("Verify Competence")
self.report.progress_add_step("Touching the Intangible")
self.report.progress_add_step("Harvesting Actors") self.report.progress_add_step("Harvesting Actors")
if self._op.lighting_method != "skip": if self._op.lighting_method != "skip":
etlight.LightBaker.add_progress_steps(self.report) etlight.LightBaker.add_progress_steps(self.report)
@ -89,6 +94,10 @@ class Exporter:
# is no ruddy funny business going on. # is no ruddy funny business going on.
self._check_sanity() self._check_sanity()
# Step 2.2: Run through all the objects again and ask them to "pre_export" themselves.
# In other words, generate any ephemeral Blender objects that need to be exported.
self._pre_export_scene_objects()
# Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships # Step 2.5: Run through all the objects we collected in Step 2 and see if any relationships
# that the artist made requires something to have a CoordinateInterface # that the artist made requires something to have a CoordinateInterface
self._harvest_actors() self._harvest_actors()
@ -381,6 +390,57 @@ class Exporter:
proc(self, bl_obj, sceneobject) proc(self, bl_obj, sceneobject)
inc_progress() inc_progress()
def _pre_export_scene_objects(self):
self.report.progress_advance()
self.report.progress_range = len(self._objects)
inc_progress = self.report.progress_increment
self.report.msg("\nGenerating export dependency objects...")
# New objects may be generate during this process; they will be appended at the end.
new_objects = []
@functools.singledispatch
def handle_temporary(temporary, parent):
raise RuntimeError("Temporary object of type '{}' generated by '{}' was unhandled".format(temporary.__class__, parent.name))
@handle_temporary.register(bpy.types.Object)
def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.objects.remove))
self.report.msg("'{}': generated Object '{}' (Plasma Object: {})", parent.name,
temporary.name, temporary.plasma_object.enabled, indent=1)
if temporary.plasma_object.enabled:
new_objects.append(temporary)
# If the object is marked as a Plasma Object, be sure that we go into the same page
# as the requestor, unless the modifier decided it knows better.
if not temporary.plasma_object.property_set("page"):
temporary.plasma_object.page = parent.plasma_object.page
# Wow, recursively generated objects. Aren't you special?
for mod in temporary.plasma_modifiers.modifiers:
mod.sanity_check()
do_pre_export(temporary)
@handle_temporary.register(bpy.types.NodeTree)
def _(temporary, parent):
self.exit_stack.enter_context(TemporaryObject(temporary, bpy.data.node_groups.remove))
self.report.msg("'{}' generated NodeTree '{}'", parent.name, temporary.name)
if temporary.bl_idname == "PlasmaNodeTree":
parent_so = self.mgr.find_create_object(plSceneObject, bl=parent)
self.want_node_trees[temporary.name].add((parent, parent_so))
def do_pre_export(bo):
for mod in bo.plasma_modifiers.modifiers:
for i in filter(None, mod.pre_export(self, bo)):
handle_temporary(i, bo)
for bl_obj in self._objects:
do_pre_export(bl_obj)
inc_progress()
self.report.msg("... {} new object(s) were generated!", len(new_objects))
self._objects += new_objects
def _pack_ancillary_python(self): def _pack_ancillary_python(self):
texts = bpy.data.texts texts = bpy.data.texts
self.report.progress_advance() self.report.progress_advance()

5
korman/exporter/utils.py

@ -15,7 +15,8 @@
import bmesh import bmesh
import bpy import bpy
from typing import Callable
from typing import Callable, Iterator, Tuple
from contextlib import contextmanager from contextlib import contextmanager
from PyHSPlasma import * from PyHSPlasma import *
@ -73,7 +74,7 @@ def bmesh_temporary_object(name : str, factory : Callable, page_name : str=None)
bpy.context.scene.objects.unlink(obj) bpy.context.scene.objects.unlink(obj)
@contextmanager @contextmanager
def bmesh_object(name : str): def bmesh_object(name: str) -> Iterator[Tuple[bpy.types.Object, bmesh.types.BMesh]]:
"""Creates an object and mesh that will be removed if the context is exited """Creates an object and mesh that will be removed if the context is exited
due to an error""" due to an error"""
mesh = bpy.data.meshes.new(name) mesh = bpy.data.meshes.new(name)

2
korman/operators/op_modifier.py

@ -346,7 +346,7 @@ class ModifierLogicWizOperator(ModifierOperator, bpy.types.Operator):
print("WRN: This modifier is not actually enabled!") print("WRN: This modifier is not actually enabled!")
start = time.process_time() start = time.process_time()
mod.logicwiz(obj) mod.create_logic(obj)
end = time.process_time() end = time.process_time()
print("\nLogicWiz finished in {:.2f} seconds".format(end-start)) print("\nLogicWiz finished in {:.2f} seconds".format(end-start))
return {"FINISHED"} return {"FINISHED"}

17
korman/properties/modifiers/avatar.py

@ -115,19 +115,9 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
description="How far away we will tolerate the avatar facing the clickable", description="How far away we will tolerate the avatar facing the clickable",
min=-180, max=180, default=45) min=-180, max=180, default=45)
def export(self, exporter, bo, so):
# The user absolutely MUST specify a clickable or this won't export worth crap.
if self.clickable_object is None:
raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name))
# Generate the logic nodes now
with self.generate_logic(bo) as tree:
tree.export(exporter, bo, so)
def harvest_actors(self): def harvest_actors(self):
if self.facing_enabled: if self.facing_enabled:
return (self.clickable_object.name,) yield self.clickable_object.name
return ()
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
nodes = tree.nodes nodes = tree.nodes
@ -177,3 +167,8 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
def requires_actor(self): def requires_actor(self):
# This should be an empty, really... # This should be an empty, really...
return True return True
def sanity_check(self):
# The user absolutely MUST specify a clickable or this won't export worth crap.
if self.clickable_object is None:
raise ExportError("'{}': Sitting Behavior's clickable object is invalid".format(self.key_name))

39
korman/properties/modifiers/base.py

@ -13,10 +13,11 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>. # along with Korman. If not, see <http://www.gnu.org/licenses/>.
import abc
import bpy import bpy
from bpy.props import * from bpy.props import *
from contextlib import contextmanager
import abc
from typing import Generator
class PlasmaModifierProperties(bpy.types.PropertyGroup): class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property @property
@ -49,6 +50,13 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def enabled(self): def enabled(self):
return self.display_order >= 0 return self.display_order >= 0
def export(self, exporter, bo, so):
"""This is the main phase of the modifier export where most, if not all, PRP objects should
be generated. No new Blender objects should be created unless their lifespan is constrained
to the duration of this method.
"""
pass
@property @property
def face_sort(self): def face_sort(self):
"""Indicates that the geometry's faces should be sorted by the engine""" """Indicates that the geometry's faces should be sorted by the engine"""
@ -72,6 +80,14 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
Drawables that will render in the same pass""" Drawables that will render in the same pass"""
return False return False
def pre_export(self, exporter, bo: bpy.types.Object) -> Generator:
"""This is the first phase of the modifier export; allowing modifiers to create additonal
objects or logic nodes to be used by the exporter. To do so, overload this method
and yield any Blender ID from your method. That ID will then be exported and deleted
when the export completes. PRP objects should generally not be exported in this phase.
"""
yield
@property @property
def requires_actor(self): def requires_actor(self):
"""Indicates if this modifier requires the object to be a movable actor""" """Indicates if this modifier requires the object to be a movable actor"""
@ -96,25 +112,30 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
class PlasmaModifierLogicWiz: class PlasmaModifierLogicWiz:
@contextmanager def convert_logic(self, bo, **kwargs):
def generate_logic(self, bo, **kwargs): """Creates, converts, and returns an unmanaged NodeTree for this logic wizard. If the wizard
fails during conversion, the temporary tree is deleted for you. However, on success, you
are responsible for removing the tree from Blender, if applicable."""
name = kwargs.pop("name", self.key_name) name = kwargs.pop("name", self.key_name)
assert not "tree" in kwargs assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree") tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
kwargs["tree"] = tree kwargs["tree"] = tree
try: try:
self.logicwiz(bo, **kwargs) self.logicwiz(bo, **kwargs)
yield tree except:
finally:
bpy.data.node_groups.remove(tree) bpy.data.node_groups.remove(tree)
raise
else:
return tree
@abc.abstractmethod @abc.abstractmethod
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
pass pass
def export_logic(self, exporter, bo, so, **kwargs): def pre_export(self, exporter, bo):
with self.generate_logic(bo, **kwargs) as tree: """Default implementation of the pre_export phase for logic wizards that simply triggers
tree.export(exporter, bo, so) the logic nodes to be created and for their export to be scheduled."""
yield self.convert_logic(bo)
class PlasmaModifierUpgradable: class PlasmaModifierUpgradable:

104
korman/properties/modifiers/gui.py

@ -25,8 +25,6 @@ from ...addon_prefs import game_versions
from ...exporter import ExportError, utils from ...exporter import ExportError, utils
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from ... import idprops from ... import idprops
from ...helpers import TemporaryObject
journal_pfms = { journal_pfms = {
pvPots : { pvPots : {
@ -198,7 +196,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
get=_get_translation, set=_set_translation, get=_get_translation, set=_set_translation,
options=set()) options=set())
def export(self, exporter, bo, so): def pre_export(self, exporter, bo):
our_versions = (globals()[j] for j in self.versions) our_versions = (globals()[j] for j in self.versions)
version = exporter.mgr.getVer() version = exporter.mgr.getVer()
if version not in our_versions: if version not in our_versions:
@ -217,32 +215,21 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
exporter.locman.add_journal(self.key_name, i.language, i.text_id, indent=2) exporter.locman.add_journal(self.key_name, i.language, i.text_id, indent=2)
if self.clickable_region is None: if self.clickable_region is None:
# Create a region for the clickable's condition with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm):
rgn_mesh = bpy.data.meshes.new("{}_Journal_ClkRgn".format(self.key_name))
self.temp_rgn = bpy.data.objects.new("{}_Journal_ClkRgn".format(self.key_name), rgn_mesh)
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=(6.0)) bmesh.ops.create_cube(bm, size=(6.0))
bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.location - self.temp_rgn.location), space=self.temp_rgn.matrix_world, verts=bm.verts) bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.location - rgn_obj.location),
bm.to_mesh(rgn_mesh) space=rgn_obj.matrix_world, verts=bm.verts)
bm.free() rgn_obj.plasma_object.enabled = True
rgn_obj.hide_render = True
# No need to enable the object as a Plasma object; we're exported automatically as part of the node tree. yield rgn_obj
# It does need a page, however, so we'll put it in the same place as the journal object itself.
self.temp_rgn.plasma_object.page = bo.plasma_object.page
bpy.context.scene.objects.link(self.temp_rgn)
else: else:
# Use the region provided # Use the region provided
self.temp_rgn = self.clickable_region rgn_obj = self.clickable_region
# Generate the logic nodes # Generate the logic nodes
with self.generate_logic(bo, age_name=exporter.age_name, version=version) as tree: yield self.convert_logic(bo, age_name=exporter.age_name, rgn_obj=rgn_obj, version=version)
tree.export(exporter, bo, so)
# Get rid of our temporary clickable region def logicwiz(self, bo, tree, age_name, rgn_obj, version):
if self.clickable_region is None:
bpy.context.scene.objects.unlink(self.temp_rgn)
def logicwiz(self, bo, tree, age_name, version):
nodes = tree.nodes nodes = tree.nodes
# Assign journal script based on target version # Assign journal script based on target version
@ -261,13 +248,13 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
journalnode.update() journalnode.update()
if version <= pvPots: if version <= pvPots:
self._create_pots_nodes(bo, nodes, journalnode, age_name) self._create_pots_nodes(bo, nodes, journalnode, age_name, rgn_obj)
else: else:
self._create_moul_nodes(bo, nodes, journalnode, age_name) self._create_moul_nodes(bo, nodes, journalnode, age_name, rgn_obj)
def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name): def _create_pots_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj):
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = self.temp_rgn clickable_region.region_object = rgn_obj
facing_object = nodes.new("PlasmaFacingTargetNode") facing_object = nodes.new("PlasmaFacingTargetNode")
facing_object.directional = False facing_object.directional = False
@ -295,9 +282,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
height.link_output(journalnode, "pfm", "BookHeight") height.link_output(journalnode, "pfm", "BookHeight")
height.value_float = self.book_scale_h / 100.0 height.value_float = self.book_scale_h / 100.0
def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name): def _create_moul_nodes(self, clickable_object, nodes, journalnode, age_name, rgn_obj):
clickable_region = nodes.new("PlasmaClickableRegionNode") clickable_region = nodes.new("PlasmaClickableRegionNode")
clickable_region.region_object = self.temp_rgn clickable_region.region_object = rgn_obj
facing_object = nodes.new("PlasmaFacingTargetNode") facing_object = nodes.new("PlasmaFacingTargetNode")
facing_object.directional = False facing_object.directional = False
@ -456,22 +443,33 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
default=255, default=255,
subtype="UNSIGNED") subtype="UNSIGNED")
def export(self, exporter, bo, so): def _check_version(self, *args) -> bool:
our_versions = (globals()[j] for j in self.versions) our_versions = frozenset((globals()[j] for j in self.versions))
version = exporter.mgr.getVer() return frozenset(args) & our_versions
if version not in our_versions:
def pre_export(self, exporter, bo):
if not self._check_version(exporter.mgr.getVer()):
# We aren't needed here # We aren't needed here
exporter.report.port("Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.", exporter.report.port("Object '{}' has a LinkingBookMod not enabled for export to the selected engine. Skipping.",
bo.name, indent=2) self.id_data.name, indent=2)
return return
if self.clickable is None: # Auto-generate a six-foot cube region around the clickable if none was provided.
raise ExportError("{}: Linking Book modifier requires a clickable!", bo.name) if self.clickable_region is None:
with utils.bmesh_object("{}_LinkingBook_ClkRgn".format(self.key_name)) as (rgn_obj, bm):
bmesh.ops.create_cube(bm, size=(6.0))
rgn_offset = mathutils.Matrix.Translation(self.clickable.location - bo.location)
bmesh.ops.transform(bm, matrix=rgn_offset, space=bo.matrix_world, verts=bm.verts)
rgn_obj.plasma_object.enabled = True
rgn_obj.hide_render = True
yield rgn_obj
else:
rgn_obj = self.clickable_region
if self.seek_point is None: yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer(), region=rgn_obj)
raise ExportError("{}: Linking Book modifier requires a seek point!", bo.name)
if version <= pvPots: def export(self, exporter, bo, so):
if self._check_version(pvPrime, pvPots):
# Create ImageLibraryMod in which to store the Cover, Linking Panel, and Stamp images # Create ImageLibraryMod in which to store the Cover, Linking Panel, and Stamp images
ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name) ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name)
@ -481,23 +479,9 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
exporter.mesh.material.export_prepared_image(owner=ilmod, image=image, exporter.mesh.material.export_prepared_image(owner=ilmod, image=image,
allowed_formats={"JPG", "PNG"}, extension="hsm") allowed_formats={"JPG", "PNG"}, extension="hsm")
# Auto-generate a six-foot cube region around the clickable if none was provided. def harvest_actors(self):
if self.clickable_region is None: if self.seek_point is not None:
# Create a region for the clickable's condition yield self.seek_point.name
def _make_rgn(bm):
bmesh.ops.create_cube(bm, size=(6.0))
rgn_offset = mathutils.Matrix.Translation(self.clickable.location - bo.location)
bmesh.ops.transform(bm, matrix=rgn_offset, space=bo.matrix_world, verts=bm.verts)
with utils.bmesh_temporary_object("{}_LinkingBook_ClkRgn".format(self.key_name),
_make_rgn, self.clickable.plasma_object.page) as temp_rgn:
# Generate the logic nodes
self.export_logic(exporter, bo, so, age_name=exporter.age_name, version=version,
region=temp_rgn)
else:
# Generate the logic nodes
self.export_logic(exporter, bo, so, age_name=exporter.age_name, version=version,
region=self.clickable_region)
def logicwiz(self, bo, tree, age_name, version, region): def logicwiz(self, bo, tree, age_name, version, region):
nodes = tree.nodes nodes = tree.nodes
@ -655,6 +639,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
linking_panel_name.value = self.link_destination if self.link_destination else self.age_name linking_panel_name.value = self.link_destination if self.link_destination else self.age_name
linking_panel_name.link_output(linkingnode, "pfm", "TargetAge") linking_panel_name.link_output(linkingnode, "pfm", "TargetAge")
def harvest_actors(self): def sanity_check(self):
if self.seek_point is not None: if self.clickable is None:
yield self.seek_point.name raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name)
if self.seek_point is None:
raise ExportError("{}: Linking Book modifier requires a seek point!", self.id_data.name)

2
korman/properties/modifiers/logic.py

@ -60,7 +60,7 @@ class PlasmaAdvancedLogic(PlasmaModifierProperties):
raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.name)) raise ExportError("'{}': Advanced Logic is missing a node tree for '{}'".format(bo.name, i.name))
# Defer node tree export until all trees are harvested. # Defer node tree export until all trees are harvested.
exporter.want_node_trees.setdefault(i.node_tree.name, set()).add((bo, so)) exporter.want_node_trees[i.node_tree.name].add((bo, so))
def harvest_actors(self): def harvest_actors(self):
actors = set() actors = set()

4
korman/properties/modifiers/region.py

@ -143,10 +143,6 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
items=bounds_types, items=bounds_types,
default="hull") default="hull")
def export(self, exporter, bo, so):
with self.generate_logic(bo) as tree:
tree.export(exporter, bo, so)
def logicwiz(self, bo, tree): def logicwiz(self, bo, tree):
nodes = tree.nodes nodes = tree.nodes

Loading…
Cancel
Save