Loading and Saving Plug-in Data

3ds Max automatically saves all items referenced by a plug-in when saving the plug-in to a scene file. All parameter blocks are expected to be exposed as references and will therefore be saved and loaded automatically. Any additional data that is not explicitly referenced by the plug-in or contained in a parameter block must be saved or loaded manually.

There are two methods of the ReferenceMaker class which are called by the system when the plug-in's data needs to be loaded or saved. These methods are named ReferenceMaker::Load() and ReferenceMaker::Save(). The plug-in implements these methods to load or save any special data it has, that is not managed by the parameter blocks.

When a 3ds Max file is saved, each plug-in object is asked to save its data and is provided with an ISave pointer. This data can be partitioned into chunks. When an object needs to be saved, 3ds Max creates a chunk for the object. Inside that chunk, another chunk is created to contain reference information which the system writes. The plug-in's ReferenceMaker::Save() method is then called. Before ReferenceMaker::Save() is called the file pointer is positioned inside the plug-in's chunk but after the first chunk containing the reference data. The plug-in can then create its own chunks. Each chunk may contain sub-chunks or data, but not both. If you want to put both data and chunks inside another chunk, the data needs to be contained in a separate chunk. To save a block of binary data, the ISave::Write() method is used that writes a block of bytes to the output stream.

Loading a 3ds Max file is similar. The plug-in is asked to load its data and is given an ILoad pointer which allows it to read chunks. When a plug-in DLL is asked to create a new instance of its class (using the ClassDesc::Create() method), a flag is passed to indicate whether the new instance is being created with the intention of loading an object from a disk file. In this way, if the flag is TRUE, the object doesn't have to perform any initialization because it is guaranteed that it will be asked to read data from a file.

Loading and Saving Class Data

The following three methods of the ClassDesc class may be used to save data associated with a class in a 3ds Max file. This is appropriate for preference data and other similar information associated with a class. If you want to save data associated with the class then you must override ClassDesc::NeedsToSave() to return TRUE and implement the Save() and Load() methods. See the topic on Loading and Saving for more details on the way data is written to and read from 3ds Max files.

Note: One peculiarity with the loading and saving of the class data is that if the user does a File New or File Reset then the class data is not cleared.

Loading and Saving Hierarchical Data

There are two classes available for writing and reading hierarchical data structures to a linear stream, such as an AppData block. These are AppSave and AppLoad. Please see those classes for more details.

Sample Code

The following code demonstrates how both ReferenceMaker::Save() and ReferenceMaker::Load() may be implemented for a plug-in with two pieces of data it needs to save and restore.

This example writes an array of 10 floating point values and a single DWORD containing flags. The process of saving is very simple. The output file is already open and ready to be written to. A pointer to the ISave class has been passed in with which the plug-in can call methods to save data. The plug-in begins by creating a chunk using the ISave::BeginChunk() method and passing an ID it has defined. This ID can be any USHORT, as only the plug-in's loading and saving procedures will ever use it. It then writes the data using the ISave::Write() method. It then closes the chunk using ISave::EndChunk(). It begins a new chunk and writes the flag data. After ending this chunk it returns IO_OK to indicate a successful save.

The data is saved in chunks like this so that it may be loaded in a manner which is not order dependent. One could write the flags first and the array second and it would still be read correctly.

#define SAMPLE_DATA_CHUNK 1000
#define SAMPLE_FLAGS_CHUNK 1010
 
IOResult Sample::Save(ISave* isave)
{
   ULONG nb;
   isave->BeginChunk(SAMPLE_DATA_CHUNK);
   isave->Write(myArray, sizeof(float) * 10, &nb);
   isave->EndChunk();
   isave->BeginChunk(SAMPLE_FLAGS_CHUNK);
   isave->Write(&flags, sizeof(DWORD), &nb);
   isave->EndChunk();
   return IO_OK;
}

The following code demonstrates the loading process. Again, in preparation for this call the system has opened the file and positioned the file pointer at the plug-in's objects chunk. The code loops, opening chunks and reading the data until ILoad::OpenChunk() no longer returns IO_OK. This indicates there are no more chunk at this level and the process is done.

Inside the loop, the code switches on the chunk ID. For each ID it recognizes it reads the data using ILoad::Read(). The chunk is then closed using ILoad::CloseChunk(). If Read() did not return IO_OK an error occurred and this error code is returned. Otherwise, the loop begins again.

If the loading was successful, IO_OK is returned to indicate so.

IOResult Sample::Load(ILoad* iload)
{
   ULONG nb;
   IOResult res;
   while (IO_OK==(res=iload->OpenChunk()))
   {
     switch(iload->CurChunkID())
     {
      case SAMPLE_DATA_CHUNK:
         res=iload->Read(myArray, sizeof(float)*10, &nb);
         break;
 
      case SAMPLE_FLAGS_CHUNK:
         res=iload->Read(&flags, sizeof(DWORD), &nb);
         break;
     }
     iload->CloseChunk();
     if (res!=IO_OK) return res;
   }
   return IO_OK;
}

The possible return values for IOResult are: