Loading and Saving Plug-In Data
See Also: Class ReferenceMaker, Class ILoad, Class ISave, Class AppSave, Class AppLoad.
Overview
Plug-in developers need to be aware of the way 3ds max saves and loads a plug-in's data from disk. In certain cases, the plug-in's data will be saved automatically without the plug-in developer having to provide any special code. In other cases, the plug-in must save and load the data it maintains for its operation.
The following two cases are examples of 3ds max automatically saving plug-in data.
1 3ds max takes care of loading and saving any references the plug-in has when it is saved to disk. The plug-in does not need to explicitly save its references, nor does it need to load its references. After a scene is loaded, the references will automatically be restored. See the Advanced Topics section on References for more details on the loading and saving of references.
2 If a plug-in's parameters are all handled by parameter blocks, the plug-in does not need to save its parameters because the parameter block is responsible for loading and saving to disk.
If the plug-in maintains any other type of data it needs for its operation, it must specifically save this data to disk.
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 Load() and Save(). The plug-in implements these methods to load or save any special data it has.
The process works as follows: When a 3ds max file is saved, each plug-in object is asked to save its data. It is provided with an interface which has methods to write data. This data can be partitioned into chunks. A chunk is simply a container used to organize the data in the file. 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 Save() method is then called. When the plug-in begins execution in its Save() procedure, 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. In other words, any single chunk may contain either data only, or other sub-chunks only. If you want to put both data and chunks inside another chunk, the data needs to be bracketed inside a chunk itself. To save data, a Write() method is used that simply writes a block of bytes to the output stream. See the sample program below for an example using chunks and data to save information.
Loading a 3ds max file is similar. The plug-in is asked to load its data and is given an interface pointer which allows it to read chunks. When a plug-in DLL is asked to create a new instance of its class (using the Create() method of the plug-ins class descriptor), a parameter 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 parameter 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. The parameter referred to here is the loading flag passed into ClassDesc::Create(BOOL loading).
Sample Code
The following code demonstrates how both Save() and Load() may be implemented for a plug-in with two pieces of data it needs to save and restore.
This sample 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 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 Write() method. It then closes the chunk using 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 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 Read(). The chunk is then closed using 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:
IO_OK - The result was acceptable - no errors.
IO_END - This is returned from OpenChunk() when the end of the chunks at a certain level have been reached. It is used as a signal to terminate the processing of chunks at that level.
IO_ERROR - This is returned if an error occurred. Note that the plug-in should not put up a message box if a read error occurred. It should simply return the error status. This prevents a overabundance of messages from appearing.
For Reference information see: ILoad, ISave.
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 Class AppSave and Class AppLoad. Please see those classes for more details.