Plugin Structure

Plugins are stored in the plugins folder within the user folder. Each plugin should have a suffix called .pyp or .pypv (for encrypted files). When Cinema 4D starts, it finds all files in this folder that end with .pyp or .pypv and executes the plugin. A simple plugin possible thus looks like this

def main():
    print("Hello World!")

main()

Such a plugin is not very interesting, as it is only run when the program starts. Therefore we have the ability to register plugin hooks in various parts of the program.

Hook

All plugin hooks are built upon data classes derived from BaseData. These data classes contain a set of methods that are called by Cinema 4D. An example from MessageData

class SampleData(plugins.MessageData):

    def CoreMessage(self, id, bc):
        pass

Registration

To register the derived class with Cinema 4D there is a specific Register*() function for each data class. Some of them take a new object of the data class or just the data class so Cinema 4D can create instances on its own

plugins.RegisterCommandPlugin(id=PLUGIN_ID, str="TestBase-Plugin", info=0, dat=SampleData())

The registration functions for NodeData plugins instead want a data class like this

class SampleData(plugins.ObjectData):
    def GetVirtualObjects(self, op, hierarchyhelp):
        pass

plugins.RegisterObjectPlugin(id=PLUGIN_ID, str="TestNode-Plugin",
                             g=SampleData, description="", icon=None,
                             info=c4d.OBJECT_GENERATOR)

Warning

The ID of a plugin, PLUGIN_ID in the example above, passed to such registration functions has to be globally unique. Attempting to reuse an identifier will cause only one of the plugins that tries to claim that identifier to be successfully loaded on the startup of Cinema 4D. Please make sure to always obtain identifiers for plugins from the Plugin ID generator on Plugin Café to avoid identifier collisions. You must create an account with Plugin Café in order to use the generator.

Lifetime

There are a few things to say about the lifetime of the data class instances, especially with concern to member variables. In those cases where a new object is passed to the registration function, as shown in the first example above, then this instance is kept for the whole Cinema 4D session. Since it is constructor and destructor are called as usual, no special concern is necessary. The data classes where the name needs to be passed to the registration function have a 1:1 correspondence with a node in Cinema 4D. Thus they are allocated and deleted by Cinema 4D along with the node. When this happens the constructor and the destructor are called as usual. However, Cinema 4D additionally calls the NodeData.Init() method after the constructor and NodeData.Free() before the destructor.

Directory Structure

While .pyp or .pypv can be placed directly in the plugin directory, it is often better to group them into hierarchies. The standard layout for a plugin folder is like this:

myPlugin/
    myPlugin.pyp
    ...
    res/
        c4d_symbols.h
        description/
            myDescription.h
            myDescription.res
            ...
        dialogs/
            myDialog.res
            ...

        strings_en-US/
            c4d_strings.str
            description/
                myDescription.str
                ...
            dialogs/
                myDialog.str
                ...
            strings_de-DE/
            strings_ja-JP/
            ...
    myIcon.tif
    myWhatever.any
    ...

The main file is myPlugin.pyp, which registers the hooks. The res directory contains plugin resources, which currently means dialogs, descriptions and strings.

For each description there is a .res file with the description and a .h file with enums for the constants used in the description. See Descriptions in Cinema 4D. Each dialog is contained in its own .res file. The c4d_symbols.h file should contain enums for the constants used in the .res files.

Then there should be a directory named strings_xx-XX for each language that the plugin supports, according to the ISO 639-1 standard. Currently there are Cinema 4D versions available for the following codes:

  • Arabic: ar-AR

  • Chinese: zh-CN

  • Czech: cs-CZ

  • German: de-DE

  • Italian: it-IT

  • Korean: ko-KR

  • Japanese: ja-JP

  • Polish: pl-PL

  • Russian: ru-RU

  • Spanish: es-ES

  • French: fr-FR

  • English: en-US

Based on the ISO standard it is also possible to introduce new languages. A new language must be declared in the “resourcelanguage” folder of Cinema 4D. Create a sub-folder named after the above scheme (“en-US”) and define a language.txt file that includes the name of that language.

Before R20 the naming scheme was:

  • cz - Czech

  • de - German

  • es - Spanish

  • fr - French

  • it - Italian

  • jp - Japanese

  • kr - Korean

  • pl - Polish

  • ru - Russian

  • us - English

Note

For compatibility reason, strings_us keeps working for plugins in R20.

Each of the language directories should contain a .str file for every dialog and a c4d_strings.str for other resource strings. It is recommended to first develop the plugin in one language, and then just copy the strings directory before translating. Finally it is possible to have any other files in the plugin’s folder, for example icons or logos. These can be conveniently accessed using __file__:

dir, file = os.path.split(__file__)

Warning

If the exception “Could not find required ‘__res__’.” is written to the Python console at startup of Cinema 4D, the resource of a plugin seems to be corrupt.
Plugins need a special resource structure, like a res folder, a file c4d_symbols.h, etc. even if these files are empty or do not contain anything useful.

Plugin Messages

PluginMessage(id, data)

Called to receive plugin messages. These can either be from Cinema 4D or from other plugins via GePluginMessage().

Parameters
  • id (int) –

    The message ID. Built-in ones are:

    C4DPL_INIT_SYS

    Initialize system.

    C4DPL_INIT

    Initialize the plugin, calling PluginStart.

    C4DPL_END

    End the plugin, calling PluginEnd.

    C4DPL_MESSAGE

    Receive plugin message, calling PluginMessage.

    C4DPL_BUILDMENU

    Called when the menus are built during startup. Use GetMenuResource() etc. to add own menus here if necessary. See the example.

    C4DPL_STARTACTIVITY

    Sent to all plugins after all PluginStart have been called.

    C4DPL_ENDACTIVITY

    Sent to all plugins before any PluginEnd has been called. This allows to close dialogs, end threads, delete temporary/undo buffers etc.

    C4DPL_CHANGEDSECURITYTOKEN

    Sent to all plugins if the Security Token of Team Render was changed.

    C4DPL_SHUTDOWNTHREADS

    Sent to all plugins before the threading system of Cinema 4D is shutdown. After that no threads should be started.

    C4DPL_LAYOUTCHANGED

    Sent to all plugins after the layout has been switched.

    C4DPL_RELOADPYTHONPLUGINS

    Sent when Python plugins are to be reloaded. See Python Plugins Reloading.

    C4DPL_COMMANDLINEARGS

    Sent to let plugins parse the command line arguments used when starting Cinema 4D. Retrieve the arguments by calling sys.argv. See Command Line Arguments.

    C4DPL_EDITIMAGE

    Sent to allow plugins to start any other actions when Cinema 4D tries to show an image (e.g. in the shader popup). data is dict{‘imagefn’: str, filename of the image that triggered the message}

    C4DPL_ENDPROGRAM

    Sent when Cinema 4D is about to close. data is dict{‘cancel’: bool, set to True to cancel close of Cinema 4D}

    C4DPL_DEVICECHANGE

    Sent when a device has changed. data is dict{‘name’: str, name of the device, ‘eject’: bool Eject, state of the device}

    C4DPL_NETWORK_CHANGE

    Sent to all plugins if the network configuration on the system changed.

    C4DPL_SYSTEM_SLEEP

    Sent to all plugins if the system goes to sleep.

    C4DPL_SYSTEM_WAKE

    Sent to all plugins if the system wakes up.

    C4DPL_PROGRAM_STARTED

    Sent when the application has been started.

  • data (any) – The message data.

Return type

bool

Returns

True if the message was consumed, otherwise False.

Command Line Arguments

To retrieve Cinema 4D command line arguments at any time, implement PluginMessage() and filter the C4DPL_COMMANDLINEARGS message

import c4d
import sys

def PluginMessage(id, data):
    if id==c4d.C4DPL_COMMANDLINEARGS:
        print(sys.argv) #print arguments
        return True

    return False
  • Save this code in a .pyp file and place it in the plugins folder.

  • Start Cinema 4D with some arguments like “Cinema 4D.exe –hello”.

  • Open the console, it should contain the passed argument(s).

Warning

Arguments which are handled by Cinema 4D modules are removed from the list. For example, try starting Cinema 4D with “Cinema 4D.exe –hello -parallel”: open the console, parallel is not in the printed arguments list.

Python Plugins Reloading

The Python in Cinema 4D function reloads and recompiles the source of a .pyp file. Python modules which are imported by a .pyp file will not be reloaded again. Python first checks if the module is already imported, if yes this is skipped and just the reference is set.

You can use importlib.reload() to force the reload of a Python module when C4DPL_RELOADPYTHONPLUGINS message is received in PluginMessage(). This is also the place to close system resources (e.g. sockets, files) opened before.

Enhancing the Main Menu

To add custom menus, intercept C4DPL_BUILDMENU in PluginMessage() and call GetMenuResource() to retrieve the main menu resource container. Here is a complete example:

def EnhanceMainMenu():
    mainMenu = gui.GetMenuResource("M_EDITOR")                    # Get main menu resource
    pluginsMenu = gui.SearchPluginMenuResource()                  # Get 'Plugins' main menu resource

    menu = c4d.BaseContainer()                                    # Create a container to hold a new menu information
    menu.InsData(c4d.MENURESOURCE_SUBTITLE, "Py-Test")            # Set the name of the menu
    menu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_NEU")             # Add registered default command 'New Scene' to the menu
    menu.InsData(c4d.MENURESOURCE_SEPERATOR, True);               # Add a separator
    menu.InsData(c4d.MENURESOURCE_COMMAND, "PLUGIN_CMD_5159")     # Add command 'Cube' with ID 5159 to the menu

    submenu = c4d.BaseContainer()                                 # Create a new submenu container
    submenu.InsData(c4d.MENURESOURCE_SUBTITLE, "Submenu")         # This is a submenu
    submenu.InsData(c4d.MENURESOURCE_COMMAND, "IDM_SPEICHERN")    # Add registered default command 'Save' to the menu

    menu.InsData(c4d.MENURESOURCE_SUBMENU, submenu)               # Add the submenu

    if pluginsMenu:
        # Insert menu after 'Plugins' menu
        mainMenu.InsDataAfter(c4d.MENURESOURCE_STRING, menu, pluginsMenu)
    else:
        # Insert menu after the last existing menu ('Plugins' menu was not found)
        mainMenu.InsData(c4d.MENURESOURCE_STRING, menu)

def PluginMessage(id, data):
    if id==c4d.C4DPL_BUILDMENU:
        EnhanceMainMenu()

Note

If menus are modified from outside C4DPL_BUILDMENU message, gui.UpdateMenus() needs to be called.