Hello @Márton,
Thank you for reaching out to us. No, I would say we are fine here in this thread since the subject is literally 'Monitoring object parameter changes'.
The primary issue is that you are using the wrong method. You should be using BaseObject::GetUniqueIP but there is also a gotcha as it is not as unique as one might think it is. Because it is only unique within a cache. And since caches can contain caches, this can then lead to ambiguity.
I would also question a bit as how sensible it is that you poke around that deeply in caches. Even for render engines which must do scene complex synchronization, I find this a bit questionable. I understand the desire for doing this for performance reasons. But caches are not meant to be updated/scanned partially, when an object is dirty and rebuilt its cache, you should sync the full cache. I would always try to stay on the GeMarker/MAXON_CREATOR_ID level when possible on pull over whole new caches when a generator went dirty. Partially updating caches, i.e., you try to figure out what in a cache changed and only update that, strikes me as very error prone as this relies on the assumption that the plugin/object implementing that cache is doing nothing hacky itself for performance reasons (and there is no such gurantee). When carefully designed, one can problably write something that chery picks only the 'relevant' stuff, but I would consider this a workaround or hack.
I would also recommend having a look at How to detect a new light and pram change? for a practical example of how to sync a scene graph (and I deliberately did not go into the cache level there). The example is in Python but translates relatively directly to C++. As metioned in my code example below, in C++ one would usually use BaseList2D::GetGeMarker instead of MAXON_REATOR_ID as the former is the data the latter is based on (but markers are inaccessible in Python). Finally, here is some older thread about the subject of IDs.
Find below a brief example in Python. Again, I am aware that you are on C++. Please come back when you need help with translating any example code to C++.
Cheers,
Ferdinand
Result
Output in Cinema 4D for a Figure object, running the script a second time after changing the Segments paramater.
d8aaba2e-65f6-46e1-bafe-0132a9bc7b34-image.png
The diff. The GUID and marker ID obviously changed. But in the case of the Figure object it seems also to be the case that its hierarchy is not stable, something in the "Right Joint" hierarchy jumped. This also highlights why I would avoid tracking caches when I can, as they are meant to be dynamic and each object implementation can pull off countless hacky things with its cache. I would strongly recommend tracking the dirtiness of an object and operate on the generator level as shown in How to detect a new light and pram change?.
6858d95d-e22f-44b5-989b-021e98c659e6-image.png
Code
"""Stores the different identifiers of the objects in the cache of the selected object and compares
them to a previous state saved to disk.
To run this script, first save it to a file, e.g., `inspect_cache.py`, as it infers other file paths
from its own location. Then select an object in the Object Manager and execute the script. The script
will print the current cache of the selected object to the console and save it to disk. Now invoke
a cache rebuild on the object by for example changing a parameter. Run the script again and it will
print the new cache and compare it to the previous one. If the caches differ, a HTML diff will be
created and opened in the default web browser.
"""
import os
import difflib
import webbrowser
import c4d
import mxutils
doc: c4d.documents.BaseDocument # The currently active document.
op: c4d.BaseObject | None # The primary selected object in `doc`. Can be `None`.
DIFF_CHANGES: bool = True # If `True`, the script will open an HTML dif for the changes.
PATH_PREV: str = os.path.join(os.path.dirname(__file__), "id_prev.txt") # Path to the previous cache.
PATH_DIFF: str = os.path.join(os.path.dirname(__file__), "id_diff.html") # Path to the diff file.
def main() -> None:
"""Called by Cinema 4D when the script is being executed.
"""
if op is None:
raise ValueError("No object selected.")
# Iterate over the cache of the selected object #op.
result: str = ""
for node, _, depth, *_ in mxutils.RecurseGraph(op, yieldCaches=True, yieldHierarchy=True,
complexYield=True):
if not isinstance(node, c4d.BaseObject):
continue # Should not happen since we do not step into branches.
# The padded name of the object, e.g., " + Cube ".
head: str = f"{' ' * depth} + {node.GetName()}".ljust(40)
# This identifies an object uniquely in a cache and is persistent over cache rebuilding.
# A cache hierarchy [a, b, c] will always return the same unique IPs [s, t, u], even when
# the user changed a parameter resulting in the same hierarchy (but possibly otherwise
# different data). But there is a gotcha: These IDs are only unique within the same cache.
# And since caches can contain caches, you can end up with this:
#
# generator
# a - First cache element of #generator, UIP: 1
# b - First cache element of #a, UIP: 1
# c - Second cache element of #a, UIP: 2
# b - Second cache element of #generator, UIP: 2
# a - First cache element of #b, UIP: 1
# c - Second cache element of #b, UIP: 2
# c - Third cache element of #generator, UIP: 3
#
# There is no builtin way to safely decompose caches in that detail safely, you must write
# something yourself (you could for example hash everything up to the generator and call
# that the unique ID of the object).
#
uip: str = str(node.GetUniqueIP()).ljust(30)
# I have quite frankly no idea for what this ID is good for, I never used it. The docs
# also strike me as not quite correct, since they claim that this falls back to #uip, but in
# some cases, e.g., the Figure object, this does not seem to hold true, as this ID changes
# over cache rebuilds. This is also not persistent over Cinema 4D sessions (load/save).
guid: str = str(node.GetGUID()).ljust(30)
# This is the ID assigned to an object when it is being created. It wil change for cache
# rebuilds but is persistent over different Cinema 4D sessions (load/save). Since caches
# are being rebuilt (the whole point of generators) it is useless for identifying objects
# in a cache but for everything else it is the ID of choice. In C++ you can use GeMarker
# and BaseList2D::GetMarker() to directly access the data the MAXON_CREATOR_ID is based on.
uid: str = str(bytes(node.FindUniqueID(c4d.MAXON_CREATOR_ID)))
result += f"{head} uip = {uip} guid = {guid} uid = {uid}\n"
# Print our little tree to the console and check for changes.
print(f"Current hierarchy of {op}:")
print(result)
if os.path.exists(PATH_PREV):
cache: str = ""
with open(PATH_PREV, "r") as f:
cache: str = f.read()
print("-" * 80)
print("Previous hierarchy:")
print(cache)
print("-" * 80)
print("Cache is matching current hierarchy:", cache == result)
# Build the HTML diff when asked for and the cache has changed. Open it in the browser.
if cache != result and DIFF_CHANGES:
diff: str = difflib.HtmlDiff().make_file(
cache.splitlines(), result.splitlines(),
fromdesc="Previous cache", todesc="Current hierarchy")
with open(PATH_DIFF, "w") as f:
f.write(diff)
webbrowser.open(PATH_DIFF)
# Write the current hierarchy as the new previous state to disk.
with open(PATH_PREV, "w") as f:
f.write(result)
print("-" * 80)
print(f"Cache written to {PATH_PREV}.")
if __name__ == '__main__':
main()