diff --git a/SabreTools.Library/Tools/SeparatedValueWriter.cs b/SabreTools.Library/Tools/SeparatedValueWriter.cs
new file mode 100644
index 00000000..a7a9f475
--- /dev/null
+++ b/SabreTools.Library/Tools/SeparatedValueWriter.cs
@@ -0,0 +1,143 @@
+using System;
+using System.IO;
+using System.Text;
+
+namespace SabreTools.Library.Tools
+{
+ public class SeparatedValueWriter : IDisposable
+ {
+ ///
+ /// Internal stream writer for outputting
+ ///
+ private StreamWriter sw;
+
+ ///
+ /// Internal value if we've written a header before
+ ///
+ private bool header = false;
+
+ ///
+ /// Internal value if we've written our first line before
+ ///
+ private bool firstRow = false;
+
+ ///
+ /// Internal value to say how many fields should be written
+ ///
+ private int fields = -1;
+
+ ///
+ /// Set if values should be wrapped in quotes
+ ///
+ public bool Quotes { get; set; } = true;
+
+ ///
+ /// Set what string should be used as a separator
+ ///
+ public string Separator { get; set; } = ",";
+
+ ///
+ /// Set if field count should be verified from the first row
+ ///
+ public bool VerifyFieldCount { get; set; } = true;
+
+ ///
+ /// Constructor for writing to a file
+ ///
+ public SeparatedValueWriter(string filename)
+ {
+ sw = new StreamWriter(filename);
+ }
+
+ ///
+ /// Consturctor for writing to a stream
+ ///
+ public SeparatedValueWriter(Stream stream, Encoding encoding)
+ {
+ sw = new StreamWriter(stream, encoding);
+ }
+
+ ///
+ /// Write a header row
+ ///
+ public void WriteHeader(string[] headers)
+ {
+ // If we haven't written anything out, we can write headers
+ if (!header && !firstRow)
+ WriteValues(headers);
+
+ header = true;
+ }
+
+ ///
+ /// Write a value row
+ ///
+ public void WriteValues(object[] values)
+ {
+ // If the writer can't be used, we error
+ if (sw == null || !sw.BaseStream.CanWrite)
+ throw new ArgumentException(nameof(sw));
+
+ // If the separator character is invalid, we error
+ if (string.IsNullOrEmpty(Separator))
+ throw new ArgumentException(nameof(Separator));
+
+ // If we have the first row, set the bool and the field count
+ if (!firstRow)
+ {
+ firstRow = true;
+ if (VerifyFieldCount && fields == -1)
+ fields = values.Length;
+ }
+
+ // Get the number of fields to write out
+ int fieldCount = values.Length;
+ if (VerifyFieldCount)
+ fieldCount = Math.Min(fieldCount, fields);
+
+ // Iterate over the fields, writing out each
+ bool firstField = true;
+ for (int i = 0; i < fieldCount; i++)
+ {
+ var value = values[i];
+
+ if (!firstField)
+ sw.Write(Separator);
+
+ if (Quotes)
+ sw.Write("\"");
+ sw.Write(value.ToString());
+ if (Quotes)
+ sw.Write("\"");
+
+ firstField = false;
+ }
+
+ // If we need to pad out the number of fields, add empties
+ if (VerifyFieldCount && values.Length < fields)
+ {
+ for (int i = 0; i < fields - values.Length; i++)
+ {
+ sw.Write(Separator);
+
+ if (Quotes)
+ sw.Write("\"\"");
+ }
+ }
+
+ // Add a newline
+ sw.WriteLine();
+
+ // Flush the buffer
+ sw.Flush();
+ }
+
+ ///
+ /// Dispose of the underlying writer
+ ///
+ public void Dispose()
+ {
+ sw.Dispose();
+ }
+ }
+}