diff --git a/SabreTools.Library/Tools/ClrMameProWriter.cs b/SabreTools.Library/Tools/ClrMameProWriter.cs
new file mode 100644
index 00000000..c2f55f88
--- /dev/null
+++ b/SabreTools.Library/Tools/ClrMameProWriter.cs
@@ -0,0 +1,482 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace SabreTools.Library.Tools
+{
+ ///
+ /// ClrMamePro writer patterned heavily off of XmlTextWriter
+ ///
+ ///
+ public class ClrMameProWriter
+ {
+ ///
+ /// State machine state for use in the table
+ ///
+ private enum State
+ {
+ Start,
+ Prolog,
+ Element,
+ Attribute,
+ Content,
+ AttrOnly,
+ Epilog,
+ Error,
+ Closed,
+ }
+
+ ///
+ /// Potential token types
+ ///
+ private enum Token
+ {
+ None,
+ Standalone,
+ StartElement,
+ EndElement,
+ LongEndElement,
+ StartAttribute,
+ EndAttribute,
+ Content,
+ }
+
+ ///
+ /// Tag information for the stack
+ ///
+ private struct TagInfo
+ {
+ public string Name;
+ public bool Mixed;
+
+ public void Init()
+ {
+ Name = null;
+ Mixed = false;
+ }
+ }
+
+ ///
+ /// Internal stream writer
+ ///
+ private StreamWriter textWriter;
+
+ ///
+ /// Stack for tracking current node
+ ///
+ private TagInfo[] stack;
+
+ ///
+ /// Pointer to current top element in the stack
+ ///
+ private int top;
+
+ ///
+ /// State table for determining the state machine
+ ///
+ private readonly State[] stateTable = {
+ // State.Start State.Prolog State.Element State.Attribute State.Content State.AttrOnly State.Epilog
+ //
+ /* Token.None */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
+ /* Token.Standalone */ State.Prolog, State.Prolog, State.Content, State.Content, State.Content, State.Error, State.Epilog,
+ /* Token.StartElement */ State.Element, State.Element, State.Element, State.Element, State.Element, State.Error, State.Element,
+ /* Token.EndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
+ /* Token.LongEndElement */ State.Error, State.Error, State.Content, State.Content, State.Content, State.Error, State.Error,
+ /* Token.StartAttribute */ State.AttrOnly, State.Error, State.Attribute, State.Attribute, State.Error, State.Error, State.Error,
+ /* Token.EndAttribute */ State.Error, State.Error, State.Error, State.Element, State.Error, State.Epilog, State.Error,
+ /* Token.Content */ State.Content, State.Content, State.Content, State.Attribute, State.Content, State.Attribute, State.Epilog,
+ };
+
+ ///
+ /// Current state in the machine
+ ///
+ private State currentState;
+
+ ///
+ /// Last seen token
+ ///
+ private Token lastToken;
+
+ ///
+ /// Get if quotes should surround attribute values
+ ///
+ public bool Quotes { get; set; }
+
+ ///
+ /// Constructor for opening a write from a file
+ ///
+ public ClrMameProWriter(string filename)
+ {
+ textWriter = new StreamWriter(filename);
+ Quotes = true;
+ stack = new TagInfo[10];
+ top = 0;
+ }
+
+ ///
+ /// Constructor for opening a write from a stream and encoding
+ ///
+ public ClrMameProWriter(Stream stream, Encoding encoding)
+ {
+ textWriter = new StreamWriter(stream, encoding);
+ Quotes = true;
+
+ // Element stack
+ stack = new TagInfo[10];
+ top = 0;
+ stack[top].Init();
+ }
+
+ ///
+ /// Base stream for easy access
+ ///
+ public Stream BaseStream
+ {
+ get { return textWriter?.BaseStream ?? null; }
+ }
+
+ ///
+ /// Write the start of an element node
+ ///
+ public void WriteStartElement(string name)
+ {
+ try
+ {
+ AutoComplete(Token.StartElement);
+ PushStack();
+ stack[top].Name = name;
+ textWriter.Write(name);
+ textWriter.Write(" (");
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Write the end of an element node
+ ///
+ public void WriteEndElement()
+ {
+ InternalWriteEndElement(false);
+ }
+
+ ///
+ /// Write the end of a mixed element node
+ ///
+ public void WriteFullEndElement()
+ {
+ InternalWriteEndElement(true);
+ }
+
+ ///
+ /// Write a complete element with content
+ ///
+ public void WriteElementString(string name, string value)
+ {
+ WriteStartElement(name);
+ WriteString(value);
+ WriteEndElement();
+ }
+
+ ///
+ /// Write the start of an attribute node
+ ///
+ public void WriteStartAttribute(string name)
+ {
+ try
+ {
+ AutoComplete(Token.StartAttribute);
+ textWriter.Write(name);
+ textWriter.Write(" ");
+ if (Quotes)
+ textWriter.Write("\"");
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Write the end of an attribute node
+ ///
+ public void WriteEndAttribute()
+ {
+ try
+ {
+ AutoComplete(Token.EndAttribute);
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Write a complete attribute with content
+ ///
+ public void WriteAttributeString(string name, string value)
+ {
+ WriteStartAttribute(name);
+ WriteString(value);
+ WriteEndAttribute();
+ }
+
+ ///
+ /// Write a standalone attribute
+ ///
+ public void WriteStandalone(string name, string value, bool? quoteOverride = null)
+ {
+ try
+ {
+ if (string.IsNullOrEmpty(name))
+ throw new ArgumentException();
+
+ AutoComplete(Token.Standalone);
+ textWriter.Write(name);
+ textWriter.Write(" ");
+ if ((quoteOverride == null && Quotes)
+ || (quoteOverride == true))
+ {
+ textWriter.Write("\"");
+ }
+ textWriter.Write(value);
+ if ((quoteOverride == null && Quotes)
+ || (quoteOverride == true))
+ {
+ textWriter.Write("\"");
+ }
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Write a string content value
+ ///
+ public void WriteString(string value)
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(value))
+ {
+ AutoComplete(Token.Content);
+ textWriter.Write(value);
+ }
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Close the writer
+ ///
+ public void Close()
+ {
+ try
+ {
+ AutoCompleteAll();
+ }
+ catch
+ {
+ // Don't fail at this step
+ }
+ finally
+ {
+ currentState = State.Closed;
+ textWriter.Close();
+ }
+ }
+
+ ///
+ /// Flush the base TextWriter
+ ///
+ public void Flush()
+ {
+ textWriter.Flush();
+ }
+
+ ///
+ /// Prepare for the next token to be written
+ ///
+ private void AutoComplete(Token token)
+ {
+ // Handle the error cases
+ if (currentState == State.Closed)
+ throw new InvalidOperationException();
+ else if (currentState == State.Error)
+ throw new InvalidOperationException();
+
+ State newState = stateTable[(int)token * 7 + (int)currentState];
+ if (newState == State.Error)
+ throw new InvalidOperationException();
+
+ // TODO: Figure out how to get attributes on their own lines ONLY if an element contains both attributes and elements
+ switch (token)
+ {
+ case Token.StartElement:
+ case Token.Standalone:
+ if (currentState == State.Attribute)
+ {
+ WriteEndAttributeQuote();
+ WriteEndStartTag(false);
+ }
+ else if (currentState == State.Element)
+ {
+ WriteEndStartTag(false);
+ }
+
+ if (currentState != State.Start)
+ Indent(false);
+
+ break;
+
+ case Token.EndElement:
+ case Token.LongEndElement:
+ if (currentState == State.Attribute)
+ WriteEndAttributeQuote();
+
+ if (currentState == State.Content)
+ token = Token.LongEndElement;
+ else
+ WriteEndStartTag(token == Token.EndElement);
+
+ break;
+
+ case Token.StartAttribute:
+ if (currentState == State.Attribute)
+ {
+ WriteEndAttributeQuote();
+ textWriter.Write(' ');
+ }
+ else if (currentState == State.Element)
+ {
+ textWriter.Write(' ');
+ }
+
+ break;
+
+ case Token.EndAttribute:
+ WriteEndAttributeQuote();
+ break;
+
+ case Token.Content:
+ if (currentState == State.Element && lastToken != Token.Content)
+ WriteEndStartTag(false);
+
+ if (newState == State.Content)
+ stack[top].Mixed = true;
+
+ break;
+
+ default:
+ throw new InvalidOperationException();
+ }
+
+ currentState = newState;
+ lastToken = token;
+ }
+
+ ///
+ /// Autocomplete all open element nodes
+ ///
+ private void AutoCompleteAll()
+ {
+ while (top > 0)
+ {
+ WriteEndElement();
+ }
+ }
+
+ ///
+ /// Internal helper to write the end of an element
+ ///
+ private void InternalWriteEndElement(bool longFormat)
+ {
+ try
+ {
+ if (top <= 0)
+ throw new InvalidOperationException();
+
+ AutoComplete(longFormat ? Token.LongEndElement : Token.EndElement);
+ if (this.lastToken == Token.LongEndElement)
+ {
+ Indent(true);
+ textWriter.Write(')');
+ }
+
+ top--;
+ }
+ catch
+ {
+ currentState = State.Error;
+ throw;
+ }
+ }
+
+ ///
+ /// Internal helper to write the end of a tag
+ ///
+ private void WriteEndStartTag(bool empty)
+ {
+ if (empty)
+ textWriter.Write(" )");
+ }
+
+ ///
+ /// Internal helper to write the end of an attribute
+ ///
+ private void WriteEndAttributeQuote()
+ {
+ if (Quotes)
+ textWriter.Write("\"");
+ }
+
+ ///
+ /// Internal helper to indent a node, if necessary
+ ///
+ private void Indent(bool beforeEndElement)
+ {
+ if (top == 0)
+ {
+ textWriter.WriteLine();
+ }
+ else if (!stack[top].Mixed)
+ {
+ textWriter.WriteLine();
+ int i = beforeEndElement ? top - 1 : top;
+ for (; i > 0; i--)
+ {
+ textWriter.Write('\t');
+ }
+ }
+ }
+
+ ///
+ /// Move up one element in the stack
+ ///
+ private void PushStack()
+ {
+ if (top == stack.Length - 1)
+ {
+ TagInfo[] na = new TagInfo[stack.Length + 10];
+ if (top > 0) Array.Copy(stack, na, top + 1);
+ stack = na;
+ }
+
+ top++; // Move up stack
+ stack[top].Init();
+ }
+ }
+}