Compare commits

...

5 Commits

Author SHA1 Message Date
Adam Johnson f303244481
Bump all actions to current. 3 months ago
Adam Johnson cd5edb064f
Remove outdated information from the README. 3 months ago
Adam Johnson 46a72a6cd8
Merge pull request #406 from Hoikas/debounce_duplicate_note_popups 3 months ago
Adam Johnson 20ccfa87f9
Ensure note popups don't clobber each other. 3 months ago
Adam Johnson 3c5e2d61d5
Debounce GUI object duplication from the Note Popup modifier. 3 months ago
  1. 10
      .github/workflows/ci_build.yml
  2. 12
      README.md
  3. 6
      korman/exporter/convert.py
  4. 92
      korman/exporter/gui.py
  5. 2
      korman/properties/modifiers/anim.py
  6. 2
      korman/properties/modifiers/avatar.py
  7. 2
      korman/properties/modifiers/game_gui.py
  8. 86
      korman/properties/modifiers/gui.py
  9. 2
      korman/properties/modifiers/logic.py
  10. 4
      korman/properties/modifiers/render.py
  11. 2
      korman/properties/modifiers/sound.py

10
.github/workflows/ci_build.yml

@ -30,12 +30,12 @@ jobs:
runs-on: ${{ matrix.cfg.os }}
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: korman
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: "3.7"
architecture: ${{ matrix.cfg.python-arch }}
@ -55,7 +55,7 @@ jobs:
-NoInstaller -NoBlender
- name: Upload Standalone Korman
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: korman-standalone-${{ matrix.cfg.str }}
path: build/package
@ -80,13 +80,13 @@ jobs:
steps:
- name: Checkout Korman
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
path: korman
- name: Download Artifacts
id: download
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
path: artifacts

12
README.md

@ -8,20 +8,14 @@ Dependencies
------------
- [Blender](http://blender3d.org) - 3D modeling software
- [libHSPlasma](https://github.com/H-uru/libhsplasma) - Universal Plasma library used for manipulating data
- [PhysX 2.6 SDK](http://www.nvidia.com/object/physx_archives.html) - optional, required only for exporting
ages to the Myst Online: URU Live format with libHSPlasma.
Building
--------
Korman is written primarily in Python and therefore requires little in the way of compiling. However, Korman
depends on the libHSPlasma Python bindings called "PyHSPlasma". Therefore, you will need to compile libHSPlasma
with python bindings for the platform of your choice. You will need to be certain that you use the same version
of Python that ships with your Blender install. Once you have done this, copy the HSPlasma library and PyHSPlasma
python library into Blender's `python/lib/site-packages`.
See the installer directory for NSIS scripts. You can make a Windows installer by using `makensis
-DPYTHON_DLL=[pythonDllName] Installer.nsi`. Be sure to provide the Visual C++ redistributable and
libHSPlasma libraries. Prebuilt installers will be provided on the Guild of Writers website.
with python bindings for the platform of your choice. A helper script has been provided to compile PyHSPlasma
and all dependency libraries for you on Windows. To build Korman for rapid development, run
`./build.ps1 -Dev -BlenderDir "<path to blender 2.79>"`
Installing
----------

6
korman/exporter/convert.py

@ -229,7 +229,7 @@ class Exporter:
for mod in bl_obj.plasma_modifiers.modifiers:
fn = getattr(mod, "sanity_check", None)
if fn is not None:
fn()
fn(self)
inc_progress()
self.report.msg("... Age is grinning and holding a spatula. Must be OK, then.")
@ -502,7 +502,9 @@ class Exporter:
# Wow, recursively generated objects. Aren't you special?
with indent():
for mod in temporary.plasma_modifiers.modifiers:
mod.sanity_check()
fn = getattr(mod, "sanity_check", None)
if fn is not None:
fn(self)
do_pre_export(temporary)
return temporary

92
korman/exporter/gui.py

@ -32,6 +32,7 @@ from . import utils
if TYPE_CHECKING:
from .convert import Exporter
from .logger import _ExportLogger as ExportLogger
from ..properties.modifiers.game_gui import *
class Clipping(NamedTuple):
@ -48,9 +49,13 @@ class GuiConverter:
if TYPE_CHECKING:
_parent: weakref.ref[Exporter] = ...
_pages: Dict[str, Any] = ...
_mods_exported: Set[str] = ...
def __init__(self, parent: Optional[Exporter] = None):
self._parent = weakref.ref(parent) if parent is not None else None
self._pages = {}
self._mods_exported = set()
# Go ahead and prepare the GUI transparent material for future use.
if parent is not None:
@ -203,6 +208,93 @@ class GuiConverter:
w2c[2, i] *= -1.0
return PostEffectModMatrices(c2w, w2c)
def check_pre_export(self, name: str, **kwargs):
previous = self._pages.setdefault(name, kwargs)
if previous != kwargs:
diff = set(previous.items()) - set(kwargs.items())
raise ExportError(f"GUI Page '{name}' has target modifiers with conflicting settings:\n{diff}")
def create_note_gui(self, gui_page: str, gui_camera: bpy.types.Object):
if not gui_page in self._mods_exported:
guidialog_object = utils.create_empty_object(f"{gui_page}_NoteDialog")
guidialog_object.plasma_object.enabled = True
guidialog_object.plasma_object.page = gui_page
yield guidialog_object
guidialog_mod: PlasmaGameGuiDialogModifier = guidialog_object.plasma_modifiers.gui_dialog
guidialog_mod.enabled = True
guidialog_mod.is_modal = True
if gui_camera is not None:
guidialog_mod.camera_object = gui_camera
else:
# Abuse the GUI Dialog's lookat caLculation to make us a camera that looks at everything
# the artist has placed into the GUI page. We want to do this NOW because we will very
# soon be adding more objects into the GUI page.
camera_object = yield utils.create_camera_object(f"{gui_page}_GUICamera")
camera_object.data.angle = math.radians(45.0)
camera_object.data.lens_unit = "FOV"
visible_objects = [
i for i in self._parent().get_objects(gui_page)
if i.type == "MESH" and i.data.materials
]
camera_object.matrix_world = self.calc_camera_matrix(
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
clipping = self.calc_clipping(
camera_object.matrix_world,
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
camera_object.data.clip_start = clipping.hither
camera_object.data.clip_end = clipping.yonder
guidialog_mod.camera_object = camera_object
# Begin creating the object for the clickoff plane. We want to yield it immediately
# to the exporter in case something goes wrong during the export, allowing the stale
# object to be cleaned up.
click_plane_object = utils.BMeshObject(f"{gui_page}_Exit")
click_plane_object.matrix_world = guidialog_mod.camera_object.matrix_world
click_plane_object.plasma_object.enabled = True
click_plane_object.plasma_object.page = gui_page
yield click_plane_object
# We have a camera on guidialog_mod.camera_object. We will now use it to generate the
# points for the click-off plane button.
# TODO: Allow this to be configurable to 4:3, 16:9, or 21:9?
with ExitStack() as stack:
stack.enter_context(self.generate_camera_render_settings(bpy.context.scene))
toggle = stack.enter_context(helpers.GoodNeighbor())
# Temporarily adjust the clipping plane out to the farthest point we can find to ensure
# that the click-off button ecompasses everything. This is a bit heavy-handed, but if
# you want more refined control, you won't be using this helper.
clipping = max((guidialog_mod.camera_object.data.clip_start, guidialog_mod.camera_object.data.clip_end))
toggle.track(guidialog_mod.camera_object.data, "clip_start", clipping - 0.1)
view_frame = guidialog_mod.camera_object.data.view_frame(bpy.context.scene)
click_plane_object.data.materials.append(self.transparent_material)
with click_plane_object as click_plane_mesh:
verts = [click_plane_mesh.verts.new(i) for i in view_frame]
face = click_plane_mesh.faces.new(verts)
# TODO: Ensure the face is pointing toward the camera!
# I feel like we should be fine by assuming that Blender returns the viewframe
# verts in the correct order, but this is Blender... So test that assumption carefully.
# TODO: Apparently not!
face.normal_flip()
# We've now created the mesh object - handle the GUI Button stuff
click_plane_object.plasma_modifiers.gui_button.enabled = True
# NOTE: We will be using xDialogToggle.py, so we use a special tag ID instead of the
# close dialog procedure.
click_plane_object.plasma_modifiers.gui_control.tag_id = 99
self._mods_exported.add(gui_page)
@contextmanager
def generate_camera_render_settings(self, scene: bpy.types.Scene) -> Iterator[None]:
# Set the render info to basically TV NTSC 4:3, which will set Blender's camera

2
korman/properties/modifiers/anim.py

@ -42,7 +42,7 @@ class ActionModifier:
return None
raise ExportError("'{}': Object has an animation modifier but is not animated".format(bo.name))
def sanity_check(self) -> None:
def sanity_check(self, exporter) -> None:
if not self.id_data.plasma_object.has_animation_data:
raise ExportError("'{}': Has an animation modifier but no animation data.", self.id_data.name)

2
korman/properties/modifiers/avatar.py

@ -189,7 +189,7 @@ class PlasmaSittingBehavior(idprops.IDPropObjectMixin, PlasmaModifierProperties,
# This should be an empty, really...
return True
def sanity_check(self):
def sanity_check(self, exporter):
# 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))

2
korman/properties/modifiers/game_gui.py

@ -70,7 +70,7 @@ class _GameGuiMixin:
def requires_dyntext(self) -> bool:
return False
def sanity_check(self):
def sanity_check(self, exporter):
age: PlasmaAge = bpy.context.scene.world.plasma_age
# Game GUI modifiers must be attached to objects in a GUI page, ONLY

86
korman/properties/modifiers/gui.py

@ -660,7 +660,7 @@ class PlasmaLinkingBookModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz
share.link_input(share_anim_stage, "stage", "stage_refs")
share.link_output(linkingnode, "hosts", "shareBookSeek")
def sanity_check(self):
def sanity_check(self, exporter):
if self.clickable is None:
raise ExportError("{}: Linking Book modifier requires a clickable!", self.id_data.name)
if self.seek_point is None:
@ -724,88 +724,18 @@ class PlasmaNotePopupModifier(PlasmaModifierProperties, PlasmaModifierLogicWiz):
if self.id_data.type == "MESH":
return self.id_data
def sanity_check(self):
def sanity_check(self, exporter: Exporter):
page_type = helpers.get_page_type(self.id_data.plasma_object.page)
if page_type != "room":
raise ExportError(f"Note Popup modifiers should be in a 'room' page, not a '{page_type}' page!")
# It's OK if multiple note popups point to the same GUI page,
# they just need to have the same camera.
exporter.gui.check_pre_export(self.gui_page, pl_id="note_popup", camera=self.gui_camera)
def pre_export(self, exporter: Exporter, bo: bpy.types.Object):
guidialog_object = utils.create_empty_object(f"{self.gui_page}_NoteDialog")
guidialog_object.plasma_object.enabled = True
guidialog_object.plasma_object.page = self.gui_page
yield guidialog_object
guidialog_mod: PlasmaGameGuiDialogModifier = guidialog_object.plasma_modifiers.gui_dialog
guidialog_mod.enabled = True
guidialog_mod.is_modal = True
if self.gui_camera:
guidialog_mod.camera_object = self.gui_camera
else:
# Abuse the GUI Dialog's lookat caLculation to make us a camera that looks at everything
# the artist has placed into the GUI page. We want to do this NOW because we will very
# soon be adding more objects into the GUI page.
camera_object = yield utils.create_camera_object(f"{self.key_name}_GUICamera")
camera_object.data.angle = math.radians(45.0)
camera_object.data.lens_unit = "FOV"
visible_objects = [
i for i in exporter.get_objects(self.gui_page)
if i.type == "MESH" and i.data.materials
]
camera_object.matrix_world = exporter.gui.calc_camera_matrix(
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
clipping = exporter.gui.calc_clipping(
camera_object.matrix_world,
bpy.context.scene,
visible_objects,
camera_object.data.angle
)
camera_object.data.clip_start = clipping.hither
camera_object.data.clip_end = clipping.yonder
guidialog_mod.camera_object = camera_object
# Begin creating the object for the clickoff plane. We want to yield it immediately
# to the exporter in case something goes wrong during the export, allowing the stale
# object to be cleaned up.
click_plane_object = utils.BMeshObject(f"{self.key_name}_Exit")
click_plane_object.matrix_world = guidialog_mod.camera_object.matrix_world
click_plane_object.plasma_object.enabled = True
click_plane_object.plasma_object.page = self.gui_page
yield click_plane_object
# We have a camera on guidialog_mod.camera_object. We will now use it to generate the
# points for the click-off plane button.
# TODO: Allow this to be configurable to 4:3, 16:9, or 21:9?
with ExitStack() as stack:
stack.enter_context(exporter.gui.generate_camera_render_settings(bpy.context.scene))
toggle = stack.enter_context(helpers.GoodNeighbor())
# Temporarily adjust the clipping plane out to the farthest point we can find to ensure
# that the click-off button ecompasses everything. This is a bit heavy-handed, but if
# you want more refined control, you won't be using this helper.
clipping = max((guidialog_mod.camera_object.data.clip_start, guidialog_mod.camera_object.data.clip_end))
toggle.track(guidialog_mod.camera_object.data, "clip_start", clipping - 0.1)
view_frame = guidialog_mod.camera_object.data.view_frame(bpy.context.scene)
click_plane_object.data.materials.append(exporter.gui.transparent_material)
with click_plane_object as click_plane_mesh:
verts = [click_plane_mesh.verts.new(i) for i in view_frame]
face = click_plane_mesh.faces.new(verts)
# TODO: Ensure the face is pointing toward the camera!
# I feel like we should be fine by assuming that Blender returns the viewframe
# verts in the correct order, but this is Blender... So test that assumption carefully.
# TODO: Apparently not!
face.normal_flip()
# We've now created the mesh object - handle the GUI Button stuff
click_plane_object.plasma_modifiers.gui_button.enabled = True
# NOTE: We will be using xDialogToggle.py, so we use a special tag ID instead of the
# close dialog procedure.
click_plane_object.plasma_modifiers.gui_control.tag_id = 99
# The GUI converter will debounce duplicate GUI dialogs.
yield from exporter.gui.create_note_gui(self.gui_page, self.gui_camera)
# Auto-generate a six-foot cube region around the clickable if none was provided.
if self.clickable_region is None:

2
korman/properties/modifiers/logic.py

@ -154,7 +154,7 @@ class PlasmaTelescope(PlasmaModifierProperties, PlasmaModifierLogicWiz):
type=bpy.types.Object,
poll=idprops.poll_camera_objects)
def sanity_check(self):
def sanity_check(self, exporter):
if self.camera_object is None:
raise ExportError(f"'{self.id_data.name}': Telescopes must specify a camera!")

4
korman/properties/modifiers/render.py

@ -120,7 +120,7 @@ class PlasmaBlendMod(PlasmaModifierProperties):
for i in (j.blend_onto for j in self.dependencies if j.blend_onto is not None and j.enabled):
yield i
def sanity_check(self):
def sanity_check(self, exporter):
if self.has_circular_dependency:
raise ExportError("'{}': Circular Render Dependency detected!".format(self.id_data.name))
@ -770,7 +770,7 @@ class PlasmaLocalizedTextModifier(PlasmaModifierProperties, PlasmaModifierLogicW
def localization_set(self):
return "DynaTexts"
def sanity_check(self):
def sanity_check(self, exporter):
if self.texture is None:
raise ExportError("'{}': Localized Text modifier requires a texture", self.id_data.name)

2
korman/properties/modifiers/sound.py

@ -533,7 +533,7 @@ class PlasmaSoundEmitter(PlasmaModifierProperties):
stereize_left = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
stereize_right = PointerProperty(type=bpy.types.Object, options={"HIDDEN", "SKIP_SAVE"})
def sanity_check(self):
def sanity_check(self, exporter):
modifiers = self.id_data.plasma_modifiers
# Sound emitters can potentially export sounds to more than one emitter SceneObject. Currently,

Loading…
Cancel
Save