Cinema 4D R20 – Using a centralized plugin repository

Introduction

Maxon has released Release 20 of Cinema 4D and it comes with a bunch of new features (amongst them plugin breaking API changes, which is a bit of a bummer, but it needed to be done…). One of the lesser talked about features is the ability to set up multiple Plugin Search Paths. Previously you had you AppData plugin path and the system-wide plugin path in the Cinema 4D Rxx/plugins folder. The latter does not exist any more by default. Instead you are told to use your AppData plugin folder instead. This works fine if you are the only person working on your Workstation. It’s a pain if you are not.

New PluginPath feature in R20

Luckily you can still create your own Cinema 4D R20/plugins if you are so inclined. However, the big thing here (at least for me) is: the Search Paths also support UNC paths! Which means you can set up a shared network folder and have all your Cinema 4D R20 installations point to this folder and magically manage all your plugins through a single folder.

Licensing plugins

Using a shared folder works great for plugins that do not require a license to begin with or those that use the licensing system provided by Cinema itself (via its serial number). These licenses are stored in your workstation’s registry and are thus independent from the plugin folder location.

A problem occurs when you are dealing with plugins that require license files. Unfortunately those you will still have to install in your local plugin folder (e.g. the AppData one). But since you can combine multiple plugin folder paths, this is not a big issue.

Multi-User Environments

Cinema store the plugin paths in a file called plugins.json, located in the AppData folders. This means it stores the setting per-User. Which also means it’s tricky to centrally manage the setting for all users in a domain. You can of course pre-create a plugins.json file and just copy it over the existing (or non-existing-yet) one per user-login. But this will remove any custom setting that user may have made. Instead I decided to write a small Python script, since this is plain JSON and easy to parse. If you’re interested, I’ve put the script on Github.

import json
import codecs
import os

_DEBUG = False
# _DEBUG = True

# C4D can use UNC paths, but the next time they are changed within the app, they are stored as path + authoritypath
# instead, so for each UNC path there are two possible ways they may be stored as
_PLUGINPATHUNC = "//SERVER/FOLDER/R20/plugins"
_PLUGINPATH = "FOLDER/R20/plugins"
_PLUGINAUTHORITYPATH = "SERVER"

# A JSON block that contains a path definition
_NEWPATH = {
            "_0": {
                "referenceIndex": 0,
                "referenceDataType": "net.maxon.interface.url-C",
                "_scheme": "file",
                "_path": _PLUGINPATH,
                "_authority": {
                    "referenceDataType": "net.maxon.interface.url-C",
                    "_scheme": "authority",
                    "_path": _PLUGINAUTHORITYPATH,
                    "_authority": {},
                    "_data": {}
                },
                "_data": {}
            },
            "_1": True
        }

# The contents of a brand new definition file with out path already in it (by default the plugins.json file does 
# not exist until a path is manually added in the application)
_NEWFILE = {
    "identification": "plugins",
    "content": {
        "referenceDataType": "net.maxon.interface.datadictionary-C",
        "_impl": {
            "_mode": 2,
            "_data": [
                {
                    "dataType": "net.maxon.datatype.id",
                    "content": "searchPaths"
                },
                {
                    "dataType": "(net.maxon.interface.url-C,bool)",
                    "isArray": True,
                    "content": [
                        {
                            "_0": {
                                "referenceIndex": 0,
                                "referenceDataType": "net.maxon.interface.url-C",
                                "_scheme": "file",
                                "_path": _PLUGINPATH,
                                "_authority": {
                                    "referenceDataType": "net.maxon.interface.url-C",
                                    "_scheme": "authority",
                                    "_path": _PLUGINAUTHORITYPATH,
                                    "_authority": {},
                                    "_data": {}
                                },
                                "_data": {}
                            },
                            "_1": True
                        }
                    ]
                }
            ]
        }
    }
}

def debug(msg):
    if _DEBUG:
        print(msg)

def hasPluginPath(content, pathToCheckFor):
    if "_path" in content:
        if content["_path"] == pathToCheckFor:
            return True
    return False

userAppData = os.getenv('APPDATA')
debug(userAppData)
maxon = os.path.join(userAppData, "MAXON")
debug(maxon)
if (os.path.exists(maxon)):
    # only look into R20 user folders
    userDirsR20 = [x for x in os.listdir(maxon) if str(x).lower().startswith("cinema 4d r20")]
    for userDir in userDirsR20:
        pluginsJsonPath = os.path.join(maxon, userDir, "plugins.json")
        debug(pluginsJsonPath)
        if os.path.exists(pluginsJsonPath):
            # the plugins.json file is stored as "UTF-8 with BOM", so regular open() will not open it correctly and json.load will fail
            with codecs.open(pluginsJsonPath, "r+", "utf-8-sig") as f:
                j = json.load(f)
                found_PLUGINPATH = False
                for data in j['content']['_impl']['_data']:
                    if data['dataType'] == "(net.maxon.interface.url-C,bool)":
                        for idx, content in enumerate(data['content']):
                            debug(content)
                            if hasPluginPath(content["_0"], _PLUGINPATH) or hasPluginPath(content["_0"], _PLUGINPATHUNC):
                                found_PLUGINPATH = True
                                data['content'][idx] = _NEWPATH
                                break
                        if not found_PLUGINPATH:
                            found_PLUGINPATH = True
                            data['content'].append(_NEWPATH)
                # write the changes back into the file
                if found_PLUGINPATH:
                    f.seek(0)
                    if _DEBUG:
                        print(json.dumps(j, indent=4, sort_keys=False))
                    else:
                        f.writelines(json.dumps(j, indent=4, sort_keys=False))
                        f.truncate()
        else:
            with codecs.open(pluginsJsonPath, "w+", "utf-8-sig") as f:
                f.writelines(json.dumps(_NEWFILE, indent=4, sort_keys=False))
else:
    exit

Leave a Reply

Your email address will not be published. Required fields are marked *