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 use SetUserDataContainer() to restore it later. Is that possible? As GetUserDataContainer() returns a list, I'm not able to use BaseContainer's SetContainer() method as demonstrated in the example below. My first two attempts were discovering that the UserDataContainer from GetUserDataContainer() 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 the DescID 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 via BaseList2D.GetData() or via GeListNode.__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



  • Hi,

    your major misconception is about BaseList2D.GetUserDataContainer(). It returns a list of tuples, where the first value is the DescID 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 via BaseList2D.GetData() or via GeListNode.__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 the my_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 Windows ;)

    On the hyperfile stuff:

    1. Well, good or not good is a rather subjective classification. The only thing I can say, is thatHyperfile has been designed for this kind of scenario.
    2. 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.
    3. Not unless you really piss off your cat ;)
    4. 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



  • 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!


Log in to reply