# 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 <http://www.gnu.org/licenses/>.
from __future__ import annotations
import bpy
from bpy . props import *
from PyHSPlasma import *
from typing import *
from . node_core import *
from . . properties . modifiers . physics import subworld_types
from . . properties . modifiers . region import footstep_surfaces , footstep_surface_ids
from . . exporter import ExportError
from . . import idprops
if TYPE_CHECKING :
from . . exporter import Exporter
class PlasmaMessageSocketBase ( PlasmaNodeSocketBase ) :
bl_color = ( 0.004 , 0.282 , 0.349 , 1.0 )
class PlasmaMessageSocket ( PlasmaMessageSocketBase , bpy . types . NodeSocket ) :
pass
class PlasmaMessageNode ( PlasmaNodeBase ) :
input_sockets : dict [ str , dict [ str , Any ] ] = {
" sender " : {
" text " : " Sender " ,
" type " : " PlasmaMessageSocket " ,
" valid_link_sockets " : " PlasmaMessageSocket " ,
" spawn_empty " : True ,
} ,
}
@property
def has_callbacks ( self ) :
""" This message does not have callbacks that can be waited on by a Responder """
return False
class PlasmaMessageWithCallbacksNode ( PlasmaMessageNode ) :
output_sockets : dict [ str , dict [ str , str ] ] = {
" msgs " : {
" can_link " : " can_link_callback " ,
" text " : " Send On Completion " ,
" type " : " PlasmaMessageSocket " ,
" valid_link_sockets " : " PlasmaMessageSocket " ,
} ,
}
@property
def can_link_callback ( self ) :
""" Determines if a callback message can be linked to this socket """
# Node Graphs enable us to draw lots of fancy logic, unfortunately, not
# everything that can potentially be represented in a node tree can be
# exported to URU in a way that will actually work. Responder commands can
# wait on other responder commands, but the way they are executed in Plasma is
# serialized. It's really a list of commands that are executed until a wait
# is encountered. At that time, Plasma waits and resumes running the list when
# the wait callback is received.
# So what does this mean???
# It means that only one "branch" of message nodes can have waits.
def check_for_callbacks ( parent_node , child_node ) :
for sibling_node in parent_node . find_outputs ( " msgs " ) :
if sibling_node == child_node :
continue
if getattr ( sibling_node , " has_linked_callbacks " , False ) :
return True
for grandparent_node in parent_node . find_inputs ( " sender " ) :
return check_for_callbacks ( grandparent_node , parent_node )
return False
for sender_node in self . find_inputs ( " sender " ) :
if check_for_callbacks ( sender_node , self ) :
return False
return True
@property
def has_callbacks ( self ) :
""" This message has callbacks that can be waited on by a Responder """
return True
@property
def has_linked_callbacks ( self ) :
return self . find_output ( " msgs " ) is not None
class PlasmaAnimCmdMsgNode ( idprops . IDPropMixin , PlasmaMessageWithCallbacksNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaAnimCmdMsgNode "
bl_label = " Animation Command "
bl_width_default = 190
anim_type = EnumProperty ( name = " Type " ,
description = " Animation type to affect " ,
items = [ ( " OBJECT " , " Object " , " Mesh Action " ) ,
( " TEXTURE " , " Texture " , " Texture Action " ) ] ,
default = " OBJECT " )
def _poll_texture ( self , value ) :
# must be a legal option... but is it a member of this material... or, if no material,
# any of the materials attached to the object?
if self . target_material is not None :
return value . name in self . target_material . texture_slots
elif self . target_object is not None :
for i in ( slot . material for slot in self . target_object . material_slots if slot and slot . material ) :
if value in ( slot . texture for slot in i . texture_slots if slot and slot . texture ) :
return True
return False
else :
return True
def _poll_material ( self , value ) :
# Don't filter materials by texture - this would (potentially) result in surprising UX
# in that you would have to clear the texture selection before being able to select
# certain materials.
if self . target_object is not None :
object_materials = ( slot . material for slot in self . target_object . material_slots if slot and slot . material )
return value in object_materials
return True
target_object = PointerProperty ( name = " Object " ,
description = " Target object " ,
type = bpy . types . Object )
target_material = PointerProperty ( name = " Material " ,
description = " Target material " ,
type = bpy . types . Material ,
poll = _poll_material )
target_texture = PointerProperty ( name = " Texture " ,
description = " Target texture " ,
type = bpy . types . Texture ,
poll = _poll_texture )
go_to = EnumProperty ( name = " Go To " ,
description = " Where should the animation start? " ,
items = [ ( " kGoToBegin " , " Beginning " , " The beginning " ) ,
( " kGoToLoopBegin " , " Loop Beginning " , " The beginning of the active loop " ) ,
( " CURRENT " , " (Don ' t Change) " , " The current position " ) ,
( " kGoToEnd " , " Ending " , " The end " ) ,
( " kGoToLoopEnd " , " Loop Ending " , " The end of the active loop " ) ] ,
default = " CURRENT " )
action = EnumProperty ( name = " Action " ,
description = " What do you want the animation to do? " ,
items = [ ( " kContinue " , " Play " , " Plays the animation " ) ,
( " kPlayToPercent " , " Play to Percent " , " Plays the animation until a given percent is complete " ) ,
( " kPlayToTime " , " Play to Frame " , " Plays the animation up to a given frame number " ) ,
( " kStop " , " Stop " , " Stops the animation " , ) ,
( " kToggleState " , " Toggle " , " Toggles between Play and Stop " ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the animation ' s playing state " ) ] ,
default = " CURRENT " )
play_direction = EnumProperty ( name = " Direction " ,
description = " Which direction do you want to play from? " ,
items = [ ( " kSetForwards " , " Forward " , " Play forwards " ) ,
( " kSetBackwards " , " Backwards " , " Play backwards " ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the play direction " ) ] ,
default = " CURRENT " )
play_to_percent = IntProperty ( name = " Play To " ,
description = " Percentage at which to stop the animation " ,
subtype = " PERCENTAGE " ,
min = 0 , max = 100 , default = 50 )
play_to_frame = IntProperty ( name = " Play To " ,
description = " Frame at which to stop the animation " ,
min = 0 )
def _set_loop_name ( self , context ) :
""" Updates loop_begin and loop_end when the loop name is changed """
pass
looping = EnumProperty ( name = " Looping " ,
description = " Is the animation looping? " ,
items = [ ( " kSetLooping " , " Yes " , " The animation is looping " , ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the loop status " ) ,
( " kSetUnLooping " , " No " , " The animation is NOT looping " ) ] ,
default = " CURRENT " )
loop_name = StringProperty ( name = " Active Loop " ,
description = " Name of the active loop " ,
update = _set_loop_name )
loop_begin = IntProperty ( name = " Loop Begin " ,
description = " Frame number at which the loop begins " ,
min = 0 )
loop_end = IntProperty ( name = " Loop End " ,
description = " Frame number at which the loop ends " ,
min = 0 )
event = EnumProperty ( name = " Callback " ,
description = " Event upon which to callback the Responder " ,
items = [ ( " kEnd " , " End " , " When the action ends " ) ,
( " NONE " , " (None) " , " Don ' t notify the Responder at all " ) ,
( " kStop " , " Stop " , " When the action is stopped by a message " ) ] ,
default = " kEnd " )
# Blender memory workaround
_ENTIRE_ANIMATION = " (Entire Animation) "
def _get_anim_names ( self , context ) :
if self . anim_type == " OBJECT " :
items = [ ( anim . animation_name , anim . animation_name , " " )
for anim in self . target_object . plasma_modifiers . animation . subanimations ]
elif self . anim_type == " TEXTURE " :
if self . target_texture is not None :
items = [ ( anim . animation_name , anim . animation_name , " " )
for anim in self . target_texture . plasma_layer . subanimations ]
elif self . target_material is not None or self . target_object is not None :
if self . target_material is None :
materials = ( i . material for i in self . target_object . material_slots if i and i . material )
else :
materials = ( self . target_material , )
layer_props = ( i . texture . plasma_layer for mat in materials for i in mat . texture_slots if i and i . texture )
all_anims = frozenset ( ( anim . animation_name for i in layer_props for anim in i . subanimations ) )
items = [ ( i , i , " " ) for i in all_anims ]
else :
items = [ ( PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , " " ) ]
else :
raise RuntimeError ( )
# We always want "(Entire Animation)", if it exists, to be the first item.
entire = items . index ( ( PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , " " ) )
if entire not in ( - 1 , 0 ) :
items . pop ( entire )
items . insert ( 0 , ( PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , PlasmaAnimCmdMsgNode . _ENTIRE_ANIMATION , " " ) )
return items
anim_name = EnumProperty ( name = " Animation " ,
description = " Name of the animation to control " ,
items = _get_anim_names ,
options = set ( ) )
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " anim_type " )
col = layout . column ( )
if self . anim_type == " OBJECT " :
col . alert = self . target_object is None
else :
col . alert = not any ( ( self . target_object , self . target_material , self . target_texture ) )
col . prop ( self , " target_object " )
if self . anim_type != " OBJECT " :
col . prop ( self , " target_material " )
col . prop ( self , " target_texture " )
col . prop ( self , " anim_name " )
layout . prop ( self , " go_to " )
layout . prop ( self , " action " )
layout . prop ( self , " play_direction " )
if self . action == " kPlayToPercent " :
layout . prop ( self , " play_to_percent " )
elif self . action == " kPlayToTime " :
layout . prop ( self , " play_to_frame " )
layout . prop ( self , " looping " )
col = layout . column ( )
col . enabled = self . looping != " CURRENT "
if self . anim_type != " OBJECT " :
loops = None
else :
loops = None if self . target_object is None else self . target_object . plasma_modifiers . animation_loop
if loops is not None and loops . enabled :
layout . prop_search ( self , " loop_name " , loops , " loops " , icon = " PMARKER_ACT " )
else :
layout . prop ( self , " loop_begin " )
layout . prop ( self , " loop_end " )
layout . prop ( self , " event " )
def convert_callback_message ( self , exporter , so , msg , target , wait ) :
cb = plEventCallbackMsg ( )
cb . addReceiver ( target )
cb . event = globals ( ) [ self . event ]
cb . user = wait
msg . addCallback ( cb )
msg . setCmd ( plAnimCmdMsg . kAddCallbacks , True )
def convert_message ( self , exporter , so ) :
msg = plAnimCmdMsg ( )
# We're either sending this off to an AGMasterMod or a LayerAnim
obj = self . target_object
if self . anim_type == " OBJECT " :
if obj is None :
self . raise_error ( " target object must be specified " )
if not obj . plasma_object . has_animation_data :
self . raise_error ( " invalid animation " )
target = ( exporter . animation . get_animation_key ( obj ) , )
else :
material = self . target_material
texture = self . target_texture
if obj is None and material is None and texture is None :
self . raise_error ( " At least one of: target object, material, texture MUST be specified " )
target = exporter . mesh . material . get_texture_animation_key ( obj , material , texture , self . anim_name )
target = [ i for i in target if not isinstance ( i . object , ( plAgeGlobalAnim , plLayerSDLAnimation ) ) ]
if not target :
self . raise_error ( " No controllable animations were found. " )
for i in target :
msg . addReceiver ( i )
# Check the enum properties to see what commands we need to add
for prop in ( self . go_to , self . action , self . play_direction , self . looping ) :
cmd = getattr ( plAnimCmdMsg , prop , None )
if cmd is not None :
msg . setCmd ( cmd , True )
# Easier part starts here???
msg . animName = self . anim_name
if self . action == " kPlayToPercent " :
msg . time = self . play_to_percent
elif self . action == " kPlayToTime " :
msg . time = exporter . animation . convert_frame_time ( self . play_to_frame )
# Implicit s better than explicit, I guess...
if self . loop_begin != self . loop_end :
# NOTE: loop name is not used in the engine AFAICT
msg . setCmd ( plAnimCmdMsg . kSetLoopBegin , True )
msg . setCmd ( plAnimCmdMsg . kSetLoopEnd , True )
msg . loopBegin = exporter . animation . convert_frame_time ( self . loop_begin )
msg . loopEnd = exporter . animation . convert_frame_time ( self . loop_end )
# Whew, this was crazy
return msg
@property
def has_callbacks ( self ) :
return self . event != " NONE "
@classmethod
def _idprop_mapping ( cls ) :
return { " target_object " : " object_name " ,
" target_material " : " material_name " ,
" target_texture " : " texture_name " }
def _idprop_sources ( self ) :
return { " object_name " : bpy . data . objects ,
" material_name " : bpy . data . materials ,
" texture_name " : bpy . data . textures }
class PlasmaCameraMsgNode ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaCameraMsgNode "
bl_label = " Camera "
bl_width_default = 200
cmd = EnumProperty ( name = " Command " ,
description = " Command to send to the camera system " ,
items = [ ( " push " , " Push Camera " , " Pushes a new camera onto the camera stack and transitions to it " ) ,
( " pop " , " Pop Camera " , " Pops the camera off the camera stack " ) ,
( " disablefp " , " Disable First Person " , " Forces the camera into third person if it is currently in first person and disables first person mode " ) ,
( " enablefp " , " Enable First Person " , " Reenables the first person camera and switches back to it if the player was in first person previously " ) ] ,
options = set ( ) )
camera = PointerProperty ( name = " Camera " ,
type = bpy . types . Object ,
poll = idprops . poll_camera_objects ,
options = set ( ) )
cut = BoolProperty ( name = " Cut Transition " ,
description = " Immediately swap over to the new camera without a transition animation " ,
options = set ( ) )
def convert_message ( self , exporter , so ) :
msg = plCameraMsg ( )
msg . BCastFlags | = plMessage . kLocalPropagate | plMessage . kBCastByType
if self . cmd in { " push " , " pop " } :
if self . camera is not None :
msg . newCam = exporter . mgr . find_create_key ( plSceneObject , bl = self . camera )
# It appears that kRegionPopCamera is unused. pushing is controlled by observing
# the presence of the kResponderTrigger command.
msg . setCmd ( plCameraMsg . kResponderTrigger , self . cmd == " push " )
msg . setCmd ( plCameraMsg . kRegionPushCamera , True )
msg . setCmd ( plCameraMsg . kSetAsPrimary , self . camera is None
or self . camera . data . plasma_camera . settings . primary_camera )
msg . setCmd ( plCameraMsg . kCut , self . cut )
elif self . cmd == " disablefp " :
msg . setCmd ( plCameraMsg . kResponderSetThirdPerson )
elif self . cmd == " enablefp " :
msg . setCmd ( plCameraMsg . kResponderUndoThirdPerson )
else :
raise RuntimeError ( )
return msg
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " cmd " )
if self . cmd in { " push " , " pop " } :
layout . prop ( self , " camera " )
layout . prop ( self , " cut " )
class PlasmaEnableMsgNode ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaEnableMsgNode "
bl_label = " Enable/Disable "
output_sockets : dict [ str , dict [ str , Any ] ] = {
" receivers " : {
" text " : " Send To " ,
" type " : " PlasmaEnableMessageSocket " ,
" valid_link_sockets " : { " PlasmaEnableMessageSocket " , " PlasmaNodeSocketInputGeneral " } ,
} ,
}
cmd = EnumProperty ( name = " Command " ,
description = " How should we affect the object ' s state? " ,
items = [ ( " kDisable " , " Disable " , " Deactivate the object " ) ,
( " kEnable " , " Enable " , " Activate the object " ) ] ,
default = " kEnable " )
settings = EnumProperty ( name = " Affects " ,
description = " Which attributes should we change " ,
items = [ ( " kAudible " , " Audio " , " Sounds played by this object " ) ,
( " kPhysical " , " Physics " , " Physical simulation of the object " ) ,
( " kDrawable " , " Visibility " , " Visible geometry/light of the object " ) ,
( " kModifiers " , " Modifiers " , " Modifiers attached to the object " ) ] ,
options = { " ENUM_FLAG " } ,
default = { " kAudible " , " kDrawable " , " kPhysical " , " kModifiers " } )
bcast_to_children = BoolProperty ( name = " Send to Children " ,
description = " Send the message to objects parented to the object " ,
default = False ,
options = set ( ) )
def convert_message ( self , exporter , so ) :
settings = self . settings
if not settings :
self . raise_error ( " Nothing set to enable/disable " )
receivers = [ ]
for i in self . find_outputs ( " receivers " ) :
key = i . get_key ( exporter , so )
if isinstance ( key , tuple ) :
for j in key :
receivers . append ( j )
else :
receivers . append ( key )
# OK, so, bad news old bean... In versions of the game using Havok physics, plEnableMsg
# does not actually affect the physics. So we have to potentially generate a new message
# for that.
if exporter . mgr . getVer ( ) < = pvPots :
if " kPhysical " in settings :
msg = plSimSuppressMsg ( )
for i in receivers :
msg . addReceiver ( i )
if self . bcast_to_children :
msg . BCastFlags | = plMessage . kPropagateToChildren
msg . suppress = self . cmd == " kDisable "
yield msg
msg = plEnableMsg ( )
for i in receivers :
msg . addReceiver ( i )
msg . setCmd ( getattr ( plEnableMsg , self . cmd ) , True )
# If we have a full house, let's send it to all the SO's generic modifiers as by compressing
# to kAll :) -- And no, this is not a bug. We do put the named types in commands. The types
# bit vector is for raw Plasma class IDs listing which modifier types we prop to if "kByType"
# is a command. Nice flexibility--I have no idea where that's used in Uru though...
# NOTE: kAll will never be set for PotS because enable/disable physicals seems to do nothing.
if settings > = { " kAudible " , " kPhysical " , " kDrawable " } :
msg . setCmd ( plEnableMsg . kAll , True )
else :
for i in settings :
bit = getattr ( plEnableMsg , i , None )
if bit is not None :
msg . setCmd ( bit , True )
# Propagation to modifiers for, for exmple, ladders
if " kModifiers " in settings :
msg . BCastFlags | = plMessage . kPropagateToModifiers
if self . bcast_to_children :
msg . BCastFlags | = plMessage . kPropagateToChildren
yield msg
def draw_buttons ( self , context , layout ) :
layout . row ( align = True ) . prop ( self , " cmd " , expand = True )
layout . prop ( self , " bcast_to_children " )
layout . separator ( )
layout . label ( " Affects: " )
layout . column ( align = True ) . prop ( self , " settings " )
class PlasmaEnableMessageSocket ( PlasmaNodeSocketBase , bpy . types . NodeSocket ) :
bl_color = ( 0.427 , 0.196 , 0.0 , 1.0 )
class PlasmaExcludeRegionMsg ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaExcludeRegionMsg "
bl_label = " Exclude Region "
output_sockets : dict [ str , dict [ str , str ] ] = {
" region " : {
" text " : " Region " ,
" type " : " PlasmaExcludeMessageSocket "
} ,
}
cmd = EnumProperty ( name = " Command " ,
description = " Exclude Region State " ,
items = [ ( " kClear " , " Clear " , " Clear all avatars from the region " ) ,
( " kRelease " , " Release " , " Allow avatars to enter the region " ) ] ,
default = " kClear " )
def convert_message ( self , exporter , so ) :
msg = plExcludeRegionMsg ( )
for i in self . find_outputs ( " region " ) :
msg . addReceiver ( i . get_key ( exporter , so ) )
msg . cmd = getattr ( plExcludeRegionMsg , self . cmd )
return msg
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " cmd " , text = " Cmd " )
class PlasmaLinkToAgeMsg ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaLinkToAgeMsg "
bl_label = " Link to Age "
bl_width_default = 280
rules = EnumProperty ( name = " Rules " ,
description = " Rules describing which age instance to link to " ,
items = [ ( " kOriginalBook " , " Original Age " , " Links to a personally owned instance, creating if none exists " ) ,
( " kOwnedBook " , " Owned Age " , " Links to a personally owned instance, fails if none exists " ) ,
( " kChildAgeBook " , " Child Age " , " Links to an age instance parented to another personal age " ) ,
( " kSubAgeBook " , " Sub Age " , " Links to an age instance owned by the current age instance " ) ,
( " kBasicLink " , " Basic " , " Links to a specific age instance " ) ] )
parent_filename = StringProperty ( name = " Parent Age " ,
description = " Filename of the age that owns the age instance we ' re linking to " )
age_filename = StringProperty ( name = " Age Filename " ,
description = " Filename of the age to link to (eg ' Garden ' " )
age_instance = StringProperty ( name = " Age Instance " ,
description = " Instance name of the age to link to (eg ' Eder Kemo ' ) " )
age_uuid = StringProperty ( name = " Age Guid " ,
description = " Instance GUID to link to (eg ' ea489821-6c35-4bd0-9dae-bb17c585e680 ' ) " )
spawn_title = StringProperty ( name = " Spawn Title " ,
description = " Title of the Spawn Point to use " ,
default = " Default " )
spawn_point = StringProperty ( name = " Spawn Point " ,
description = " Name of the Spawn Point ' s Plasma Object " ,
default = " LinkInPointDefault " )
def convert_message ( self , exporter , so ) :
msg = plLinkToAgeMsg ( )
als = msg . ageLink
ais , spi = als . ageInfo , als . spawnPoint
als . linkingRules = getattr ( plAgeLinkStruct , self . rules )
if self . rules == " kChildAgeBook " :
als . parentAgeFilename = self . parent_filename
ais . ageFilename = self . age_filename
ais . ageInstanceName = self . age_instance if self . age_instance else self . age_filename
if self . rules == " kBasicLink " :
try :
ais . ageInstanceGuid = self . age_uuid
except ValueError :
self . raise_error ( " Age Instance GUID is not a valid UUID " )
spi . title = self . spawn_title
spi . spawnPt = self . spawn_point
link_oneshot = self . _find_link_oneshot ( self )
if link_oneshot is not None :
msg . linkEffects . linkInAnimName = link_oneshot . animation
return msg
def _find_link_oneshot ( self , node , state = None ) :
if state is None :
state = set ( )
state . add ( node )
# Recursively search the responder tree for what avatar animation (OneShot) we are blocking
# on when linking. We'll continue playing that when the link-in completes.
for child_node in node . find_inputs ( " sender " ) :
if child_node in state :
continue
if isinstance ( child_node , PlasmaOneShotMsgNode ) :
if child_node . has_callbacks :
return child_node
elif isinstance ( child_node , PlasmaMessageNode ) and child_node :
return self . _find_link_oneshot ( child_node , state )
return None
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " rules " )
if self . rules == " kChildAgeBook " :
layout . prop ( self , " parent_filename " )
layout . separator ( )
layout . prop ( self , " age_filename " )
layout . prop ( self , " age_instance " )
if self . rules == " kBasicLink " :
layout . prop ( self , " age_uuid " )
layout . separator ( )
layout . prop ( self , " spawn_title " )
layout . prop ( self , " spawn_point " )
class PlasmaOneShotMsgNode ( idprops . IDPropObjectMixin , PlasmaMessageWithCallbacksNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaOneShotMsgNode "
bl_label = " One Shot "
bl_width_default = 210
pos_object = PointerProperty ( name = " Position " ,
description = " Object defining the OneShot position " ,
type = bpy . types . Object )
seek = EnumProperty ( name = " Seek " ,
description = " How the avatar should approach the OneShot position " ,
items = [ ( " SMART " , " Smart Seek " , " Let the engine figure out the best path " ) ,
( " DUMB " , " Seek " , " Shuffle to the OneShot position " ) ,
( " NONE " , " Warp " , " Warp the avatar to the OneShot position " ) ] ,
default = " SMART " )
animation = StringProperty ( name = " Animation " ,
description = " Name of the animation the avatar should execute " )
marker = StringProperty ( name = " Marker " ,
description = " Name of the marker specifying when to notify the Responder " )
drivable = BoolProperty ( name = " Drivable " ,
description = " Player retains control of the avatar during the OneShot " ,
default = False )
reversable = BoolProperty ( name = " Reversable " ,
description = " Player can reverse the OneShot " ,
default = False )
def convert_callback_message ( self , exporter , so , msg , target , wait ) :
msg . addCallback ( self . marker , target , wait )
def convert_message ( self , exporter , so ) :
msg = plOneShotMsg ( )
msg . addReceiver ( self . get_key ( exporter , so ) )
return msg
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " animation " , text = " Anim " )
layout . prop ( self , " marker " )
row = layout . row ( )
row . prop ( self , " drivable " )
row . prop ( self , " reversable " )
layout . prop ( self , " pos_object " , icon = " EMPTY_DATA " )
layout . prop ( self , " seek " )
def export ( self , exporter , bo , so ) :
# Note: we purposefully allow this to proceed because plOneShotMod is a MultiMod, so we
# want all referencing SOs to get a copy of the modifier.
oneshotmod = self . get_key ( exporter , so ) . object
oneshotmod . animName = self . animation
oneshotmod . drivable = self . drivable
oneshotmod . reversable = self . reversable
oneshotmod . smartSeek = self . seek == " SMART "
oneshotmod . noSeek = self . seek == " NONE "
oneshotmod . seekDuration = 1.0
def get_key ( self , exporter , so ) :
if self . pos_object is not None :
pos_so = exporter . mgr . find_create_object ( plSceneObject , bl = self . pos_object )
return self . _find_create_key ( plOneShotMod , exporter , so = pos_so )
else :
return self . _find_create_key ( plOneShotMod , exporter , so = so )
def harvest_actors ( self ) :
if self . pos_object :
yield self . pos_object . name
@property
def has_callbacks ( self ) :
return bool ( self . marker )
@property
def requires_actor ( self ) :
return self . pos_object is None
@classmethod
def _idprop_mapping ( cls ) :
return { " pos_object " : " pos " }
class PlasmaOneShotCallbackSocket ( PlasmaMessageSocketBase , bpy . types . NodeSocket ) :
marker = StringProperty ( name = " Marker " ,
description = " Marker specifying the time at which to send a callback to this Responder " )
def draw ( self , context , layout , node , text ) :
layout . prop ( self , " marker " )
class PlasmaSceneObjectMsgRcvrNode ( idprops . IDPropObjectMixin , PlasmaNodeBase , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaSceneObjectMsgRcvrNode "
bl_label = " Send To Object "
bl_width_default = 190
input_sockets : dict [ str , dict [ str , Any ] ] = {
" message " : {
" text " : " Message " ,
" type " : " PlasmaNodeSocketInputGeneral " ,
" valid_link_sockets " : { " PlasmaEnableMessageSocket " } ,
" spawn_empty " : True ,
} ,
}
target_object = PointerProperty ( name = " Object " ,
description = " Object to send the message to " ,
type = bpy . types . Object )
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " target_object " )
def get_key ( self , exporter , so ) :
bo = self . target_object
if bo is None :
self . raise_error ( " target object must be specified " )
ref_so_key = exporter . mgr . find_create_key ( plSceneObject , bl = bo )
return ref_so_key
@classmethod
def _idprop_mapping ( cls ) :
return { " target_object " : " object_name " }
class PlasmaSoundMsgNode ( idprops . IDPropObjectMixin , PlasmaMessageWithCallbacksNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaSoundMsgNode "
bl_label = " Sound "
bl_width_default = 190
def _poll_sound_emitters ( self , value ) :
return value . plasma_modifiers . soundemit . enabled
emitter_object = PointerProperty ( name = " Object " ,
description = " Sound emitter object " ,
type = bpy . types . Object ,
poll = _poll_sound_emitters )
sound_name = StringProperty ( name = " Sound " ,
description = " Sound datablock " )
go_to = EnumProperty ( name = " Go To " ,
description = " Where should the sound start? " ,
items = [ ( " BEGIN " , " Beginning " , " The beginning " ) ,
( " CURRENT " , " (Don ' t Change) " , " The current position " ) ,
( " TIME " , " Time " , " The time specified in seconds " ) ] ,
default = " CURRENT " )
looping = EnumProperty ( name = " Looping " ,
description = " Is the sound looping? " ,
items = [ ( " kSetLooping " , " Yes " , " The sound is looping " , ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the loop status " ) ,
( " kUnSetLooping " , " No " , " The sound is NOT looping " ) ] ,
default = " CURRENT " )
action = EnumProperty ( name = " Action " ,
description = " What do you want the sound to do? " ,
items = [ ( " kPlay " , " Play " , " Plays the sound " ) ,
( " kStop " , " Stop " , " Stops the sound " , ) ,
( " kToggleState " , " Toggle " , " Toggles between Play and Stop " ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the sound ' s playing state " ) ] ,
default = " CURRENT " )
volume = EnumProperty ( name = " Volume " ,
description = " What should happen to the volume? " ,
items = [ ( " MUTE " , " Mute " , " Mutes the volume " ) ,
( " CURRENT " , " (Don ' t Change) " , " Don ' t change the volume " ) ,
( " CUSTOM " , " Custom " , " Manually specify the volume " ) ] ,
default = " CURRENT " )
time = FloatProperty ( name = " Time " ,
description = " Time in seconds to begin playing from " ,
min = 0.0 , default = 0.0 ,
options = set ( ) , subtype = " TIME " , unit = " TIME " )
volume_pct = IntProperty ( name = " Volume Level " ,
description = " Volume to play the sound " ,
min = 0 , max = 100 , default = 100 ,
options = set ( ) ,
subtype = " PERCENTAGE " )
event = EnumProperty ( name = " Callback " ,
description = " Event upon which to callback the Responder " ,
items = [ ( " kEnd " , " End " , " When the sound ends " ) ,
( " NONE " , " (None) " , " Don ' t notify the Responder at all " ) ,
( " kStop " , " Stop " , " When the sound is stopped by a message " ) ] ,
default = " NONE " )
def convert_callback_message ( self , exporter , so , msg , target , wait ) :
assert not self . is_random_sound , " Callbacks are not available for random sounds "
cb = plEventCallbackMsg ( )
cb . addReceiver ( target )
cb . event = globals ( ) [ self . event ]
cb . user = wait
msg . addCallback ( cb )
msg . setCmd ( plSoundMsg . kAddCallbacks )
def convert_message ( self , exporter , so ) :
if self . emitter_object is None :
self . raise_error ( " Sound emitter must be set " )
soundemit = self . emitter_object . plasma_modifiers . soundemit
if not soundemit . enabled :
self . raise_error ( " ' {} ' is not a valid Sound Emitter " . format ( self . emitter_object . name ) )
if self . is_random_sound :
yield from self . _convert_random_sound_msg ( exporter , so )
else :
yield from self . _convert_sound_emitter_msg ( exporter , so )
def _convert_random_sound_msg ( self , exporter , so ) :
# Yas, plAnimCmdMsg
msg = plAnimCmdMsg ( )
msg . addReceiver ( exporter . mgr . find_key ( plRandomSoundMod , bl = self . emitter_object ) )
if self . action == " kPlay " :
msg . setCmd ( plAnimCmdMsg . kContinue , True )
elif self . action == " kStop " :
msg . setCmd ( plAnimCmdMsg . kStop , True )
elif self . action == " kToggleState " :
msg . setCmd ( plAnimCmdMsg . kToggleState , True )
if self . volume != " CURRENT " :
# No, you are not imagining things...
msg . setCmd ( plAnimCmdMsg . kSetSpeed , True )
msg . speed = self . volume_pct / 100.0 if self . volume == " CUSTOM " else 0.0
yield msg
def _convert_sound_emitter_msg ( self , exporter , so ) :
soundemit = self . emitter_object . plasma_modifiers . soundemit
# Always test the specified audible for validity
if self . sound_name and soundemit . sounds . get ( self . sound_name , None ) is None :
self . raise_error ( " Invalid Sound ' {} ' requested from Sound Emitter ' {} ' " . format ( self . sound_name , self . emitter_object . name ) )
# Remember that 3D stereo sounds are exported as two emitters...
# But, if we only have one sound attached, who cares, we can just address the message to all
msg = plSoundMsg ( )
sound_keys = tuple ( soundemit . get_sound_keys ( exporter , self . sound_name ) )
indices = frozenset ( ( i [ 1 ] for i in sound_keys ) )
if indices :
assert len ( indices ) == 1 , " Only one sound index should result from a sound emitter "
msg . index = next ( iter ( indices ) )
else :
msg . index = - 1
for i in sound_keys :
msg . addReceiver ( i [ 0 ] )
# NOTE: There are a number of commands in Plasma's enumeration that do nothing.
# This is what I determine to be the most useful and functional subset...
# Please see plAudioInterface::MsgReceive for more details.
if self . go_to == " BEGIN " :
msg . setCmd ( plSoundMsg . kGoToTime )
msg . time = 0.0
elif self . go_to == " TIME " :
msg . setCmd ( plSoundMsg . kGoToTime )
msg . time = self . time
if self . volume == " MUTE " :
msg . setCmd ( plSoundMsg . kSetVolume )
msg . volume = 0.0
elif self . volume == " CUSTOM " :
msg . setCmd ( plSoundMsg . kSetVolume )
msg . volume = self . volume_pct / 100.0
if self . looping != " CURRENT " :
msg . setCmd ( getattr ( plSoundMsg , self . looping ) )
if self . action != " CURRENT " :
sound = soundemit . sounds . get ( self . sound_name , None )
if sound is not None and sound . is_3d_stereo :
exporter . report . warn ( f " ' { self . id_data . name } ' Node ' { self . name } ' : 3D Stereo sounds should not be started or stopped by messages - they may get out of sync. " )
msg . setCmd ( getattr ( plSoundMsg , self . action ) )
# This used to potentially result in multiple messages. Not anymore!
# However, I'm leaving it as a yield for now to avoid potentially breaking something.
yield msg
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " emitter_object " )
# Random Sound emitters can only control the entire emitter object, not the
# individual sounds.
random = self . is_random_sound
if not random :
if self . emitter_object is not None :
soundemit = self . emitter_object . plasma_modifiers . soundemit
if soundemit . enabled :
layout . prop_search ( self , " sound_name " , soundemit , " sounds " , icon = " SOUND " )
else :
layout . label ( " Not a Sound Emitter " , icon = " ERROR " )
layout . prop ( self , " go_to " )
if self . go_to == " TIME " :
layout . prop ( self , " time " )
if not random and self . emitter_object is not None :
soundemit = self . emitter_object . plasma_modifiers . soundemit
sound = soundemit . sounds . get ( self . sound_name , None )
action_on_3d_stereo = sound is not None and sound . is_3d_stereo and self . action != " CURRENT "
layout . alert = action_on_3d_stereo
layout . prop ( self , " action " )
layout . alert = False
else :
layout . prop ( self , " action " )
if self . volume == " CUSTOM " :
layout . prop ( self , " volume_pct " )
if not random :
layout . prop ( self , " looping " )
layout . prop ( self , " volume " )
if not random :
layout . prop ( self , " event " )
@property
def has_callbacks ( self ) :
if not self . is_random_sound :
return self . event != " NONE "
return False
@classmethod
def _idprop_mapping ( cls ) :
return { " emitter_object " : " object_name " }
@property
def is_random_sound ( self ) :
if self . emitter_object is not None :
return self . emitter_object . plasma_modifiers . random_sound . enabled
return False
class PlasmaSubworldMsgNode ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaSubworldMsgNode "
bl_label = " Change Subworld "
bl_width_default = 200
sub_type_value = EnumProperty (
items = subworld_types ,
default = " subworld " ,
options = { " HIDDEN " }
)
def _get_sub_type ( self ) - > int :
if self . subworld is not None :
self . sub_type_value = self . subworld . plasma_modifiers . subworld_def . sub_type
if not self . sub_type_value :
self . sub_type_value = " subworld "
return next (
i for i , sub_type in enumerate ( subworld_types )
if sub_type [ 0 ] == self . sub_type_value
)
def _set_sub_type ( self , value : int ) :
value_str = subworld_types [ value ] [ 0 ]
if self . subworld is not None :
self . subworld . plasma_modifiers . subworld_def . sub_type = value_str
self . sub_type_value = value_str
sub_type : str = EnumProperty (
name = " Subworld Type " ,
description = " Specifies the physics strategy to use for this subworld " ,
items = subworld_types ,
get = _get_sub_type ,
set = _set_sub_type ,
options = set ( )
)
subworld : bpy . types . Object = PointerProperty (
name = " Subworld " ,
description = " Subworld to move the player to (leave empty for the main world) " ,
poll = idprops . poll_subworld_objects ,
type = bpy . types . Object
)
def draw_buttons ( self , context , layout ) :
need_world_type = self . subworld is None and self . sub_type == " auto "
layout . alert = need_world_type
layout . prop ( self , " sub_type " , text = " Type " )
if need_world_type :
layout . label ( " When leaving a subworld, the subworld type MUST be specified! " , icon = " ERROR " )
layout . alert = False
layout . prop ( self , " subworld " )
def convert_message ( self , exporter : Exporter , so : plSceneObject ) :
if self . subworld is None and self . sub_type == " auto " :
self . raise_error ( " When leaving a subworld, the subworld type MUST be specified! " )
if exporter . physics . is_dedicated_subworld ( self . subworld ) or self . sub_type == " subworld " :
msg = plSubWorldMsg ( )
if self . subworld :
msg . worldKey = exporter . mgr . find_key ( plSceneObject , bl = self . subworld )
return msg
else :
msg = plRideAnimatedPhysMsg ( )
msg . BCastFlags | = plMessage . kPropagateToModifiers
msg . entering = self . subworld is not None
return msg
class PlasmaTimerCallbackMsgNode ( PlasmaMessageWithCallbacksNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaTimerCallbackMsgNode "
bl_label = " Timed Callback "
delay = FloatProperty ( name = " Delay " ,
description = " Time (in seconds) to wait until continuing " ,
min = 0.1 ,
default = 1.0 )
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " delay " )
def convert_callback_message ( self , exporter , so , msg , target , wait ) :
msg . addReceiver ( target )
msg . ID = wait
def convert_message ( self , exporter , so ) :
msg = plTimerCallbackMsg ( )
msg . time = self . delay
return msg
class PlasmaTriggerMultiStageMsgNode ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaTriggerMultiStageMsgNode "
bl_label = " Trigger MultiStage "
output_sockets : dict [ str , dict [ str , Any ] ] = {
" satisfies " : {
" text " : " Trigger " ,
" type " : " PlasmaConditionSocket " ,
" valid_link_nodes " : " PlasmaMultiStageBehaviorNode " ,
" valid_link_sockets " : " PlasmaConditionSocket " ,
" link_limit " : 1 ,
}
}
def convert_message ( self , exporter , so ) :
# Yeah, this is not a REAL Plasma message, but the Korman way is to try to hide these little
# low-level notifications behind higher level abstractions, so here you go. A notify message
# that only targets plMultiStageBehMod. You're welcome!
msg = self . generate_notify_msg ( exporter , so , " satisfies " )
# The MultiStageBehMod needs to receive the avatar key that whatdonetriggeredit. We don't know
# this information at export-time, but plResponderModifier::IContinueSending will interpret
# a collision event as "ohey, let's add the avatar key for MSBs" - nice.
msg . addEvent ( proCollisionEventData ( ) )
return msg
class PlasmaFootstepSoundMsgNode ( PlasmaMessageNode , bpy . types . Node ) :
bl_category = " MSG "
bl_idname = " PlasmaFootstepSoundMsgNode "
bl_label = " Footstep Sound "
surface = EnumProperty ( name = " Surface " ,
description = " What kind of surface are we walking on? " ,
items = footstep_surfaces ,
default = " stone " )
def draw_buttons ( self , context , layout ) :
layout . prop ( self , " surface " )
def convert_message ( self , exporter , so ) :
msg = plArmatureEffectStateMsg ( )
msg . BCastFlags | = ( plMessage . kPropagateToModifiers | plMessage . kNetPropagate )
msg . surface = footstep_surface_ids [ self . surface ]
return msg