Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions korman/exporter/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
# You should have received a copy of the GNU General Public License
# along with Korman. If not, see <http://www.gnu.org/licenses/>.

from __future__ import annotations

import bpy

from collections import defaultdict
Expand Down Expand Up @@ -70,6 +72,7 @@ def __init__(self, op):
self.actors = set()
self.want_node_trees = defaultdict(set)
self.exported_nodes = {}
self._claims: dict[str, dict[str, bpy.types.bpy_struct]] = defaultdict(dict)

def run(self):
log = logger.ExportVerboseLogger if self._op.verbose else logger.ExportProgressLogger
Expand Down Expand Up @@ -171,6 +174,62 @@ def _bake_static_lighting(self):
if self._op.lighting_method != "skip":
self.oven.bake_static_lighting(self._objects)

def claim_object(
self,
claimant: bpy.types.bpy_struct,
bl: bpy.types.Object,
claim_type: str,
*,
err: str = "",
strict: bool = True
) -> Optional[bpy.types.bpy_struct]:
"""
This is a way to assign that a certain property group or object has claimed a Blender object
for a specific purpose and that object cannot be reused for that purpose. Claims are scoped
by ``claim_type``, so the same object can still be claimed for other, unrelated purposes without
conflict.

If the object is already claimed under this ``claim_type`` by someone else, we will either raise
an ``ExportError`` (if ``strict``) or hand back the existing claimant so the caller can decide what to
do (if not ``strict``). By default, a generic error message is used, but you can supply your own
via ``err`` - it's passed through ``str.format()`` with ``object_name``, ``claim_type``,
``owner_name``, and ``claimant_name`` available for substitution, where ``owner_name`` and
``claimant_name`` are slash-joined paths identifying the existing/new claimant.
"""
def iter_name_parts(obj: bpy.types.bpy_struct):
if not isinstance(obj, bpy.types.ID):
yield obj.id_data.name

name = getattr(obj, "name", None)
if name:
yield name

claim = self._claims.get(bl.name)
if claim is None:
self._claims[bl.name][claim_type] = claimant
return None

prev_claimant = claim.get(claim_type)
if prev_claimant is not None and prev_claimant != claimant:
if strict:
if not err:
err = (
"'{object_name}' has already been claimed as a(n) {claim_type} by "
"'{owner_name}'. '{claimant_name}' cannot claim it again."
)
raise explosions.ExportError(
err.format(
object_name=bl.name,
owner_name="/".join(iter_name_parts(prev_claimant)),
claimant_name="/".join(iter_name_parts(claimant)),
claim_type=claim_type
)
)
return prev_claimant

claim[claim_type] = claimant
return None

def _collect_objects(self):
scene = bpy.context.scene
self.report.progress_advance()
Expand Down
112 changes: 110 additions & 2 deletions korman/nodes/node_conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from .node_core import *
from .node_deprecated import PlasmaVersionedNode
from .. import idprops
from ..ui.ui_list import draw_node_list

class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.types.Node):
bl_category = "CONDITIONS"
Expand Down Expand Up @@ -56,6 +57,10 @@ class PlasmaClickableNode(idprops.IDPropObjectMixin, PlasmaVersionedNode, bpy.ty
"text": "Avatar Facing Target",
"type": "PlasmaFacingTargetSocket",
},
"sdl_hotspot": {
"text": "Hotspot SDL",
"type": "PlasmaClickableSDLSocket",
},
"message": {
"text": "Message",
"type": "PlasmaEnableMessageSocket",
Expand All @@ -75,15 +80,30 @@ def draw_buttons(self, context, layout):
layout.prop(self, "clickable_object", icon="MESH_DATA")
layout.prop(self, "bounds")

def export(self, exporter, parent_bo, parent_so):
def export(self, exporter: Exporter, parent_bo: bpy.types.Object, parent_so: plSceneObject):
clickable_bo, clickable_so = self._get_objects(exporter, parent_so)
if clickable_bo is None:
clickable_bo = parent_bo

# It doesn't make sense to have the same object be used in multiple clickables.
# This can lead to surprising results. For example, if you have multiple logic modifiers
# on a single clickable object, any of them being disabled will cause the hotspot to
# vanish for all of them. The SceneInputInterface code assumes that there's only one
# LogicMod per InterfaceInfo, and MOUL's PhysX LOS probes bail if any LogicMod is disabled.
exporter.claim_object(self, clickable_bo, "clickable")

# In PlasmaMAX, the InterfaceInfoModifier isn't created until late in the export process.
# It's basically supposed to be a list of any LogicMod keys that might cause the cursor
# to change from the typical "null" cursor. They do this by collecting the receivers of
# any PickingDetectors and any LogicMods attached to each SceneObject. The justification
# for this is that any single SceneObject can have multiple LogicMods attached. That's
# true in theory, but in practice having multiple LogicMods that interact with the cursor
# won't work due to how the LOS probe is filtered.
interface = self._find_create_object(plInterfaceInfoModifier, exporter, bl=clickable_bo, so=clickable_so)
logicmod = self._find_create_key(plLogicModifier, exporter, bl=clickable_bo, so=clickable_so)
interface.addIntfKey(logicmod)
# Matches data seen in Cyan's PRPs...
# Matches data seen in Cyan's PRPs... See above explanation. This could probably be removed
# but leaving it in for now.
interface.addIntfKey(logicmod)
logicmod = logicmod.object

Expand Down Expand Up @@ -561,6 +581,94 @@ def export_once(self):
return True



class ClickableSDLValue(bpy.types.PropertyGroup):
value = IntProperty(
name="SDL Value",
description="Enable the clickable when the SDL Variable is set to this",
default=1,
options=set()
)


class PlasmaClickableSDLNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaClickableSDLNode"
bl_label = "SDL Hotspot"
bl_width_default = 170

input_sockets: dict[str, dict[str, Any]] = {
"variable": {
"text": "Dependends on SDL",
"type": "PlasmaSDLTriggererSocket",
},
}

output_sockets: dict[str, dict[str, Any]] = {
"satisfies": {
"text": "Satisfies",
"type": "PlasmaClickableSDLSocket",
}
}

states = CollectionProperty(type=ClickableSDLValue)

def init(self, context):
self.states.add()

def draw_buttons(self, context, layout: bpy.types.UILayout):
draw_node_list(
self,
layout,
"states",
self._draw_state,
header="Enable Clickable On",
footer="Add New State"
)

def _draw_state(self, state, layout: bpy.types.UILayout):
layout.alert = any((state.value == i.value for i in self.states if i != state))
layout.prop(state, "value")

def export(self, exporter: Exporter, bo, so: plSceneObject):
# If the artist has removed all of the state values from the node, don't export anything.
# This will (hopefully) be the least surprising option in this case. Remember: Blender
# property collections don't handle implicit truthiness correctly.
if not bool(self.states):
exporter.report.warn("No 'enabled' states specified for clickable, assuming always enabled")
return

# This node may be exported multiple times if the node tree is reused in multiple
# Advanced Logic modifiers. Each time this node is re-exported, the PFM we generate
# is attached to the new host SceneObject. If the clickable node in the reused tree
# suddenly exposes a new LogicMod, we'll slurp that new LogicMod in as an activator.
# The "surprise" here is that this PFM will be attached to the host SceneObject,
# not the SceneObject referenced as the clickable object like the rest of the
# clickable logic objects. Whatever.
pfm = self._find_create_object(plPythonFileMod, exporter, so=so)
if not pfm.filename:
variable_node = self.find_input("variable")
if variable_node is None:
self.raise_error("SDL Variable must be linked!")

pfm.filename = "xAgeSDLIntActEnabler"
self._add_py_parameter(pfm, 1, plPythonParameter.kString, variable_node.variable_name)
# Skipping ID 2 becuase these are the LogicMod keys - cluster them at the end.
self._add_py_parameter(
pfm, 3, plPythonParameter.kString, ",".join(frozenset((str(i.value) for i in self.states)))
)

extant_keys = frozenset((i.value for i in pfm.parameters if i.id == 2))
for clickable_node in self.find_outputs("satisfies"):
logic_key = clickable_node.get_key(exporter, so)
if logic_key not in extant_keys:
self._add_py_parameter(pfm, 2, plPythonParameter.kActivator, logic_key)


class PlasmaClickableSDLSocket(PlasmaNodeSocketBase, bpy.types.NodeSocket):
bl_color = (0.58, 0.65, 0.42, 1.0)


class PlasmaVolumeReportNode(PlasmaNodeBase, bpy.types.Node):
bl_category = "CONDITIONS"
bl_idname = "PlasmaVolumeReportNode"
Expand Down
7 changes: 6 additions & 1 deletion korman/nodes/node_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,18 @@ def update(self):

def _update_init_sockets(self, defs, sockets):
# Create any missing sockets and spawn any required empties.
insert_at = 0
for alias, options in defs.items():
working_sockets = [(i, socket) for i, socket in enumerate(sockets) if socket.alias == alias]
num_sockets = options.get("min_sockets", 1)
num_used = sum((1 for i, socket in working_sockets if socket.is_linked))
if not working_sockets:
for _ in range(num_sockets):
self._spawn_socket(alias, options, sockets)
new_socket_id = len(sockets)
new_socket = self._spawn_socket(alias, options, sockets)
if new_socket_id != insert_at:
sockets.move(new_socket_id, insert_at)
insert_at += 1
else:
last_socket_id = working_sockets[-1][0]
if options.get("spawn_empty", False):
Expand Down
Loading