using System;
using System.Collections;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Text;
namespace NWN.FileTypes
{
///
/// This class represents a 2da file. It contains all of the functionality
/// necessary to merge the 2da files.
///
public class _2DA
{
#region public properties
public const string Empty = "****";
///
/// Gets the number of rows in the 2da.
///
public int Rows { get { return rows.Count; } }
///
/// Gets the number of columns in the 2da.
///
public int Columns { get { return heading.Count; } }
///
/// Gets the heading row.
///
public string Heading
{
get { return BuildString(heading); }
}
///
/// Gets the schema for the 2da, the schema defines the columns.
///
public StringCollection Schema { get { return heading; } }
///
/// Gets/sets the index'th row of the 2da.
///
public string this[int index]
{
get { return BuildString((StringCollection) rows[index]); }
set
{
// Make sure we have a schema.
TestForSchema();
// Parse the line to get the individual cells and throw an exception if
// the row's cell count does not match our cell count.
StringCollection row = ParseLine(value, false);
if (row.Count < heading.Count)
throw new InvalidOperationException("Row does not contain enough cells");
else if (row.Count > heading.Count)
throw new InvalidOperationException("Row contains too many cells");
// Pad the 2da to make sure it has room for the row, then add it.
Pad(index + 1);
rows[index] = row;
}
}
///
/// Gets/sets a cell from the 2da.
///
public string this[int row, int column]
{
get
{
return ((StringCollection) rows[row])[column];
}
set
{
// Make sure we have a schema.
TestForSchema();
// Pad the 2da to make sure it has room for the cell, then add it.
Pad(row + 1);
((StringCollection) rows[row])[column] = value;
}
}
///
/// Gets the 2da file name w/o any path.
///
public string Name { get { return Path.GetFileName(fileName); } }
///
/// Gets the 2da name with any specified path information.
///
public string FileName { get { return fileName; } }
///
/// Gets/sets the 2da offset. All rows in the 2da have their row numbers
/// shifted to be relative to this offset, i.e. the first row has a row
/// number of offset, the second offset + 1, etc.
///
public int Offset
{
get { return offset; }
set
{
if (value == offset) return;
// Adjust all of the row numbers to
// the correct index value.
int index = value;
foreach (StringCollection row in rows)
{
row[0] = index.ToString();
index++;
}
// Save the offset.
offset = value;
}
}
#endregion
#region public methods
///
/// Default constructor
///
public _2DA()
{
heading = new StringCollection();
rows = new ArrayList();
colSizes = new int[100];
offset = 0;
fileName = string.Empty;
}
///
/// Class constructor
///
/// The schema for the 2da. The schema defines
/// the columns contained in the 2da
public _2DA(StringCollection schema) : this()
{
SetSchema(schema);
}
///
/// Class constructor
///
/// The schema for the 2da. The schema defines
/// the columns contained in the 2da
public _2DA(string[] schema) : this()
{
StringCollection schemaColl = new StringCollection();
schemaColl.AddRange(schema);
SetSchema(schemaColl);
}
///
/// Sets the schema for the 2da. Setting the schema also clears the 2da.
///
/// The schema for the 2da. The schema defines
/// the columns contained in the 2da
public void SetSchema(string schema)
{
// Setup the schema for the 2da.
heading = ParseSchema(schema);
AddRowColumnToSchema(heading);
// Changing the schema clears the 2da.
Clear();
}
///
/// Sets the schema for the 2da. Setting the schema also clears the 2da.
///
/// The schema for the 2da. The schema defines
/// the columns contained in the 2da
public void SetSchema(StringCollection schema)
{
// Setup the schema for the 2da.
heading = schema;
AddRowColumnToSchema(heading);
// Changing the schema clears the 2da.
Clear();
}
///
/// Sets the schema for the 2da. Setting the schema also clears the 2da.
///
/// The schema for the 2da. The schema defines
/// the columns contained in the 2da
public void SetSchema(string[] schema)
{
// Setup the schema for the 2da.
heading = new StringCollection();
heading.AddRange(schema);
AddRowColumnToSchema(heading);
// Changing the schema clears the 2da.
Clear();
}
///
/// Gets the index of a column given it's heading.
///
/// The heading text
/// The index of the column or -1 if it's not found
public int GetIndex(string headingText)
{
// Loop through the headings doing a case-insensetive compare of the
// passed heading text, if we find it return the index.
for (int i = 0; i < heading.Count; i++)
if (0 == string.Compare(headingText, heading[i], true, CultureInfo.InvariantCulture))
return i;
// We didn't find the heading return -1.
return -1;
}
///
/// Tests to see if the specified row is empty.
///
/// The row to test
/// true if the row is empty, false if it is not.
public bool IsEmpty (int row)
{
// Reality check on row argument.
if (row < 0 || row >= rows.Count) return false;
// Get the row and loop through all of the columns except the first
// (which is the row number) checking to see if any are not empty. As
// soon as we find a non-empty row return false.
StringCollection strings = (StringCollection) rows[row];
for (int i = 1; i < strings.Count; i++)
if (_2DA.Empty != strings[i])
{
// We need a special case here. Many 2da's use the label column
// to indicate reserved or free rows, purely as an informational
// message to someone trying to mod the 2da. If a row has data
// in the label column but no other then we want to consider the
// row empty as it has no meaningful content.
if (0 == string.Compare("label", heading[i], true, CultureInfo.InvariantCulture)) continue;
return false;
}
// All columns but the row number are empty return true.
return true;
}
///
/// Tests to see if the specified cell is empty.
///
/// The row of the cell
/// The column of the cell
/// true if the row is empty, false if it is not.
public bool IsEmpty(int row, int column)
{
return Empty == this[row, column];
}
///
/// Clears the 2da of all cell data, preserving the schema.
///
public void Clear()
{
// Reset the 2da to be empty.
offset = 0;
rows.Clear();
// Set the column widths to be the widths of the heading cells.
for (int i = 0; i < heading.Count; i++)
colSizes[i] = heading[i].Length;
}
///
/// Pads the 2da to have the specified number of rows. If the 2da already has
/// more rows than the specified number nothing is done, if it doesn't then
/// blank rows are added to the end to pad.
///
/// The new row count
public void Pad(int length)
{
// Figure out how many rows we need to pad.
int numPad = length - rows.Count;
if (numPad <= 0) return;
// Add enough empty rows to pad the 2da.
for (int i = 0; i < numPad; i++)
{
StringCollection empty = EmptyRow(rows.Count);
rows.Add(empty);
}
}
///
/// Copies a row from a source 2da to this 2da.
///
/// The source 2da
/// The index of the row in the source 2da
/// The index of the row in this 2da
public void CopyRow(_2DA source, int sourceRow, int row)
{
// Get the row from the source 2da and let our overload do all of the
// work.
StringCollection sourceRowData = (StringCollection) source.rows[sourceRow];
CopyRow(sourceRowData, row);
}
///
/// Copies a row to this 2da.
///
///
///
public void CopyRow(StringCollection sourceRow, int row)
{
// Get the target StringCollection, and determine the
// number of columns to copy being the minimum between the source
// 2da's column count and ours.
StringCollection rowData = (StringCollection) rows[row];
int columns = System.Math.Min(sourceRow.Count, heading.Count);
// Copy the row data, adjusting our column widths as necessary.
rowData[0] = row.ToString();
colSizes[0] = System.Math.Max(colSizes[0], rowData[0].Length);
for (int i = 1; i < columns; i++)
{
rowData[i] = sourceRow[i];
colSizes[i] = System.Math.Max(colSizes[i], rowData[i].Length);
}
}
///
/// Gets the StringCollection containing the row data for the
/// given row.
///
/// The row for which to get the row data
/// A StringCollection containing the row data
public StringCollection GetRowData(int row)
{
return (StringCollection) rows[row];
}
///
/// Fixes up a 2da join column by adding the given offset to all of the values
/// in the column.
///
/// The index of the column (0 biased)
/// The offset to add to the column values
public void Fixup2daColumn(int column, int offset)
{
// Loop through each row, adding offset to all of the
// values in the specified column.
foreach (StringCollection row in rows)
{
if ("****" != row[column])
{
int val = System.Int32.Parse(row[column]) + offset;
row[column] = val.ToString();
}
}
}
///
/// Fixes up a 2da join column by adding the given offset to all of the values
/// in the column.
///
/// The index of the column (0 biased)
/// The offset to add to the column values
/// If true, indicaets that the tlk offset is for a custom
/// tlk, this causes the custom tlk offset to be added to the offset as well.
public void FixupTlkColumn(int column, int offset, bool customTlk)
{
// Custom tlk's have 0x1000000 added to their
// value, i.e. entry 0 in the tlk is really
// index 0x1000000, etc.
if (customTlk) offset += 0x1000000;
Fixup2daColumn(column, offset);
}
///
/// Saves the 2da.
///
public void Save()
{
SaveAs(fileName);
}
///
/// Saves the 2da with the given file name.
///
/// The name of the 2da
public void SaveAs(string fileName)
{
using (StreamWriter writer = new StreamWriter(fileName, false, Encoding.ASCII))
{
// Write the 2da header.
writer.WriteLine(headerString);
writer.WriteLine();
writer.WriteLine(Heading);
// Write the row data.
for (int i = 0; i < rows.Count; i++)
writer.WriteLine(this[i]);
}
this.fileName = fileName;
}
#endregion
#region public static methods
///
/// Compares 2 2da cell values to see if they are the same or not.
///
/// The first value
/// The second value
/// True if the comparison should be case-insensitive
/// True if the cells are the same, false if they are different
public static bool CompareCell(string value1, string value2, bool ignoreCase)
{
return 0 == string.Compare(value1, value2, ignoreCase,
CultureInfo.InvariantCulture);
}
///
/// Compares rows in 2 different 2da files to see if they are equal or not.
///
/// The first 2da to test
/// The row in the first 2da to compare
/// The second 2da to test
/// The row in the second 2da to compare
/// True if the comparison should be case insensitive
/// True if the rows are equal false if they are not
public static bool CompareRow(_2DA twoDA1, int row1, _2DA twoDA2, int row2,
bool ignoreCase)
{
// Get the data for each of the rows.
StringCollection row1Data = (StringCollection) twoDA1.rows[row1];
StringCollection row2Data = (StringCollection) twoDA2.rows[row2];
// If the rows have different amounts of cells then they are by
// definition different.
if (row1Data.Count != row2Data.Count) return false;
// Loop through the rows doing a cell by cell compare, stopping
// if we find any differences. We start at column 1 to skip
// the row numbrs which would of course be different.
for (int i = 1; i < row1Data.Count; i++)
if (!CompareCell(row1Data[i], row2Data[i], ignoreCase))
return false;
// The rows are identical return true.
return true;
}
///
/// Factory method to create C2da objects from 2da files.
///
/// The name of the 2da file
/// A 2da object for the 2da file.
public static _2DA Load2da (string fileName)
{
// Open the 2da file.
_2DA file = new _2DA(fileName);
using(StreamReader reader = new StreamReader(fileName))
{
file.Read(reader);
}
return file;
}
///
/// Factory method to create C2da objects from streams.
///
/// The stream to create the 2da object from
/// A 2da object for the stream.
public static _2DA Load2da(Stream stream)
{
_2DA file = new _2DA();
using (StreamReader reader = new StreamReader(stream, Encoding.ASCII))
{
file.Read(reader);
}
return file;
}
///
/// Merges 2 2da objects, saving the results in a 2da file. The method expects that
/// the source 2da will have at least enough rows to be contiguous with the merge
/// 2da (the source 2da should be padded by calling Pad() if necessary). If the
/// source and merge 2da's share some rows, the merge 2da rows will overwrite the
/// source 2da rows.
///
/// The source 2da
/// The merge 2da
/// The name of the output 2da
public static void Merge2da (_2DA source, _2DA merge, string outFile)
{
using(StreamWriter writer = new StreamWriter(outFile, false))
{
// Write the 2da header.
writer.WriteLine(headerString);
writer.WriteLine();
writer.WriteLine(source.Heading);
// Make the column sizes in the source and 2da files to be the largest
// of each file, to make the columns have the correct width.
for (int i = 0; i < source.colSizes.Length; i++)
{
source.colSizes[i] = System.Math.Max(source.colSizes[i], merge.colSizes[i]);
merge.colSizes[i] = source.colSizes[i];
}
// output all of the source strings before our merge.
for (int i = 0; i < merge.Offset; i++)
{
string s = source[i];
writer.WriteLine(s);
}
// Test all of the rows that the merge is about to overwrite to make sure they
// are really empty. If any are not then generate a warning message for those
// rows.
int end = System.Math.Min(source.rows.Count, merge.Offset + merge.rows.Count);
for (int i = merge.Offset; i < end; i++)
if (!source.IsEmpty(i))
{
//CMain.Warning("Overwriting non-empty row {0} in {1}", i, source.FileName);
}
// output all of the merge strings.
for (int i = 0; i < merge.rows.Count; i++)
{
string s = merge[i];
writer.WriteLine(s);
}
// output any remaining source strings, in case the merge is in the middle.
for (int i = merge.Offset + merge.rows.Count; i < source.rows.Count; i++)
{
string s = source[i];
writer.WriteLine(s);
}
writer.Flush();
writer.Close();
}
}
#endregion
#region private fields/properties/methods
private const string headerString = "2DA V2.0";
private StringCollection heading;
private ArrayList rows;
private int[] colSizes;
private int offset;
private string fileName;
///
/// Private constructor, to create objects use the static factory method
///
/// The name of the 2da file
private _2DA(string fileName) : this()
{
// Save the name of the 2da file.
this.fileName = fileName;
}
///
/// Checks to make sure that a schema has been defined for the 2da, and throws
/// an InvalidOperationException if it doesn't.
///
private void TestForSchema()
{
// If we have no heading row we have no schema throw an exception.
if (0 == heading.Count) throw new InvalidOperationException("2da contains no schema.");
}
///
/// Creates an empty row for the 2da file.
///
/// The index of the row which is being created
/// The empty row
private StringCollection EmptyRow(int row)
{
// Create an empty row, and assign it a row number.
StringCollection empty = new StringCollection();
empty.Add(row.ToString());
// Adjust the maximum width of our column if it changed.
colSizes[0] = System.Math.Max(colSizes[0], empty[0].Length);
// Fill all other columns with the empty value.
int count = heading.Count;
for (int i = 1; i < count; i++)
{
empty.Add(_2DA.Empty);
colSizes[i] = System.Math.Max(colSizes[i], _2DA.Empty.Length);
}
// Return the empty row.
return empty;
}
///
/// Builds a string from the data for the row. The string has padding whitespace
/// inserted such that all of the columns in the 2da will line up of the lines are
/// output to a text file.
///
/// The row for which to build the string
///
private string BuildString(StringCollection row)
{
System.Text.StringBuilder b = new System.Text.StringBuilder(4096);
int i = 0;
foreach (string s in row)
{
// If this is not the first row add a space separator.
if (i > 0) b.Append(' ');
// If the string contains spaces then wrap it in quotes.
string value = s;
if (value.IndexOf(' ') >= 0) value = string.Format("\"{0}\"", value);
// Add the string data and any whitespace padding necessary.
b.Append(value);
if (value.Length < colSizes[i]) b.Append(' ', colSizes[i] - value.Length);
i++;
}
return b.ToString();
}
///
/// Adds the empty column for the row numbers to a schema
///
/// The schema to add the line to
private void AddRowColumnToSchema(StringCollection schema)
{
// If the first entry is not blank then we need to add an empty
// column for the row number to the schema.
if (string.Empty != schema[0]) schema.Insert(0, string.Empty);
}
///
/// Builds the schema for the 2da by parsing the text line.
///
/// The line to parse
/// A string collection containing the schema
private StringCollection ParseSchema(string line)
{
// Call ParseLine() to parse the schema line into the individual cell values,
// then add an extra blank entry for the row number.
StringCollection schema = ParseLine(line, true);
AddRowColumnToSchema(schema);
return schema;
}
///
/// Parses a 2da file line to break the line into each of the column values.
/// Parses both the heading line (which contains no row number) and row data
/// lines (which do).
///
/// The line to parse
/// True if the line is a header line
/// A StringCollection containing the line's data
private StringCollection ParseLine(string line, bool headerLine)
{
StringCollection coll = new StringCollection();
// Determine the start index for the column sizes, if it's a header
// line we are parsing then the start index is 1 otherwise it's 0
int iColSize = headerLine ? 1 : 0;
try
{
for (int i= 0;;)
{
// Skip whitespace, if we skip past the end of the string then an
// index out of range exception will be thrown which we catch
// to end the parse.
while (' ' == line[i] || '\t' == line[i]) i++;
// OK, hack time. Some 2da's (itemprops.2da is the known case have
// a "Label" column as the last column. In this case the "label"
// IGNORES the rules about quoting spaces and allows spaces in the name,
// we have to catch this case by checking the schema to see if we are
// on the last column and it is called "Label".
bool isLabelColumn = !headerLine &&
coll.Count == heading.Count - 1 &&
0 == string.Compare(heading[coll.Count], "LABEL", true, CultureInfo.InvariantCulture);
// items in a 2da are separated by whitespace, unless quoted.
if (isLabelColumn)
{
// If we are reading the name column just grab the rest of the text to the
// end of the line and remove trailing whitespace.
string s = line.Substring(i);
s = s.Trim();
coll.Add(s);
i = line.Length;
}
else if ('"' == line[i])
{
// Find the end quote then add the substring.
int iEndQuote = line.IndexOf('"', i + 1);
string s = line.Substring(i + 1, iEndQuote - i - 1);
coll.Add (s);
// If the string we just added is the widest string for this column we've
// seen then save that info.
colSizes[iColSize] = System.Math.Max(colSizes[iColSize], s.Length);
iColSize++;
// Advance i past what we just added.
i = iEndQuote + 1;
}
else
{
// Find the next whitespace char and add the substring to the collection.
int iFirstWhitespace = line.IndexOfAny(new char[]{' ', '\t'}, i);
if (-1 == iFirstWhitespace) iFirstWhitespace = line.Length;
string s = line.Substring(i, iFirstWhitespace - i);
coll.Add(s);
// If the string we just added is the widest string for this column we've
// seen then save that info.
colSizes[iColSize] = System.Math.Max(colSizes[iColSize], s.Length);
iColSize++;
// Advance i past what we just added.
i = iFirstWhitespace;
}
}
}
catch (System.IndexOutOfRangeException)
{
// We use this exception as the terminating condition of the loop, so no
// error.
}
return coll;
}
///
/// Reads 2da data from the specified stream, initializing the object with
/// the 2da data.
///
/// The reader from which to read the data
private void Read(StreamReader reader)
{
// 2da files have the header followed by a line of white space.
// Consume that now.
reader.ReadLine();
reader.ReadLine();
// Read the header into memory and add an extra
// Now read the header and all of the data into memory.
for (bool fHeader = true; reader.Peek() > -1; fHeader = false)
{
string line = reader.ReadLine();
line = line.Trim();
if (0 == line.Length) continue;
// Parse the line into the collection of individual strings. If
// we have just read in the header row we don't have a heading
// for the row number, so add a dummy column to the front of
// the array so the header and data rows have the same number of
// columns.
StringCollection strings = fHeader ?
ParseSchema(line) : ParseLine(line, fHeader);
if (fHeader)
heading = strings;
else
{
// Do a reality check on the column count.
// Commented out until the CEP team fixes their fucked up 2da.
/*
if (strings.Count != Columns)
throw new InvalidOperationException(
string.Format("Row {0} in {1} does not have the correct number of columns",
rows.Count, Name));
*/
rows.Add(strings);
}
}
}
#endregion
}
}