Advice for Saving User Data?
-
Hello,
I'd like to save the state of the user data of an object for the user to recall at a later time. What would be the best way to go about this? Rather than trying to navigate through the parent groups, I'd rather store the user data in a base container in its entirety and then useSetUserDataContainer()
to restore it later. Is that possible? AsGetUserDataContainer()
returns a list, I'm not able to use BaseContainer'sSetContainer()
method as demonstrated in the example below. My first two attempts were discovering that the UserDataContainer fromGetUserDataContainer()
is a list and not a Base Container. Thank you for any help you can give!import c4d PLUGIN_ID = 1234567 def main(doc): #get selected object obj = doc.GetActiveObject() #get object's base container bc = obj.GetDataInstance() #create new container to store user data values subBc = c4d.BaseContainer() #get user data container udc = obj.GetUserDataContainer() #returns a list: [(((700, 5, 0), (1, 400006001, 0)), <c4d.BaseContainer object at 0x00000271AB6F8870>)] #ATTEMPT 1: Storing the base container with SetContainer #subBc.SetContainer(PLUGIN_ID,udc) #TypeError: argument 2 must be c4d.BaseContainer, not list #ATTEMPT 2: Storing the list by index #subBc[1000] = udc #TypeError: could not convert 'list' #ATTEMPT 3: Using the DescID to store the user data's BaseContainers #for index,item in udc: # subBc[index] = item #TypeError: __setitem__ expected int, not c4d.DescID #adding the sub BaseContainer bc.SetContainer(PLUGIN_ID,subBc) #update object's BaseContainer obj.SetData(bc) #Print the values stored in the object's base container. for cid, value in bc.GetContainer(PLUGIN_ID): print cid, value # Execute main() if __name__=='__main__': main(doc)
-
Hi,
your major misconception is about
BaseList2D.GetUserDataContainer()
. It returns a list of tuples, where the first value is theDescID
and the second value is the description container for that element. The user data values are stored like all values in the data container of the node (accessible viaBaseList2D.GetData()
or viaGeListNode.__getitem__()
for singular values).Below you will find a script which probably does what you want to do.
import c4d def read_user_data(op, file_path, key): """Reads user data from a hyper file to a node. Args: op (c4d.BaseList2D): The node to write the data to. file_path (str): A file path to read the data from. key (int): The key for the hyper file. Returns: bool: If the file has been read successfully. """ # Meaningful exception for invalid file paths goes here. # Read the hyper file hf = c4d.storage.HyperFile() if not hf.Open(ident=key, filename=file_path, mode=c4d.FILEOPEN_READ, error_dialog=c4d.FILEDIALOG_NONE): # Meaningful exception for when read access fails goes here. print "Could not read file." return False bc = hf.ReadContainer() hf.Close() # Write the data back. for cid, value in bc: descid = (c4d.ID_USERDATA, cid) # Check if the type of the element in op at the given descid matches # the data type we want to write, i.e. if the structure of the user # data has not changed. if isinstance(value, type(op[descid])): op[descid] = value return True def save_user_data(op, file_path, key): """Writes user data of a node to a hyper file. Args: op (c4d.BaseList2D): The node to get the user data from. file_path (str): A file path to write the data to. key (int): The key for the hyper file. Returns: bool: If the file has been written successfully. """ # Meaningful exception for invalid file paths goes here. # Iterate over all user data values and store them under their user data # id in a BaseContainer. bc = c4d.BaseContainer() for descid, _ in op.GetUserDataContainer(): # The first DescLevel of user data DescIDs is the c4d.ID_USERDATA # constant. The second level holds the ID of the user data element. uid = descid[1].id bc[uid] = op[descid] # Write the BaseContainer to a hyper file. hf = c4d.storage.HyperFile() if not hf.Open(ident=key, filename=file_path, mode=c4d.FILEOPEN_WRITE, error_dialog=c4d.FILEDIALOG_NONE): # Meaningful exception for when write access fails goes here. print "Could not write file." return False hf.WriteContainer(bc) hf.Close() return True def main(): """ """ if not op: return my_hyperfile_key = 12345 my_file_path = r'D:\documents\my_file.hf' if save_user_data(op, my_file_path, my_hyperfile_key): read_user_data(op, my_file_path, my_hyperfile_key) c4d.EventAdd() if __name__ == '__main__': main()
Cheers
zipit -
@zipit Thank you!
Yes, I am seeing the issue in my misconception of the UserDataContainer. I tried your script out (with a file path to an actual file on my machine) and it returns "Could not write file" &
False
.I don't know much about Hyperfiles and have a few questions with regards to my original post if you don't mind:
- is that a good solution for a commercial plugin where I don't have control over the operating system's file paths?
- Why would I use a Hyperfile as opposed to a text file?
- Do Hyperfiles get deleted from the user's machine?
- Is writing the user data list to an object's BaseContainer impossible? It seems like a better solution to associate the data with the scene file rather than accumulating disk space in a location unknown to the user.
-
Hi,
have you made sure, that you did choose a file path where you actually have write access to? For example
C:\my_file.blah
will fail on a modern Windows machine, because Windows does restrict write access to the root of the primary drive if you do not give your user the specific permission. Similar things apply for Macs. Also make sure, you did use the correct path delimiter (/ or \) and that themy_path
string in the example is raw, i.e. you do not have to escape your delimiters. The example should work, I did test it. Well, at least on WindowsOn the hyperfile stuff:
- Well, good or not good is a rather subjective classification. The only thing I can say, is that
Hyperfile
has been designed for this kind of scenario. - It depends on what you mean by text file. If you actually mean a txt file: It is possible, but seems incredibly cumbersome, since you would have to handle all the serialisation. If you just mean a human readable format like json or xml: Yes, this is very much an option. The problem here is, that not all data in a node is hashable, or specifically, only the Python types are. So you would have to write a wrapper that (de)-serialises vectors, splines, etc. for you.
- Not unless you really piss off your cat
- Yes, this is an option. Just make sure to register an ID you can store your data under. I assumed you where after some kind of preset logic which is not bound to a specific c4d file.
Cheers
zipit - Well, good or not good is a rather subjective classification. The only thing I can say, is that
-
To any future readers of this post, I got this working by saving the User Data to a Base Container using @zipit 's code:
bc = c4d.BaseContainer() for descid, _ in op.GetUserDataContainer(): # The first DescLevel of user data DescIDs is the c4d.ID_USERDATA # constant. The second level holds the ID of the user data element. uid = descid[1].id bc[uid] = op[descid]
Thanks a lot for your help, @zipit !!!
-
hello,
nothing really yo add but while we are talking about hyperfiles we have this manual where you will find a lof of informations.
Cheers,
Manuel -
@m_magalhaes I will check it out. Thanks Manuel!
-
@zipit said in Advice for Saving User Data?:
# Write the data back. for cid, value in bc: descid = (c4d.ID_USERDATA, cid) # Check if the type of the element in op at the given descid matches # the data type we want to write, i.e. if the structure of the user # data has not changed. if isinstance(value, type(op[descid])): op[descid] = value
a notice about the code part above :
this gave me type errors and i solved it by not iterating through the bc, but instead through the ops UserData
and skipping any DTYPE_GROUP and DTYPE_SEPARATOR
(could be done also when reading the values, ofc)for descid, _ in op.GetUserDataContainer(): uid = descid[1].id dtype = descid[1].dtype name = _[c4d.DESC_NAME] value = bc[uid] print " uid={} dtype={} name={!r}".format(uid, dtype, name) if dtype == c4d.DTYPE_GROUP or dtype == c4d.DTYPE_SEPARATOR: pass elif dtype == c4d.DTYPE_SUBCONTAINER: pass # DTYPE_SUBCONTAINER - should never happen? elif isinstance(value, type(op[descid])): op[descid] = value else: pass # TYPE MISMATCH - should never happen?