Hey @aghiad322,
Thank you for your code. It is still very unclear to me what you are doing on a higher more abstract level, and on a concrete level, where exactly you want to detect something. Find below your commented code and at the end a few shots into the dark from me regarding what you are trying to do.
I also just saw now that you posted in the wrong forum. I moved your topic and marked it as C++ and 2025 since you said you are not using the 2026 SDK.
I hope this help and cheers,
Ferdinand
// I wrote this blind without compiling it. So ,there might be some syntax issues in the code I wrote,
// but it should still illustrate the concepts well enough.
#include "maxon/weakrawptr.h"
using namespace cinema;
using namespace maxon;
struct NexusMember
{
// While we could consider a pointer somewhat conceptually a weak reference, it is factually
// not one, as the pointed object being deleted does not invalidate the pointer. This can lead
// to dangling pointers and access violation crashes.
BaseObject* object = nullptr; // weak ref, don’t own it
Bool isChild = false; // "include children" flag
// This is better.
WeakRawPtr<BaseTag> _tagWeakPtr;
// Gets the tag pointed to by this member, or nullptr if it has been deleted.
BaseTag* GetTag() const
{
return _tagWeakPtr.Get();
}
// Sets the tag for this member.
void Set(const BaseTag* tag)
{
_tagWeakPtr = _tagWeakPtr.Set(tag);
}
};
struct NexusGroup
{
Vector color;
// No, do not use the std library. Cinema 4D modules are by default compiled without exception
// handling for performance reasons, and the standard library uses exceptions for error handling,
// making its behavior undefined in such builds. std::vector::push_back() can throw
// std::bad_alloc, for example.
std::vector<String> links;
std::vector<NexusMember> members;
Int32 dirtyLevel = 0;
// MUST be:
BaseArray<String> links;
BaseArray<NexusMember> members;
};
class NexusRegistry : public SceneHookData
{
public:
static NexusRegistry* Get(BaseDocument* doc);
static NodeData* Alloc();
virtual Bool Message(GeListNode* node, Int32 type, void* data);
virtual EXECUTIONRESULT Execute(BaseSceneHook* node, BaseDocument* doc,
BaseThread* bt, Int32 priority, EXECUTIONFLAGS flags);
// The same applies here, the Maxon API alternatives to unordered_map are:
//
// * HashSet (just a hash set of fixed type values without keys)
// * HashMap(a hash map of fixed key and value type),
// * DataDictionary (a hash map of arbitrary key and value types).
std::unordered_map<Int32, NexusGroup> groups;
maxon::BaseArray<BaseTag*> pendingRegistration;
void ProcessPendingTags(BaseDocument* doc);
void RebuildRegistry(BaseDocument* doc);
void UpdateGroupFromTag(BaseDocument* doc, BaseTag* tag);
void RemoveObjectFromGroups(BaseObject* obj);
void RemoveTagFromGroup(BaseTag* tag);
const NexusGroup* GetGroup(Int32 hash) const;
};
// -------------------------------------------------------------------------------------------------
void NexusRegistry::UpdateGroupFromTag(BaseDocument* doc, BaseTag* tag)
{
if (!tag) return;
BaseObject* obj = tag->GetObject();
if (!obj) return;
BaseContainer* tagContainer = tag->GetDataInstance();
String id = tagContainer->GetString(ID);
// I do not know of which nature this HashID is, but our API has multiple hash functions.
Int32 hashedID = HashID(id);
// There is for once StringTemplate::GetHashCode(). I.e., you could do this:
const HashInt = id.GetHashCode();
// But all scene elements already have a marker on them which uniquely identifies them (if that
// is what you are after).
// Uniquely identifies over its time of creation and the machine it was created on. I.e., this
// value is persistent across sessions and unique across machines.
const GeMarker uuid = tag->GetMarker();
Bool includeChildren = tagContainer->GetBool(CHILDREN);
Int32 oldIdHash = tagContainer->GetInt32(NEXUS_TAG_PREV_ID, NOTOK);
if (oldIdHash != NOTOK && oldIdHash != hashedID)
{
auto it = groups.find(oldIdHash);
if (it != groups.end())
{
auto& members = it->second.members;
// std::vector::erase is not noexcept, this can crash Cinema 4D, unless you compile your
// module with exception handling enabled (which we do not recommend for performance
// reasons). I am not going to repeat this comment in similar places below.
members.erase(std::remove_if(members.begin(), members.end(),
[obj](const NexusMember& m) { return m.object == obj; }),
members.end());
it->second.dirtyLevel++;
if ((Int32)members.size() == 0)
groups.erase(it);
}
}
// Update new group
NexusGroup& group = groups[hashedID];
// Remove duplicates of this object first
group.members.erase(std::remove_if(group.members.begin(), group.members.end(),
[obj](const NexusMember& m) { return m.object == obj; }),
group.members.end());
group.members.push_back({ obj, includeChildren });
((Nexus*)tag)->UpdateInfo(doc, tag);
// Store current ID for next update
tagContainer->SetInt32(NEXUS_TAG_PREV_ID, hashedID);
}
// -------------------------------------------------------------------------------------------------
void NexusRegistry::ProcessPendingTags(BaseDocument* doc)
{
if (pendingRegistration.IsEmpty())
return;
Int32 i = 0;
while (i < pendingRegistration.GetCount())
{
BaseTag* tag = pendingRegistration[i];
BaseObject* op = tag->GetObject();
if (op)
{
UpdateGroupFromTag(doc, tag);
// This is not how our error system works. Functions of type Result<T> are our exception
// handling equivalent. You need iferr statements to catch and/or propagate errors. See
// code below.
maxon::ResultRef eraseresult = pendingRegistration.Erase(i, 1);
}
else
{
i++;
}
}
}
// -------------------------------------------------------------------------------------------------
// See https://developers.maxon.net/docs/cpp/2026_0_0/page_maxonapi_error_overview.html for a more in detail
// explanation of our error handling system.
// So we have some class with a field whose type has a function of return type Result<T>, e.g., your
// NexusRegistry. We have now three ways to handle errors when calling such functions:
// Ignoring errors (not recommended):
void NexusRegistry::AddItem(const String name)
{
links.Append(name) iferr_ignore("I do not care about errors here."); // Append is of type Result<void>
}
// Handling errors locally, i.e., within a function that itself is not of type Result<T>.
Bool NexusRegistry::RemoveItem(const String name)
{
// The scope handler for this function so that we can terminate errors when the are thrown.
iferr_scope_handler
{
// Optional, print some debug output to the console, #err is the error object.
DiagnosticOutput("Error in @: @", MAXON_FUNCTIONNAME, err);
// We just return false to indicate failure. If we would have to do cleanup/unwinding, we
// would do it here.
return false;
};
const Int32 i = links.FindIndex(name);
// We call our Result<T> function and propagate any error to the scope handler if an error
// occurs. The iferr_return keyword basically unpacks a Result<T> into its T value, or jumps
// to the error handler in the current or higher scope and propagates the error.
const String item = links.Erase(i) iferr_return;
return true;
}
// And the same thing in green but we propagate errors further up the call chain, i.e., our function
// is itself of type Result<T>. It now also does not make too much sense to return a Bool, so our
// return type is now Result<void>.
Result<void> NexusRegistry::RemoveItem(const String name)
{
// Here we just use the default handler which will just return the #err object to the caller.
iferr_scope;
const Int32 i = links.FindIndex(name);
const String item = links.Erase(i) iferr_return;
return OK; // Result<void> functions return OK on success.
}
// -------------------------------------------------------------------------------------------------
EXECUTIONRESULT NexusRegistry::Execute(BaseSceneHook* node, BaseDocument* doc,
BaseThread* bt, Int32 priority, EXECUTIONFLAGS flags)
{
ProcessPendingTags(doc);
return EXECUTIONRESULT::OK;
}
// -------------------------------------------------------------------------------------------------
// So, all in all, this does not shed too much light on what you are doing for me :) The main questions
// is if you implement your tags yourself, i.e., the items stored in your NexusGroup::members.
// When you implement a node yourself, you can override its ::Read, ::Write, and ::CopyTo functions
// to handle the node serialization and copying yourself. See https://tinyurl.com/2v4ajn58 for a
// modern example for that. So for your, let's call it NexusTag, you would do something like this:
class NexusTag : public TagData
{
Bool CopyTo(NodeData* dest, const GeListNode* snode, GeListNode* dnode, COPYFLAGS flags,
AliasTrans* trn) const
{
// This is a copy and paste event. There is no dedicated event for CTRL + DRAG you seem
// to after.
if (flags & PRIVATE_CLIPBOARD_COPY)
{
// Do something special with the destination node #dnode or call home to you registry.
}
else
{
// Do something different.
}
// We should always call the base class implementation, unless we want interrupt the copy.
return SUPER::CopyTo(dest, snode, dnode, flags, trn);
}
};
// ---
// Another way could using a MessageData hook and monitoring the EVMSG_CHANGE events, i.e., when
// something in a scene changed. This is usually how render engines synchronize scene graphs. I am
// not going to exemplify this here, as this is a lot of work.But you can have a look at this thread
// which is the most simple example we have for this (in Python, but is more or less the same in C++):
// https://developers.maxon.net/forum/topic/14124/how-to-detect-a-new-light-and-pram-change
// Here you do not have to own the tag implementation. But you could not detect how something has
// been inserted, only that it has been inserted.
// ---
// Yet another thing which could help are event notifications. I.e., you hook yourself into the copy
// event of any node (you do not have to own the node implementation for this) and get notified when
// a copy occurs. But event notifications are private for a reason, as you can easily crash Cinema
// with them. You will find some material on the forum, but they are intentionally not documented.
// https://tinyurl.com/2jj8xa6s
// ---
// Finally, with NodeData::Init you can also react in your node to it being cloned.
Bool NexusTag::Init(GeListNode* node, Bool isCloneInit)
{
if (isCloneInit)
{
// Do something special when this is a clone operation.
}
return true;
}