Maxon Developers Maxon Developers
    • Documentation
      • Cinema 4D Python API
      • Cinema 4D C++ API
      • Cineware API
      • ZBrush GoZ API
      • Code Examples on Github
    • Forum
    • Downloads
    • Support
      • Support Procedures
      • Registered Developer Program
      • Plugin IDs
      • Contact Us
    • Categories
      • Overview
      • News & Information
      • Cinema 4D SDK Support
      • Cineware SDK Support
      • ZBrush 4D SDK Support
      • Bugs
      • General Talk
    • Unread
    • Recent
    • Tags
    • Users
    • Login

    Python: PluginMessage: Execute callback on plugin register and unregister event

    Cinema 4D SDK
    python
    2
    7
    966
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • P
      paulgolter
      last edited by paulgolter

      Hey all,

      I am currently facing the issue that I want to execute code when my cinema python plugin gets registered and unregistered by cinema or a user.

      As i understand from the docs I should be able to listen to events in my .pyp file of my plugin by adding a function called PluginMessage.

      The event ids I am interested in are:

      C4DPL_INIT Initialize the plugin, calling PluginStart.
      C4DPL_END End the plugin, calling PluginEnd

      def PluginMessage(id: int, data: Any) -> bool:
          # ID's don't match up
          if id == c4d.C4DPL_INIT:        
              register()
              return True
      
          if id == c4d.C4DPL_END:
              unregister()
              return True
      
          return False
      

      The problem is that the incoming event ids never seems to match up.

      Any idea what I am doing wrong?

      ferdinandF 1 Reply Last reply Reply Quote 0
      • ferdinandF
        ferdinand @paulgolter
        last edited by ferdinand

        Hey @paulgolter,

        The reason why you are not seeing these is because not in all stages of the boot sequence of Cinema 4D all API systems are available. So, even in C++ you are basically in no-mans-land for both C4DPL_INIT and C4DPL_END where nothing works except for std language features; because for C4DPL_INIT nothing has been yet pulled up, and for C4DPL_END everything has already been torn down. Especially when Cinema 4D is shutting down, non-careful code can lead to access violations and with that crashes. For Python the unavailability of systems is even more pronounced because Python itself only comes alive when things like registries have been pulled up. But as always, you have a harder time in Python to royally shoot yourself in the foot and with that crash Cinema 4D (but not impossible after C4DPL_ENDPROGRAM or even C4DPL_ENDACTIVITY).

        In general, the markers to start and stop doing things for plugin authors are C4DPL_STARTACTIVITY and C4DPL_ENDACTIVITY or C4DPL_PROGRAM_STARTED and C4DPL_ENDPROGRAM when you want even tighter bounds where truly everything is up and running. But with C4DPL_STARTACTIVITY you have full access to the API it is just that some calls might still fail due to certain things not having started yet. This also applies to C++.

        Cheers,
        Ferdinand

        Log

        This is for just opening and then closing Cinema 4D. In the Python console we will usually never see anything from C4DPL_ENDPROGRAM onwards, as the system is there already being torn down.

        PluginMessage: C4DPL_STARTACTIVITY
        PluginMessage: C4DPL_BUILDMENU
        PluginMessage: C4DPL_LAYOUTCHANGED
        PluginMessage: C4DPL_PROGRAM_STARTED
        PluginMessage: C4DPL_COMMANDLINEARGS
        PluginMessage: C4DPL_BUILDMENU
        PluginMessage: Unknown Type (1062714)
        PluginMessage: C4DPL_LAYOUTCHANGED
        PluginMessage: C4DPL_ENDPROGRAM
        PluginMessage: C4DPL_SHUTDOWNTHREADS
        PluginMessage: C4DPL_ENDACTIVITY
        PluginMessage: Unknown Type (300002146)
        PluginMessage: Unknown Type (1026848)
        PluginMessage: Unknown Type (1026662)
        PluginMessage: Unknown Type (200000272)
        
        

        Code

        import os
        
        MESSAGE_MAP: dict[int, str] = {
            c4d.C4DPL_INIT_SYS: "C4DPL_INIT_SYS",
            c4d.C4DPL_INIT: "C4DPL_INIT",
            c4d.C4DPL_END: "C4DPL_END",
            c4d.C4DPL_MESSAGE: "C4DPL_MESSAGE",
            c4d.C4DPL_BUILDMENU: "C4DPL_BUILDMENU",
            c4d.C4DPL_STARTACTIVITY: "C4DPL_STARTACTIVITY",
            c4d.C4DPL_ENDACTIVITY: "C4DPL_ENDACTIVITY",
            c4d.C4DPL_CHANGEDSECURITYTOKEN: "C4DPL_CHANGEDSECURITYTOKEN",
            c4d.C4DPL_SHUTDOWNTHREADS: "C4DPL_SHUTDOWNTHREADS",
            c4d.C4DPL_LAYOUTCHANGED: "C4DPL_LAYOUTCHANGED",
            c4d.C4DPL_RELOADPYTHONPLUGINS: "C4DPL_RELOADPYTHONPLUGINS",
            c4d.C4DPL_COMMANDLINEARGS: "C4DPL_COMMANDLINEARGS",
            c4d.C4DPL_EDITIMAGE: "C4DPL_EDITIMAGE",
            c4d.C4DPL_ENDPROGRAM: "C4DPL_ENDPROGRAM",
            c4d.C4DPL_DEVICECHANGE: "C4DPL_DEVICECHANGE",
            c4d.C4DPL_NETWORK_CHANGE: "C4DPL_NETWORK_CHANGE",
            c4d.C4DPL_SYSTEM_SLEEP: "C4DPL_SYSTEM_SLEEP",
            c4d.C4DPL_SYSTEM_WAKE: "C4DPL_SYSTEM_WAKE",
            c4d.C4DPL_PROGRAM_STARTED: "C4DPL_PROGRAM_STARTED"
        }
        BOOT_LOG: str = os.path.join(os.path.dirname(__file__), "boot_log.txt")
        
        def PluginMessage(pid: int, data: any) -> bool:
            """Called by Cinema 4D to handle plugin messages.
            """
            print (f"PluginMessage: {MESSAGE_MAP.get(pid, f'Unknown Type ({pid})')}")
            # In production you should either check for being on the main thread or use a semaphore or lock
            # to regulate access to #BOOT_LOG.
            with open(BOOT_LOG, "a") as f:
                f.write(f"PluginMessage: {MESSAGE_MAP.get(pid, f'Unknown Type ({pid})')}\n")
        
            return False
        

        MAXON SDK Specialist
        developers.maxon.net

        P 1 Reply Last reply Reply Quote 2
        • P
          paulgolter @ferdinand
          last edited by

          Hey @ferdinand,

          thanks for the detailed background info. Would have never found that out on my own!

          Ok currently I am using the regular python way to call my register() function, which register my message and object plugins:

          if __name__ == "__main__":
              register()
          

          If i try to replace that with:

          def PluginMessage(id: int, data: Any) -> bool:
              if id == c4d.C4DPL_STARTACTIVITY:
                  register()
                  return True
          

          I get the error: OSError:cannot find pyp file - plugin registration failed

          So i guess just like you said, some calls are still failing. Then I will stick to registering the plugin with the simple if __name__ == "__main__" way and unregistering works fine with the C4DPL_ENDACTIVITY event.

          ferdinandF 1 Reply Last reply Reply Quote 0
          • ferdinandF
            ferdinand @paulgolter
            last edited by ferdinand

            Hey @paulgolter,

            The way how plugins are registered in C++ does not translate to Python. Just do it as you did before and as shown in all Python code examples: Register them in the execution context guard (if __name__ ...) of a pyp file. C4DPL_STARTACTIVITY is too late to register plugins, C4DPL_INIT would be the point to do that. But that does no translate to Python as Python itself is a plugin, so it cannot run before plugins have been loaded. When you register an ObjectData plugin in Python, you get for example a preallocated plugin slot assigned from the Python API. Which is also why users can also only have 50 Python plugins per NodeData type installed (object, tag, shader, etc.) as the Python API just reserves this many slots per node type. So, the 51th RegisterObjectPlugin etc. call will fail on a user machine.

            It is pretty obvious that you are trying to do something specific. Maybe you could try to explain on a more abstract level what you are trying to achieve, and I could then give options there, instead of just telling you what all does not work for technical reasons 😉

            Cheers,
            Ferdinand

            MAXON SDK Specialist
            developers.maxon.net

            P 1 Reply Last reply Reply Quote 1
            • P
              paulgolter @ferdinand
              last edited by

              Hey @ferdinand

              I see!

              Maybe you could try to explain on a more abstract level what you are trying to achieve

              In short, I am building a python project, that depends on being able to communicate with a webserver. It sends out things when certain events happen in cinema and also receives signals via websockets to execeute certain actions. The communication with the server is all async and runs inside of a thread to not block the dccs ui.

              Most dccs that enable / disable plugins, have some sort of functions or event that you can hook in when this happens. That way I can for instance, when my plugins is loaded, establish a connection with the webserver and when it is unloaded making sure that I gracefully shutdown the connection, the async loop as well as the thread.

              ferdinandF 1 Reply Last reply Reply Quote 0
              • ferdinandF
                ferdinand @paulgolter
                last edited by ferdinand

                Hey @paulgolter,

                I still do not 100% understand why this entails you having to poke around in the boot sequence of Cinema 4D. You should just register your plugin normally (there is no alternative to that). But at least I understand now where we are going 🙂

                1. Python plugins cannot be disabled in Cinema 4D. You can reload them, but not disable them. You also cannot unregister a plugin. The only way to do this is to reboot Cinema 4D and then bail on registration, for example based on a web server response or something like that.
                2. In general, you might run into problems with async/await in our Python interpreter, it is not a keyword/pattern that is used often in the context of the Cinema 4D API. You must keep in mind that there is the Cinema 4D main loop, the scene execution state machine. So, other than in a vanilla Python VM, the Cinema 4D Python VM does not run uninterrupted, but only when Cinema 4D calls/executes the plugin (Script Manager scripts of course run in one go, but these are besides the point here). It is hard to make here an absolute statement, but in general I would stay away from async. Python's threading is also off-limits and you must use c4d.threading, the multiprocessing module is not supported.

                In which context do you want to use async? To run a webserver with something like fastapi or sanic? I would not recommend running such server under Cinema 4D, this might cause a lot of problems. Instead you should run your webserver with a vanilla Python where you dot not have Cinema 4D lurking over your shoulder and limiting how you can uses threads and processes. You then can communicate with the Cinema 4D Python VM via sockets (or a similar technique for inter-process/app communication). You might want to have a look at these two threads:

                1. Server & Client pattern to have two Python VMs communicate : This example uses this to run a non-native GUI, but you can use that for everything that does not run well under Cinema. The problematic bit is that you must be able to serialize all your data, as you have to send it over a socket.
                2. Downloading stuff in Cinema 4D in a non-blocking manner: For that you do not need async, just some threading and usually some spider in the net plugin hook which handles the downloading of content (exposed to other plugins), a common choice as shown in the example is a MessageData plugin.

                Cheers,
                Ferdinand

                MAXON SDK Specialist
                developers.maxon.net

                P 1 Reply Last reply Reply Quote 0
                • P
                  paulgolter @ferdinand
                  last edited by

                  Hey @ferdinand

                  I still do not 100% understand why this entails you having to poke around in the boot sequence of Cinema 4D. You should just register your plugin normally (there is no alternative to that)

                  I think it's mainly because I didn't know what the proper way of doing it was. Reading the docs I thought I needed to use c4d.C4DPL_INIT or c4d.C4DPL_STARTACTIVITY to register my plugin stuff / execute my own startup code. I know there are the c4d plugin examples, but maybe it would be nice to add the "proper" way of registering a plugin to the docs.

                  Python plugins cannot be disabled in Cinema 4D. You can reload them, but not disable them. You also cannot unregister a plugin.

                  Aha also some good information that I was not aware of!

                  And to the server:
                  I am not running a server inside of cinema. It is running on some other machine in the internet. I am only connecting to it via a httpx.AsyncClient client. For this reason and some other technical reasons the client needs to run in a separate thread, but all of that works fine and we are even using the c4d.C4DThread for this.

                  But you answered my question of this thread already, thanks 🙂

                  1 Reply Last reply Reply Quote 0
                  • First post
                    Last post