Hello @mocoloco,
Thank you for reaching out to us. The behavior you are running into is caused by the Asset API. There are many facets to this problem of node reallocation.
Node Reallocation
Cinema 4D always has reallocated nodes much more frequently than it might appear to be the case for the user. But with the introduction of the Asset API, this behavior has increased significantly, which is why I then added this comment to NodeData::Init
both in Python and C++:
NodeData.Init is being called frequently due to nodes being reallocated in the background by Cinema 4D. One should therefore be careful with carrying out computationally complex tasks in this function.
This also applies by extension to NodeData::Free
. The underlying cause is that the Asset API is initializing and freeing nodes to manage presets. So, your plugin hook will not only be called for the actual node in a scene, but also all the throw-away instances allocated and freed by the asset API. With the help of the MAXON_CREATOR_ID
unique node ID, one can establish in this firework of node reallocations which one are actually the same.
So, let us take the TagData
example from your previous question and look at the data by adding the following code.
class DynamicDescriptionTagData(c4d.plugins.TagData):
"""Implements a tag hook that monitors the identity of the nodes representing the hook.
"""
@staticmethod
def GetNodeUuid(node: c4d.GeListNode) -> bytes:
"""Identifies a node over reallocation and scene reloading boundaries.
The returned UUID will express what a user would consider one node. When a user creates
a document, material, shader, object, tag, etc., it will get such UUID assigned and it
will stay the same even over reallocation and scene reloading boundaries.
"""
data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
return bytes(data or None)
@staticmethod
def GetIdNodeString(node: c4d.GeListNode) -> str:
"""Returns the memory location and MAXON_CREATOR_ID UUID of the node as a string.
"""
mem: str = str(hex(id(node))).upper()
uuid: str = str(DynamicDescriptionTagData.GetNodeUuid(node))
return f"mem: {mem}, uuid: {uuid}"
def Init(self, node: c4d.GeListNode) -> bool:
"""Called by Cinema 4D to initialize a tag instance.
Args:
node: The BaseTag instance representing this plugin object.
"""
print (f"Init: {DynamicDescriptionTagData.GetIdNodeString(node)}")
self.InitAttr(node, float, c4d.ID_OFF_X)
self.InitAttr(node, float, c4d.ID_OFF_Y)
node[c4d.ID_OFF_X] = 0.
node[c4d.ID_OFF_Y] = 0.
return True
def Free(self, node: c4d.GeListNode) -> None:
"""Called by Cinema 4D to free a tag instance.
Args:
node: The BaseTag instance representing this plugin object.
"""
print (f"Free: {DynamicDescriptionTagData.GetIdNodeString(node)}")
# ...
When we run this example with one instance of that tag in the scene and interact with the tag a bit, we will come up with something like this:
# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D530E3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D5318500, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00'
Free: mem: 0X7F95D5316640, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01$\xc1\xf7\x00Q9\x00\x00'
# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D5314FC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D52E9E40, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00'
Free: mem: 0X7F95D52E30C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01p\xc9\xf7\x00\xc49\x00\x00'
# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D52FB3C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D53096C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00'
Free: mem: 0X7F95D530A180, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\xdd\xcd\xf7\x006:\x00\x00'
# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D5309F00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D52F9D00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00'
Free: mem: 0X7F95D52FD980, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x9c\xd9\xf7\x00\xaf:\x00\x00'
# The actual tag in the scene is (re-)initialized.
Init: mem: 0X7F95D52FD940, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x19\xc1\xf7\x00\x079\x00\x00'
# An Asset API temp tag is being initialized and then freed right away.
Init: mem: 0X7F95D5317A80, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00'
Free: mem: 0X7F95D5313EC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,\xdd\xf7\x00Q;\x00\x00'
So, with this we can see, that:
- Even more than in C++, the memory location of nodes (
id()
) is in Python completely meaningless. Nodes are reallocated all the time.
- But one can see patterns by looking at the
MAXON_CREATOR_ID
UUID of nodes.
- Only the UUID for the actual node appears more than once per
Init
and Free
.
- The actual node in the scene is never freed (because I never deleted the tag).
- It is the temporary Asset API preset nodes which are freed.
What should I do?
There is no really good answer to this. Your underlying question is basically:
How can I distinguish a dummy node being passed to my node hook Init
, Free
, etc., from a 'real' node?
And the answer is currently: You cannot, even in C++. You can count UUIDs and exploit the patterns shown above, but that is quite unreliable. So, in the end you are unable to distinguish a dummy node being freed from a real one (at least from our current perspective).
But what you can do is detect a node being removed:
- By having some sort of watch node W which is watching a to be removed node R. There is no magic here, you can for example have a tag T1 which checks on each execution if the tag T2 does still exist.
- A bit more elegant solution is using event notifications, a non-public concept in the Cinema 4D API.
To implement (2.), you would do something like this:
def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
"""Called when expressions are evaluated to let a tag modify a scene.
Does nothing in this example.
"""
if not tag.FindEventNotification(doc, tag, c4d.NOTIFY_EVENT_REMOVE):
tag.AddEventNotification(tag, c4d.NOTIFY_EVENT_REMOVE, c4d.NOTIFY_EVENT_FLAG_NONE, c4d.BaseContainer())
return c4d.EXECUTIONRESULT_OK
def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool:
"""Called by Cinema 4D to convey events to the node.
"""
if mid == c4d.MSG_NOTIFY_EVENT:
print (f"{node} is being removed.")
return True
Which would then result in:
Init: mem: 0X7FDC359CCA00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Init: mem: 0X7FDC359B6100, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01:`\x86\x01\xf0)\x00\x00'
Free: mem: 0X7FDC359B53C0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01:`\x86\x01\xf0)\x00\x00'
Init: mem: 0X7FDC359C6700, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x84z\x86\x01\xfa)\x00\x00'
Free: mem: 0X7FDC359C6E00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x84z\x86\x01\xfa)\x00\x00'
Init: mem: 0X7FDC359DA640, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Free: mem: 0X7FDC359D5B00, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
Init: mem: 0X7FDC359D2580, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x87\xa4\x86\x01n*\x00\x00'
Free: mem: 0X7FDC359D2780, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01\x87\xa4\x86\x01n*\x00\x00'
Init: mem: 0X7FDC359DCBC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
<c4d.BaseTag object called DynDescription Tag/DynDescription Tag with ID 1060463 at 140583769067328> is being removed.
Free: mem: 0X7FDC359DCFC0, uuid: b'\x14}\xda\xa4\xdb\xf2\x17\x01,`\x86\x01\xbc)\x00\x00'
But here are also a few caveats to be aware of:
- You will not be notified when something up in the hierarchy chain is being removed and with it your node. So, you have a null object "Null.1" and attached to it your tag "MyTag" for which you registered for the remove event. Then "Null.1" is being removed, and with it your tag. You will not be notified, and instead only when the tag itself is being removed.
- There also other notify events, but for
NOTIFY_EVENT_FREE
you will for example need at least two nodes, one watcher and one watched, as you cannot notify yourself about being freed as you can for being removed. Some of the events will also be emitted for the dummy nodes.
- Event notification is private, and we cannot share details on demand here. You are more or less on your own.
One Important Last Thing
Your question was unintentionally quite board without telling us what you actually intended to do. In general, wanting to distinguish these events should not be necessary when one is using the plugin hooks how they are meant to be used and respecting their limits.
There are of course exceptions to this, and sometimes the classic API can get in your way. But I would urge you to try to rethink your problem, because that is often better than trying to go head first through a wall 
Cheers,
Ferdinand
Full Code:
"""Implements a tag which drives the position of a host object procedurally or via tracks.
"""
import c4d
import typing
class DynamicDescriptionTagData(c4d.plugins.TagData):
"""Implements a tag which drives the position of a host object procedurally or via tracks.
"""
# The plugin ID.
ID_PLUGIN: int = 1060463
# What you dubbed "user data", a dictionary holding some description data to be injected at
# runtime. I just changed things a bit up and include/identify things over their ID instead
# of made-up strings, but nothing would prevent you from having both, it would just be a bit
# more complicated to parse such data:
#
# INTERFACE_DATA = [
# {
# "label": "Foo",
# "id": c4d.DescId(...),
# "data": [
# {"label": "Name", "id": c4d.DESC_NAME, "value": "Bar"},
# ...
# ]
# },
# ...
# ]
INTERFACE_DATA: list[tuple[c4d.DescID, dict[int, typing.Any]]] = [
(c4d.DescID(c4d.DescLevel(1000, c4d.DTYPE_REAL, 0)),
{
c4d.DESC_NAME: "foo.x",
c4d.DESC_SHORT_NAME: "foo.x",
c4d.DESC_MIN: c4d.utils.DegToRad(-45.),
c4d.DESC_MAX: c4d.utils.DegToRad(45.),
c4d.DESC_MINSLIDER: c4d.utils.DegToRad(-45.),
c4d.DESC_MAXSLIDER: c4d.utils.DegToRad(45.),
}),
(c4d.DescID(c4d.DescLevel(1001, c4d.DTYPE_REAL, 0)),
{
c4d.DESC_NAME: "bar.y",
c4d.DESC_SHORT_NAME: "bar.y",
c4d.DESC_MIN: c4d.utils.DegToRad(-180.),
c4d.DESC_MAX: c4d.utils.DegToRad(180.),
c4d.DESC_MINSLIDER: c4d.utils.DegToRad(-180.),
c4d.DESC_MAXSLIDER: c4d.utils.DegToRad(180.),
}),
]
@staticmethod
def GetNodeUuid(node: c4d.GeListNode) -> bytes:
"""Identifies a node over reallocation and scene reloading boundaries.
The returned UUID will express what a user would consider one node. When a user creates
a document, material, shader, object, tag, etc., it will get such UUID assigned and it
will stay the same even over reallocation and scene reloading boundaries.
"""
data: memoryview = node.FindUniqueID(c4d.MAXON_CREATOR_ID)
return bytes(data or None)
@staticmethod
def GetIdNodeString(node: c4d.GeListNode) -> str:
"""Returns the memory location and MAXON_CREATOR_ID UUID of the node as a string.
"""
mem: str = str(hex(id(node))).upper()
uuid: str = str(DynamicDescriptionTagData.GetNodeUuid(node))
return f"mem: {mem}, uuid: {uuid}"
def Init(self, node: c4d.GeListNode) -> bool:
"""Called by Cinema 4D to initialize a tag instance.
Args:
node: The BaseTag instance representing this plugin object.
"""
print (f"Init: {DynamicDescriptionTagData.GetIdNodeString(node)}")
self.InitAttr(node, float, c4d.ID_OFF_X)
self.InitAttr(node, float, c4d.ID_OFF_Y)
node[c4d.ID_OFF_X] = 0.
node[c4d.ID_OFF_Y] = 0.
return True
def Free(self, node: c4d.GeListNode) -> None:
"""Called by Cinema 4D to free a tag instance.
"""
print (f"Free: {DynamicDescriptionTagData.GetIdNodeString(node)}")
def Execute(self, tag: c4d.BaseTag, doc: c4d.documents.BaseDocument, op: c4d.BaseObject,
bt: c4d.threading.BaseThread, priority: int, flags: int) -> int:
"""Called when expressions are evaluated to let a tag modify a scene.
Does nothing in this example.
"""
if not tag.FindEventNotification(doc, tag, c4d.NOTIFY_EVENT_REMOVE):
tag.AddEventNotification(tag, c4d.NOTIFY_EVENT_REMOVE, c4d.NOTIFY_EVENT_FLAG_NONE, c4d.BaseContainer())
return c4d.EXECUTIONRESULT_OK
def Message(self, node: c4d.GeListNode, mid: int, data: object) -> bool:
"""Called by Cinema 4D to convey events to the node.
"""
if mid == c4d.MSG_NOTIFY_EVENT:
print (f"{node} is being removed.")
return True
def GetDDescription(self, node: c4d.GeListNode, description: c4d.Description,
flags: int) -> typing.Union[bool, tuple[bool, int]]:
"""Called by Cinema 4D when the description of a node is being evaluated to let the node
dynamically modify its own description.
"""
if not description.LoadDescription(node.GetType()):
return False, flags
singleId: c4d.DescID = description.GetSingleDescID()
for paramId, dataDictionary in DynamicDescriptionTagData.INTERFACE_DATA:
if (singleId and not paramId.IsPartOf(singleId)):
return True, flags
data: c4d.BaseContainer = description.GetParameterI(paramId)
if data is None:
return True, flags
for key, value in dataDictionary.items():
data[key] = value
return True, flags | c4d.DESCFLAGS_DESC_LOADED
def RegisterPlugins() -> None:
"""Registers the plugins contained in this file.
"""
if not c4d.plugins.RegisterTagPlugin(
id=DynamicDescriptionTagData.ID_PLUGIN,
str="DynDescription Tag",
info=c4d.TAG_EXPRESSION | c4d.TAG_VISIBLE,
g=DynamicDescriptionTagData, description="tdyndescription",
icon=c4d.bitmaps.InitResourceBitmap(c4d.ID_MODELING_MOVE)):
print("Warning: Failed to register 'DynDescription' tag plugin.")
if __name__ == "__main__":
RegisterPlugins()