C#
Generic read/write methods for generic data in maxscript
I’ve spent a lot of time experimenting with multiple ways of saving data from 3dsmax: INI Files, raw text files written and read via maxscript (ew), and some pretty long-winded C# save/load classes. Each had its own pluses and minuses, but it was a chore to make any adjustment to the read/write code when anything about the structure of the data changed because it was all so very static in its functionality. The ideal solution is to allow our data to define our exports in a dynamic and flexible way that’s reusable.
The key to understanding how this works within max is to take a look at the maxscript command “getPropNames”, which allows us to query the names of all properties within a given structure or object. With Maxscript we can call this command using the self-referencing keyword “this” inside of a struct to gather all properties within the current context. Combine this with “getProperty” and “classof” and we have a simple way to query every property name, property value, and property type for most every structure within Maxscript. It is also a possibility to implement this within C# using similar methods, but we’ll focus on maxscript for this post:
struct animalType ( prop1 = "bear", prop2 = "cat", /* lots and lots of properties */ fn exportData = ( local properties = getPropNames this local propValues = for property in properties collect (getProperty this property) local propTypes = for value in propValues collect (classof value) ) )
These three commands give us three parallel arrays of all properties and methods within our structure, and most importantly, enough information to reconstruct the data when we want to re-load it again. This does not however, handle arrays or node references, which complicate things and will be discussed in a further post.
Moving on, we want to be able to write all of this accumulated data to some sort of file to save it. My favorite format this month is XML, so we’ll go ahead and utilize C#’s wonderful XMLWriter class to handle our output. Using our gathered data, We simply initialize our XMLwriter class and then iterate over our arrays, producing a single element for each property within our structure. At less than 40 lines with white space, it’s a pretty small piece of code that handles a lot of scenarios.
/*inside of a struct...*/
fn exportData xmlFile =
(
/* get all of our properties and methods*/
local properties = getPropNames this
local propValues = for property in properties collect (getProperty this property)
local propTypes = for value in propValues collect (classof value)
/*build a format class so we indent out output. makes it readable */
local xmlFormatter = dotnetobject "System.Xml.XmlWriterSettings"
xmlFormatter.Indent = true
/*Instantiate our xml writer class */
local XMLWriter = dotnetclass "System.Xml.XmlWriter"
XMLWriter = XMLWriter.Create xmlFile xmlFormatter
/*write the start of our document*/
XmlWriter.WriteStartDocument()
XmlWriter.WriteStartElement("structDef")
/* Iterate over all of our gathered properties, except methods*/
for i = 1 to properties.count where propTypes[i] != MAXScriptFunction do
(
/* create an element for each property. save it's type and name as an attribute */
XmlWriter.WriteStartElement("property")
XmlWriter.WriteAttributeString "name" (properties[i] as string)
XmlWriter.WriteAttributeString "type" (propTypes[i] as string)
XmlWriter.WriteString (propValues[i] as string)
XmlWriter.WriteEndElement()
)
/* write the tail of the xml document */
XmlWriter.WriteEndElement()
XmlWriter.WriteEndDocument()
/* close the file. finalizes the write and puts the data to the target */
XmlWriter.Close()
),
Running the above code yields xml similar to the example below.
<structDef> <property name="numEyes" type="Integer">6</property> <property name="id" type="String">hypnocat</property> <property name="type" type="String">cat</property> <property name="numLegs" type="Integer">15</property> <property name="averageLegLength" type="Float">70.31</property> </structDef>
When we load this data, are presented with an interesting challenge. Any data that we pull from the XML file is going to be of type string. If we assign all of these raw values straight back to our structure, we’re going to end up with a struct full of strings and lose any previous and possibly important type data regarding our parameters. Luckily, we can use our self-referencing to cast/convert all of our strings to the proper type.
fn loadData xmlFile =
(
/*open our xml file */
local xmlReader = dotNetObject "System.Xml.XmlDocument"
xmlReader.Load(xmlFile)
/* select all xml nodes of type 'property' */
local properties = dotNetClass "System.Xml.XPathNodeList"
properties = xmlReader.SelectNodes("structDef/property")
/*get the enumerator and iterate over all nodes*/
enum = properties.GetEnumerator()
while enum.MoveNext() do
(
/*build correct data type*/
local thisPropertyName = enum.current.Attributes.Item[0].Value
local thisPropertyType = enum.current.Attributes.Item[1].Value
local thisPropertyValue = enum.current.InnerText as (classof (getProperty this thisPropertyName))
/*set the property generically*/
setProperty this thisPropertyName thisPropertyValue
)
)
The key to preserving our property types is (classof (getProperty this thisPropertyName)). We can’t cast our values to their appropriate type using just the name of the Type as a string, so we work around that limitation by relying on the structure our load function exists within. Using the property name we’ve loaded, we call getProperty to fetch the already-existing value within our struct . This returns a “Type” rather than a string, and can be used to cast using “as”. Clever (or at least, I think so.)
This setup should handle most common struct properties except for arrays and node references, which we’ll handle later. This post is already incredibly long!