Browse Source

Merge pull request #273 from Hoikas/logic_persistence

Update LogicWiz modifiers for deferred logic export.
pull/275/head
Adam Johnson 3 years ago committed by GitHub
parent
commit
ff9e0e3eab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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. 51
      korman/properties/modifiers/base.py
  6. 142
      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
from collections import defaultdict
from contextlib import ExitStack
import functools
from pathlib import Path
from ..helpers import TemporaryObject
from ..korlib import ConsoleToggler
from PyHSPlasma import *
@ -41,12 +45,12 @@ class Exporter:
self._op = op # Blender export operator
self._objects = []
self.actors = set()
self.want_node_trees = {}
self.want_node_trees = defaultdict(set)
self.exported_nodes = {}
def run(self):
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
self.mgr = manager.ExportManager(self)
self.mesh = mesh.MeshConverter(self)
@ -64,6 +68,7 @@ class Exporter:
self.mesh.add_progress_presteps(self.report)
self.report.progress_add_step("Collecting Objects")
self.report.progress_add_step("Verify Competence")
self.report.progress_add_step("Touching the Intangible")
self.report.progress_add_step("Harvesting Actors")
if self._op.lighting_method != "skip":
etlight.LightBaker.add_progress_steps(self.report)
@ -89,6 +94,10 @@ class Exporter:
# is no ruddy funny business going on.
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
# that the artist made requires something to have a CoordinateInterface
self._harvest_actors()
@ -381,6 +390,57 @@ class Exporter:
proc(self, bl_obj, sceneobject)
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):
texts = bpy.data.texts
self.report.progress_advance()

5
korman/exporter/utils.py

@ -15,7 +15,8 @@
import bmesh
import bpy
from typing import Callable
from typing import Callable, Iterator, Tuple
from contextlib import contextmanager
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)
@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
due to an error"""
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!")
start = time.process_time()
mod.logicwiz(obj)
mod.create_logic(obj)
end = time.process_time()
print("\nLogicWiz finished in {:.2f} seconds".format(end-start))
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",
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):
if self.facing_enabled:
return (self.clickable_object.name,)
return ()
yield self.clickable_object.name
def logicwiz(self, bo, tree):
nodes = tree.nodes
@ -177,3 +167,8 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
def requires_actor(self):
# This should be an empty, really...
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))

51
korman/properties/modifiers/base.py

@ -13,10 +13,11 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.
import abc
import bpy
from bpy.props import *
from contextlib import contextmanager
import abc
from typing import Any, Dict, Generator
class PlasmaModifierProperties(bpy.types.PropertyGroup):
@property
@ -49,6 +50,13 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
def enabled(self):
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
def face_sort(self):
"""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"""
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
def requires_actor(self):
"""Indicates if this modifier requires the object to be a movable actor"""
@ -96,25 +112,42 @@ class PlasmaModifierProperties(bpy.types.PropertyGroup):
class PlasmaModifierLogicWiz:
@contextmanager
def generate_logic(self, bo, **kwargs):
def convert_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)
assert not "tree" in kwargs
tree = bpy.data.node_groups.new(name, "PlasmaNodeTree")
kwargs["tree"] = tree
try:
self.logicwiz(bo, **kwargs)
yield tree
finally:
except:
bpy.data.node_groups.remove(tree)
raise
else:
return tree
def _create_python_file_node(self, tree, filename: str, attributes: Dict[str, Any]) -> bpy.types.Node:
pfm_node = tree.nodes.new("PlasmaPythonFileNode")
with pfm_node.NoUpdate():
pfm_node.filename = filename
for attr in attributes:
new_attr = pfm_node.attributes.add()
new_attr.attribute_id = attr["id"]
new_attr.attribute_type = attr["type"]
new_attr.attribute_name = attr["name"]
pfm_node.update()
return pfm_node
@abc.abstractmethod
def logicwiz(self, bo, tree):
pass
def export_logic(self, exporter, bo, so, **kwargs):
with self.generate_logic(bo, **kwargs) as tree:
tree.export(exporter, bo, so)
def pre_export(self, exporter, bo):
"""Default implementation of the pre_export phase for logic wizards that simply triggers
the logic nodes to be created and for their export to be scheduled."""
yield self.convert_logic(bo)
class PlasmaModifierUpgradable:

142
korman/properties/modifiers/gui.py

@ -25,8 +25,6 @@ from ...addon_prefs import game_versions
from ...exporter import ExportError, utils
from .base import PlasmaModifierProperties, PlasmaModifierLogicWiz, PlasmaModifierUpgradable
from ... import idprops
from ...helpers import TemporaryObject
journal_pfms = {
pvPots : {
@ -198,7 +196,7 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
get=_get_translation, set=_set_translation,
options=set())
def export(self, exporter, bo, so):
def pre_export(self, exporter, bo):
our_versions = (globals()[j] for j in self.versions)
version = exporter.mgr.getVer()
if version not in our_versions:
@ -217,57 +215,32 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
exporter.locman.add_journal(self.key_name, i.language, i.text_id, indent=2)
if self.clickable_region is None:
# Create a region for the clickable's condition
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.transform(bm, matrix=mathutils.Matrix.Translation(bo.location - self.temp_rgn.location), space=self.temp_rgn.matrix_world, verts=bm.verts)
bm.to_mesh(rgn_mesh)
bm.free()
# No need to enable the object as a Plasma object; we're exported automatically as part of the node tree.
# 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)
with utils.bmesh_object("{}_Journal_ClkRgn".format(self.key_name)) as (rgn_obj, bm):
bmesh.ops.create_cube(bm, size=(6.0))
bmesh.ops.transform(bm, matrix=mathutils.Matrix.Translation(bo.location - rgn_obj.location),
space=rgn_obj.matrix_world, verts=bm.verts)
rgn_obj.plasma_object.enabled = True
rgn_obj.hide_render = True
yield rgn_obj
else:
# Use the region provided
self.temp_rgn = self.clickable_region
rgn_obj = self.clickable_region
# Generate the logic nodes
with self.generate_logic(bo, age_name=exporter.age_name, version=version) as tree:
tree.export(exporter, bo, so)
# Get rid of our temporary clickable region
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
yield self.convert_logic(bo, age_name=exporter.age_name, rgn_obj=rgn_obj, version=version)
def logicwiz(self, bo, tree, age_name, rgn_obj, version):
# Assign journal script based on target version
journal_pfm = journal_pfms[version]
journalnode = nodes.new("PlasmaPythonFileNode")
with journalnode.NoUpdate():
journalnode.filename = journal_pfm["filename"]
# Manually add required attributes to the PFM
journal_attribs = journal_pfm["attribs"]
for attr in journal_attribs:
new_attr = journalnode.attributes.add()
new_attr.attribute_id = attr["id"]
new_attr.attribute_type = attr["type"]
new_attr.attribute_name = attr["name"]
journalnode.update()
journalnode = self._create_python_file_node(tree, journal_pfm["filename"], journal_pfm["attribs"])
if version <= pvPots:
self._create_pots_nodes(bo, nodes, journalnode, age_name)
self._create_pots_nodes(bo, tree.nodes, journalnode, age_name, rgn_obj)
else:
self._create_moul_nodes(bo, nodes, journalnode, age_name)
self._create_moul_nodes(bo, tree.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.region_object = self.temp_rgn
clickable_region.region_object = rgn_obj
facing_object = nodes.new("PlasmaFacingTargetNode")
facing_object.directional = False
@ -295,9 +268,9 @@ class PlasmaJournalBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
height.link_output(journalnode, "pfm", "BookHeight")
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.region_object = self.temp_rgn
clickable_region.region_object = rgn_obj
facing_object = nodes.new("PlasmaFacingTargetNode")
facing_object.directional = False
@ -456,22 +429,33 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
default=255,
subtype="UNSIGNED")
def export(self, exporter, bo, so):
our_versions = (globals()[j] for j in self.versions)
version = exporter.mgr.getVer()
if version not in our_versions:
def _check_version(self, *args) -> bool:
our_versions = frozenset((globals()[j] for j in self.versions))
return frozenset(args) & our_versions
def pre_export(self, exporter, bo):
if not self._check_version(exporter.mgr.getVer()):
# We aren't needed here
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
if self.clickable is None:
raise ExportError("{}: Linking Book modifier requires a clickable!", bo.name)
# Auto-generate a six-foot cube region around the clickable if none was provided.
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:
raise ExportError("{}: Linking Book modifier requires a seek point!", bo.name)
yield self.convert_logic(bo, age_name=exporter.age_name, version=exporter.mgr.getVer(), region=rgn_obj)
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
ilmod = exporter.mgr.find_create_object(plImageLibMod, so=so, name=self.key_name)
@ -481,46 +465,18 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
exporter.mesh.material.export_prepared_image(owner=ilmod, image=image,
allowed_formats={"JPG", "PNG"}, extension="hsm")
# Auto-generate a six-foot cube region around the clickable if none was provided.
if self.clickable_region is None:
# Create a region for the clickable's condition
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 harvest_actors(self):
if self.seek_point is not None:
yield self.seek_point.name
def logicwiz(self, bo, tree, age_name, version, region):
nodes = tree.nodes
# Assign linking book script based on target version
linking_pfm = linking_pfms[version]
linkingnode = nodes.new("PlasmaPythonFileNode")
with linkingnode.NoUpdate():
linkingnode.filename = linking_pfm["filename"]
# Manually add required attributes to the PFM
linking_attribs = linking_pfm["attribs"]
for attr in linking_attribs:
new_attr = linkingnode.attributes.add()
new_attr.attribute_id = attr["id"]
new_attr.attribute_type = attr["type"]
new_attr.attribute_name = attr["name"]
linkingnode.update()
linkingnode = self._create_python_file_node(tree, linking_pfm["filename"], linking_pfm["attribs"])
if version <= pvPots:
self._create_pots_nodes(bo, nodes, linkingnode, age_name, region)
self._create_pots_nodes(bo, tree.nodes, linkingnode, age_name, region)
else:
self._create_moul_nodes(bo, nodes, linkingnode, age_name, region)
self._create_moul_nodes(bo, tree.nodes, linkingnode, age_name, region)
def _create_pots_nodes(self, clickable_object, nodes, linkingnode, age_name, clk_region):
# Clickable
@ -655,6 +611,8 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
linking_panel_name.value = self.link_destination if self.link_destination else self.age_name
linking_panel_name.link_output(linkingnode, "pfm", "TargetAge")
def harvest_actors(self):
if self.seek_point is not None:
yield self.seek_point.name
def sanity_check(self):
if self.clickable is None:
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))
# 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):
actors = set()

4
korman/properties/modifiers/region.py

@ -143,10 +143,6 @@ class PlasmaFootstepRegion(PlasmaModifierProperties, PlasmaModifierLogicWiz):
items=bounds_types,
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):
nodes = tree.nodes

Loading…
Cancel
Save