How to check if object or tag in scene is native i.e. not a third-party plugin?
-
I'm in the process of creating some plugin that checks scene before sending to renderfarm, so I'm looking for solution to find plugins that probably not supported. How to manage that kind of task? Thanks!
-
Hello @yura ,
Welcome to the Plugin Café forum and the Cinema 4D development community, it is great to have you with us!
Getting Started
Before creating your next postings, we would recommend making yourself accustomed with our Forum and Support Guidelines, as they line out details about the Maxon SDK Group support procedures. Of special importance are:
- Support Procedures: Scope of Support: Lines out the things we will do and what we will not do.
- Support Procedures: Confidential Data: Most questions should be accompanied by code but code cannot always be shared publicly. This section explains how to share code confidentially with Maxon.
- Forum Structure and Features: Lines out how the forum works.
- Structure of a Question: Lines out how to ask a good technical question. It is not mandatory to follow this exactly, but you follow the idea of keeping things short and mentioning your primary question in a clear manner.
About your First Question
Your question is quite clear, but there are some problems which lie below the surface. So, let's split things into bullet points!
- Maxon uses the same plugin infrastructure as third-party developers. So, checking for the presence of plugins alone is not sufficient, because large parts of native Cinema 4D features are also plugins.
- All plugins loaded by a Cinema 4D instance can be found via c4d.plugins.FilterPluginList and similar functions in the
c4d.plugins
module. - Finding plugins loaded by Cinema 4D differs from finding plugins which are used in a Cinema 4D scene. You mention objects and tags, but there are many other ways how a plugin can contribute to the state of a scene (materials, shaders, tracks, and the list goes on). This means you must be able to traverse a scene abstractly, which for most users probably is a non-trivial task. There are no builtin methods to do this in one line, we have talked about it multiple times on the forum, for example here.
- You need the ability to determine if something is a third-party plugin or not. BasePlugin.GetFilename would be the natural solution to that, because by knowing where a plugin is being stored, you also know if it is native plugin or not (native plugins are in the
corelibs
directory). Unfortunately, while answering your posting, I have discovered a bug inGetFilename
as multiple plugins can be misclassified by it. This happens when Python plugins are installed on a machine, some plugins then return an incorrect value forGetFilename
. This however only seems to happen for im-and-exporter plugins as well as special forms ofCommandData
plugins. So, for your purposes where you probably do not care about either of these plugin types as they do not contribute to a scene state, you still could use that method. - As an alternative, you could record the plugin IDs of a Cinema 4D installation with no plugins installed, and then one with the plugins installed to build the difference out of them. This will work correctly in any case. I did not flesh out this idea in my code example below.
- Last but not least, you said that 'so I'm looking for solution to find plugins that probably not supported, so I'm looking for solution to find plugins that probably not supported'. This implies that the plugins you want to find in a scene are locally installed. I answered under this premise here. If this is not the case things become much, much, much more complicated. You could try things like retrieving the icon of each node in a scene and then testing if it is the question mark icon, but this will be of course very dicey.
I have cobbled together a quick and dirty solution using the code from an older abstract scene traversal posting of mine and the
BasePlugin.GetFilename
approach. As lined out above,GetFilename
is currently bugged and scene traversal can be quite involved and the code I am using here was intended for something else, one would probably want to fine-tune this. However, running it for example on this scene:Will correctly find all plugins:
pluginPaths = ['c:\\users\\f_hoppe\\appdata\\roaming\\maxon\\2023.1.3_97abe84b\\plugins', 'e:\\plugins\\2023.1\\pysdk', 'e:\\plugins\\2023.1\\sdk'] -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.CTrack object called C++ SDK - Blinker/C++ SDK - Blinker with ID 1001152 at 2876763050048> 1001152, C++ SDK - Blinker, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64 -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseObject object called C++ SDK - Greek Temple Generator Example/C++ SDK - Greek Temple Generator Example with ID 1038235 at 2876763255232> 1038235, C++ SDK - Greek Temple Generator Example, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64 -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseObject object called Py-Gravitation/Py-Gravitation with ID 1025246 at 2876763172992> 1025246, Py-Gravitation, e:\plugins\2023.1\pysdk\plugins\py-gravitation_r12\py-gravitation_r12.pyp -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseObject object called Py-DoubleCircle/Py-DoubleCircle with ID 1025245 at 2876763255296> 1025245, Py-DoubleCircle, e:\plugins\2023.1\pysdk\plugins\py-double_circle_r19\py-double_circle_r19.pyp -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseShader object called C++ SDK - Mandelbrot/C++ SDK - Mandelbrot with ID 1001162 at 2876763218304> 1001162, C++ SDK - Mandelbrot, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64 -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseShader object called Py-Fresnel/Py-Fresnel with ID 1027089 at 2878361182400> 1027089, Py-Fresnel, e:\plugins\2023.1\pysdk\plugins\py-fresnel_r13\py-fresnel_r13.pyp -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseShader object called C++ SDK - Mandelbrot/C++ SDK - Mandelbrot with ID 1001162 at 2878361285632> 1001162, C++ SDK - Mandelbrot, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64 -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseMaterial object called C++ SDK - Simple Material/C++ SDK - Simple Material with ID 1001164 at 2878361180928> 1001164, C++ SDK - Simple Material, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64 -------------------------------------------------------------------------------- Found third party plugin node in scene graph: <c4d.BaseList2D object called Paint Brush/Paint Brush with ID 1031368 at 2878361301824> 1031368, Paint Brush, e:\plugins\2023.1\sdk\plugins\cinema4dsdk\cinema4dsdk.xdl64
This is the correct list of all plugins used in this scene. Note that the last item is
Brush/Paint Brush with ID 1031368 at 2878361301824
, which is aSceneHookData
plugin. Scene hooks make up a considerable part of a scene graph and will also be traversed. It is up you to decide if you would consider scene hooks as crucial parts of a scene state (and exclude them in the traversal). Generally speaking, they are usually not, or at least they are only when some other more tangible plugin nodes are present as object and materials. This 'Paint Brush' hook for example is completely irrelevant for evaluating a scene state.But as a counter example you could take X-Particles which probably uses multiple scene hooks to make their whole particle setup thingy work and some of them might be essential for building the particle caches. So, they would be essential for rendering, but they are of course only essential when the more easily to detect object plugins that represent the X-Particles system are present.
Cheers,
FerdinandCode:
"""Provides a foundation for detecting third party plugin nodes in a scene document. Must be run as a script manager script. WARNING: As of 2023.1, this can contain false positives when Python plugins are installed on a machine. When this is the case, the returned plugin IDs will contain false positives for native im- and exporter plugins, as well as some command plugins. """ import c4d import maxon import os import typing doc: c4d.documents.BaseDocument # The active document # --- Scene graph traversal code start --------------------------------------------------------------- # The graph traversal section has been taken from: # https://developers.maxon.net/forum/topic/14182/can-i-get-active-things-order-with-python/2 # ----------------------------------------------------------------------------------------------------- # Toggle this for IterateGraph to print out some lines showing what it does. IS_DEBUG: bool = False # A table of (int, BaseList2D) tuples used by HasSymbolBase() below. Doing this is necessary, # because classic API scene element branches express their content in terms of base types. So, we # might come along a branch with the ID Obase when iterating over a document. When we then want to # find all Ocube instances in a scene, we must be able to infer that Ocube is an instance of Obase, # so that we know that we must branch into the Obase object branch to find Ocube instances. There # is currently no other way to get this information except with BaseList2D.IsInstanceOf, i.e., we # need an instance of a type symbol to test that. This table achieves that in a performant manner by # only creating these dummy instances once. G_TYPE_TESTERS_TABLE: dict[int: c4d.BaseList2D] = {} def HasSymbolBase(t: int, other: int) -> bool: """Returns if the Cinema 4D type symbol #t is in an inheritance relation with the type symbol #other. E.g., Ocube is an instance of Obase, Mmaterial is an instance of Mbase, but Tphong for example is not an instance of CTbase. """ if not isinstance(t, int) or not isinstance(other, int): raise TypeError(f"Illegal argument types: {t, other}") # Try to get a previously allocated dummy node for the type symbol #t. dummyNode: typing.Union[c4d.BaseList2D, int, None] = G_TYPE_TESTERS_TABLE.get(t, None) # There is no node yet. if dummyNode is None: # Try to allocate one, this can fail, as the user could have passed a base symbol type. # E.g., one cannot allocate c4d.BaseList2D(c4d.Obase) # The symbol was a concrete type symbol, insert the dummy node under #t in the table. try: dummyNode = c4d.BaseList2D(t) G_TYPE_TESTERS_TABLE[t] = dummyNode # The type symbol was a base type, insert NOTOK under #t in the table. except BaseException: dummyNode = c4d.NOTOK G_TYPE_TESTERS_TABLE[t] = c4d.NOTOK # There can be no node instances for #t, #t must be a base type, compare #t with #other directly. if dummyNode == c4d.NOTOK: return t == other # Test if #t is an instance of #other return dummyNode.IsInstanceOf(other) def IterateGraph(node: c4d.BaseList2D, types: typing.Optional[list[int]] = None, inspectCaches: bool = False, root: typing.Optional[c4d.BaseList2D] = None) -> typing.Iterator[c4d.BaseList2D]: """Iterates over the branching and hierarchical relations of #node. Args: node: The starting node to inspect the contents for. Can be anything that is a BaseList2D, e.g., a document, an object, a material, a tag, a layer, etc. types (optional): The type of nodes which are in a relation with #node which should be yielded. This will also respect inheritance, e.g., [c4d.Obase] will yield all objects, and [c4d.Ocube] will only yield cube objects. Will yield all node types when None. Defaults to None. inspectCaches (optional): If the iteration should also branch into caches. Defaults to False. root: Private, do not override. Yields: Nodes which are in a relation with with #node and of a type or base type in #types. """ # Stop the iterator when #node is None. if not isinstance(node, c4d.BaseList2D): return if IS_DEBUG: print (f"IterateGraph({node, types, root})") # When this is an first user call of this function and not a recursion we set #root to #node, # so that we do not accidentally leak into other nodes. E.g, when we have this: # # Node.0 # Node.1 # Node.1.0 # Node.1.1 # Node.2 # ... # # Then we usually do want for IterateGraph(Node.1, ...) to only look at Node.1, Node.1.0, and # Node.1.1, but not at anything after that, e.g., Node.2. if root is None: root = node # Start iterating over the nodes ... while isinstance(node, c4d.BaseList2D): # Yield the node itself if it does match the type criteria. if types is None or any(node.IsInstanceOf(t) for t in types): yield node # Yield nodes which are placed in branches below #node. # # Branches are basically just contextualized hierarchical relations, e.g, a document has # an object branch, where all objects are placed. We are iterating here over all these # relations of a node. The content of a branch is not attached directly below a branch, # but under a GeListHead, a specialized variant of the hierarchy interface GeListNode of # the Cinema 4D classic API. if IS_DEBUG: print (f"\tBranches({node})") branchData: typing.Optional[list[dict]] = node.GetBranchInfo(c4d.GETBRANCHINFO_NONE) if branchData: for branchDict in branchData: # The thing to which all nodes are attached which are in the #branchName relation with # #node. For a BaseDocument and its object branch, #geListHead.GetDown() would return # the first object in that document. geListHead: c4d.GeListHead = branchDict.get("head", None) # A human readable description of the branch purpose, e.g., "Objects" for the object # branch of a document. branchName: str = branchDict.get("name", None) # The branch ID, this describes the base type of the nodes to find in this branch, for # the object branch of a document this would be c4d.Obase (5155). branchId: int = branchDict.get("id", None) # There are also flags in branch data, but I am ignoring them here. # This is malformed branching data, should not happen :) if None in (geListHead, branchId): continue if IS_DEBUG: print (f"\t\t{branchName = }, {branchId = }") # Get the first actual node in the branch, this can be None as Cinema often creates # empty branches in nodes for future access. firstNodeInBranch: typing.Optional[c4d.BaseList2D] = geListHead.GetDown() # Determine based on the branch type if we want to branch into this branch, i.e., the # user has passed Ocube and we must decide if we want to branch into Obase (yes, we # do want to :)). shouldBranch: bool = (True if types is None else any(HasSymbolBase(t, branchId) for t in types)) # Step over empty branches or branches which do not contain relevant nodes. if firstNodeInBranch is None or not shouldBranch: continue # Iterate over each node in the branch which is its own hierarchy with the root # #geListHead. for branchNode in IterateGraph(firstNodeInBranch, types, inspectCaches, root): yield branchNode # --- End of branching iteration. # Yield the content of BaseObject caches. if inspectCaches and isinstance(node, c4d.BaseObject): for cache in (node.GetCache(), node.GetDeformCache()): for cacheNode in IterateGraph(cache, types, inspectCaches, root): yield cacheNode # Yield nodes which are in a direct hierarchical down relation with #node, e.g., the # children of objects, layers, render data, etc. for descendant in IterateGraph(node.GetDown(), types, inspectCaches, root): yield descendant # We update node for the outer loop to the next sibling of #node. This a bit weird form # of handling direct hierarchies both with the outer loop, the loop above, and this # GetNext() call is necessary to implement this semi-iteratively, and thereby avoid full # recursion which could easily lead to stack overflows/Python's deep recursion mechanism # kicking in. # # We also only go to the 'next' thing when #node was not what the user considered to be # the root. node = node.GetNext() if node != root else None # --- Scene graph traversal code end --------------------------------------------------------------- def GetThirdPartyPluginIds() -> list[int]: """Returns the IDs of plugins which are not located in the core libs directory. WARNING: As of 2023.1, this can contain false positives when Python plugins are installed on a machine. When this is the case, the returned plugin IDs will contain false positives for native im- and exporter plugins, as well as some command plugins. """ # Get all plugin paths of Cinema 4D. Unfortunately, this will also contain the core libs # path, i.e., a place where "native" plugins are being stored. pluginPaths: list[str] = [str(url) for url in maxon.Application.GetModulePaths()] # Get the installation path of the running app instance, e.g, "C:/Program Files/Maxon/2023.1.3", # and remove all plugin paths that are a sub-path of the application path. applicationPath: str = str(maxon.Application.GetUrl(maxon.APPLICATION_URLTYPE.STARTUP_DIR)) pluginPaths = [path for path in pluginPaths if not path.startswith(applicationPath)] # Now we must clean up these maxon.Url file paths a bit, so that their string is more compatible # with the Filename file paths returned by BasePlugin.GetFilename() below. We chop of the file # scheme and normalize the path so that it uses the OS specific path separators. pluginPaths = [os.path.normpath(str(p).lower().replace("file:///", "", 1)) for p in pluginPaths] # We are left with the plugin paths defined by the user and the plugin path in the user prefs, # e.g., ".../AppData/Roaming/Maxon/2023.1.3_97ABE84B/plugins". We could remove that one too # if we wanted to with APPLICATION_URLTYPE.PREFS_DIR, but I left it in. print (f"{pluginPaths = }") # Now we can iterate over the plugins to find third party plugins. result = {} for plugin in c4d.plugins.FilterPluginList(type=c4d.PLUGINTYPE_ANY, sortbyname=False): # Get the file name of the plugin and check if it lies on a "native path" or not. # WARNING: BasePlugin.GetFilename can return incorrect values at the moment. fileName: str = os.path.normpath(plugin.GetFilename().lower()) isExternal: bool = any([fileName.startswith(path) for path in pluginPaths]) if isExternal: result[plugin.GetID()] = f"{plugin.GetName()}, {fileName}" return result def main() -> None: """ """ # Get the third party plugin IDs and then iterate over the whole scene graph. thirdPartyPluginIds: list[int] = GetThirdPartyPluginIds() for node in IterateGraph(doc): if node.GetType() in thirdPartyPluginIds.keys(): print ("-" * 80) print (f"Found third party plugin node in scene graph: {node}") print (f"\t{node.GetType()}, {thirdPartyPluginIds[node.GetType()]}") if __name__ == "__main__": main()
-
Oh my gosh, so comprehensive answer! I'm so impressed that you put so much effort in it. You a great, thank you a lot!
-