412 lines
13 KiB
C#
412 lines
13 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace SessionZero.Data;
|
|
|
|
public class SzfParser
|
|
{
|
|
private static readonly Regex HeaderRegex = new(@"^!(\w+):\s*(.+)$", RegexOptions.Compiled);
|
|
private static readonly Regex SectionRegex = new(@"^\[([^\]]+)\]$", RegexOptions.Compiled);
|
|
private static readonly Regex FieldRegex = new(@"^(\w+)\s*\(([^)]+)\)\s*=\s*(.*)$", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// Parse a SZF file content and return the appropriate SzfObject type
|
|
/// </summary>
|
|
public T Parse<T>(string szfContent) where T : SzfObject
|
|
{
|
|
if (string.IsNullOrWhiteSpace(szfContent))
|
|
throw new SzfParseException("SZF content cannot be empty");
|
|
|
|
var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(line => line.Trim())
|
|
.Where(line => !string.IsNullOrEmpty(line))
|
|
.ToList();
|
|
|
|
var parseResult = ParseSzfStructure(lines);
|
|
|
|
// Validate that the parsed type matches the requested type
|
|
ValidateObjectType<T>(parseResult.ObjectType);
|
|
|
|
var obj = CreateInstance<T>(parseResult.ObjectType);
|
|
PopulateSzfObject(obj, parseResult);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse SZF content without specifying the exact type - auto-detects type
|
|
/// </summary>
|
|
public SzfObject Parse(string szfContent)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(szfContent))
|
|
throw new SzfParseException("SZF content cannot be empty");
|
|
|
|
var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(line => line.Trim())
|
|
.Where(line => !string.IsNullOrEmpty(line))
|
|
.ToList();
|
|
|
|
var parseResult = ParseSzfStructure(lines);
|
|
|
|
var obj = CreateInstanceFromType(parseResult.ObjectType);
|
|
PopulateSzfObject(obj, parseResult);
|
|
|
|
return obj;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an instance of the appropriate type based on SzfObjectType
|
|
/// </summary>
|
|
private T CreateInstance<T>(SzfObjectType objectType) where T : SzfObject
|
|
{
|
|
var instance = CreateInstanceFromType(objectType);
|
|
if (instance is T typedInstance)
|
|
return typedInstance;
|
|
|
|
throw new SzfParseException($"Cannot cast {instance.GetType().Name} to {typeof(T).Name}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create an instance based on the object type
|
|
/// </summary>
|
|
private SzfObject CreateInstanceFromType(SzfObjectType objectType)
|
|
{
|
|
return objectType switch
|
|
{
|
|
SzfObjectType.Dataset => new Dataset(),
|
|
// Add other types as they're implemented
|
|
// SzfObjectType.CharacterTemplate => new CharacterTemplate(),
|
|
// SzfObjectType.Character => new Character(),
|
|
// SzfObjectType.Session => new Session(),
|
|
_ => throw new SzfParseException($"Unsupported object type: {objectType}")
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse SZF content into a generic structure before type-specific processing
|
|
/// </summary>
|
|
private SzfParseResult ParseSzfStructure(List<string> lines)
|
|
{
|
|
var result = new SzfParseResult();
|
|
var currentSection = new List<string>();
|
|
var currentSectionName = string.Empty;
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
// Skip comments
|
|
if (line.StartsWith("//") || line.StartsWith("#"))
|
|
continue;
|
|
|
|
// Parse headers (!type:, !schema:)
|
|
var headerMatch = HeaderRegex.Match(line);
|
|
if (headerMatch.Success)
|
|
{
|
|
var headerName = headerMatch.Groups[1].Value.ToLower();
|
|
var headerValue = headerMatch.Groups[2].Value.Trim();
|
|
|
|
switch (headerName)
|
|
{
|
|
case "type":
|
|
result.ObjectType = ParseObjectType(headerValue);
|
|
break;
|
|
case "schema":
|
|
result.SchemaVersion = headerValue;
|
|
break;
|
|
default:
|
|
result.Headers[headerName] = headerValue;
|
|
break;
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Parse section headers [Section Name]
|
|
var sectionMatch = SectionRegex.Match(line);
|
|
if (sectionMatch.Success)
|
|
{
|
|
// Process previous section if exists
|
|
if (!string.IsNullOrEmpty(currentSectionName))
|
|
{
|
|
ProcessSection(result, currentSectionName, currentSection);
|
|
}
|
|
|
|
// Start new section
|
|
currentSectionName = sectionMatch.Groups[1].Value.Trim();
|
|
currentSection.Clear();
|
|
continue;
|
|
}
|
|
|
|
// Add line to current section
|
|
if (!string.IsNullOrEmpty(currentSectionName))
|
|
{
|
|
currentSection.Add(line);
|
|
}
|
|
}
|
|
|
|
// Process final section
|
|
if (!string.IsNullOrEmpty(currentSectionName))
|
|
{
|
|
ProcessSection(result, currentSectionName, currentSection);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process an individual section and its fields
|
|
/// </summary>
|
|
private void ProcessSection(SzfParseResult result, string sectionName, List<string> sectionLines)
|
|
{
|
|
var section = new SzfSection { Name = sectionName };
|
|
|
|
foreach (var line in sectionLines)
|
|
{
|
|
var fieldMatch = FieldRegex.Match(line);
|
|
if (fieldMatch.Success)
|
|
{
|
|
var field = new SzfField
|
|
{
|
|
Name = fieldMatch.Groups[1].Value.Trim(),
|
|
Type = fieldMatch.Groups[2].Value.Trim(),
|
|
Value = ParseFieldValue(fieldMatch.Groups[3].Value.Trim(), fieldMatch.Groups[2].Value.Trim())
|
|
};
|
|
|
|
section.Fields.Add(field);
|
|
}
|
|
}
|
|
|
|
result.Sections.Add(section);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse field value based on its declared type
|
|
/// </summary>
|
|
private object? ParseFieldValue(string valueString, string fieldType)
|
|
{
|
|
if (string.IsNullOrEmpty(valueString))
|
|
return null;
|
|
|
|
return fieldType.ToLower() switch
|
|
{
|
|
"text" => valueString,
|
|
"text-field" => valueString,
|
|
"number" => ParseNumber(valueString),
|
|
"bool" => ParseBoolean(valueString),
|
|
"calculated" => valueString, // Store formula as string
|
|
"system" => valueString,
|
|
"dataset-reference" => valueString,
|
|
"dataset-type" => valueString,
|
|
"dataset-reference-multiple" => valueString,
|
|
"dataset-type-multiple" => valueString,
|
|
"group" => ParseBoolean(valueString), // Groups are typically true/false
|
|
_ => valueString // Default to string
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse number values (int or double)
|
|
/// </summary>
|
|
private object ParseNumber(string value)
|
|
{
|
|
if (int.TryParse(value, out var intResult))
|
|
return intResult;
|
|
|
|
if (double.TryParse(value, out var doubleResult))
|
|
return doubleResult;
|
|
|
|
throw new SzfParseException($"Invalid number format: {value}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse boolean values
|
|
/// </summary>
|
|
private bool ParseBoolean(string value)
|
|
{
|
|
return value.ToLower() switch
|
|
{
|
|
"true" => true,
|
|
"false" => false,
|
|
"1" => true,
|
|
"0" => false,
|
|
_ => throw new SzfParseException($"Invalid boolean format: {value}")
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse object type from string
|
|
/// </summary>
|
|
private SzfObjectType ParseObjectType(string typeString)
|
|
{
|
|
return typeString.ToLower() switch
|
|
{
|
|
"dataset" => SzfObjectType.Dataset,
|
|
"character_template" => SzfObjectType.CharacterTemplate,
|
|
"character" => SzfObjectType.Character,
|
|
"session" => SzfObjectType.Session,
|
|
_ => throw new SzfParseException($"Unknown object type: {typeString}")
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validate that the parsed type matches the requested generic type
|
|
/// </summary>
|
|
private void ValidateObjectType<T>(SzfObjectType parsedType) where T : SzfObject
|
|
{
|
|
var expectedType = typeof(T).Name switch
|
|
{
|
|
nameof(Dataset) => SzfObjectType.Dataset,
|
|
// Add other types as they're implemented
|
|
_ => throw new SzfParseException($"Unsupported SzfObject type: {typeof(T).Name}")
|
|
};
|
|
|
|
if (parsedType != expectedType)
|
|
{
|
|
throw new SzfParseException($"Type mismatch: Expected {expectedType}, found {parsedType}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populate the SzfObject with parsed data
|
|
/// </summary>
|
|
private void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult)
|
|
{
|
|
// Set schema version
|
|
obj.SchemaVersion = parseResult.SchemaVersion;
|
|
|
|
// Find and process Metadata section
|
|
var metadataSection =
|
|
parseResult.Sections.FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase));
|
|
if (metadataSection != null)
|
|
{
|
|
var metadata = metadataSection.Fields.ToDictionary(f => f.Name, f => f);
|
|
obj.PopulateFromMetadata(metadata);
|
|
}
|
|
|
|
// Process type-specific content
|
|
ProcessTypeSpecificContent(obj, parseResult);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process type-specific content based on the object type
|
|
/// </summary>
|
|
private void ProcessTypeSpecificContent(SzfObject obj, SzfParseResult parseResult)
|
|
{
|
|
switch (obj)
|
|
{
|
|
case Dataset dataset:
|
|
ProcessDatasetContent(dataset, parseResult);
|
|
break;
|
|
// Add other types as they're implemented
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Process dataset-specific content (entries and their sections)
|
|
/// </summary>
|
|
private void ProcessDatasetContent(Dataset dataset, SzfParseResult parseResult)
|
|
{
|
|
var entrySections = parseResult.Sections
|
|
.Where(s => s.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
foreach (var entrySection in entrySections)
|
|
{
|
|
var entryName = entrySection.Name.Substring(6).Trim(); // Remove "Entry: " prefix
|
|
var entry = new DatasetEntry { Name = entryName };
|
|
|
|
// Add fields directly on the entry
|
|
foreach (var field in entrySection.Fields)
|
|
{
|
|
entry.Fields.Add(ConvertToDatasetField(field));
|
|
}
|
|
|
|
// Find subsections for this entry
|
|
var entrySubsections = parseResult.Sections
|
|
.Where(s => s.Name.StartsWith($"Entry.{entryName}.", StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
foreach (var subsection in entrySubsections)
|
|
{
|
|
var sectionName = subsection.Name.Substring($"Entry.{entryName}.".Length);
|
|
var datasetSection = new DatasetSection { Name = sectionName };
|
|
|
|
foreach (var field in subsection.Fields)
|
|
{
|
|
datasetSection.Fields.Add(ConvertToDatasetField(field));
|
|
}
|
|
|
|
entry.Sections.Add(datasetSection);
|
|
}
|
|
|
|
dataset.Entries.Add(entry);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert SzfField to DatasetField
|
|
/// </summary>
|
|
private DatasetField ConvertToDatasetField(SzfField szfField)
|
|
{
|
|
return new DatasetField
|
|
{
|
|
Name = szfField.Name,
|
|
Type = ParseDatasetFieldType(szfField.Type),
|
|
Value = szfField.Value
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse DatasetFieldType from string
|
|
/// </summary>
|
|
private DatasetFieldType ParseDatasetFieldType(string typeString)
|
|
{
|
|
return typeString.ToLower() switch
|
|
{
|
|
"text" => DatasetFieldType.Text,
|
|
"text-field" => DatasetFieldType.TextField,
|
|
"number" => DatasetFieldType.Number,
|
|
"bool" => DatasetFieldType.Boolean,
|
|
"group" => DatasetFieldType.Group,
|
|
"calculated" => DatasetFieldType.Calculated,
|
|
"system" => DatasetFieldType.System,
|
|
"dataset-reference" => DatasetFieldType.DatasetReference,
|
|
"dataset-type" => DatasetFieldType.DatasetType,
|
|
"dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple,
|
|
"dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple,
|
|
_ => DatasetFieldType.Text
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal class to hold parsed SZF structure
|
|
/// </summary>
|
|
internal class SzfParseResult
|
|
{
|
|
public SzfObjectType ObjectType { get; set; }
|
|
public string SchemaVersion { get; set; } = "1.0.0";
|
|
public Dictionary<string, string> Headers { get; set; } = new();
|
|
public List<SzfSection> Sections { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internal class representing a parsed section
|
|
/// </summary>
|
|
internal class SzfSection
|
|
{
|
|
public string Name { get; set; } = string.Empty;
|
|
public List<SzfField> Fields { get; set; } = new();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exception thrown during SZF parsing
|
|
/// </summary>
|
|
public class SzfParseException : Exception
|
|
{
|
|
public SzfParseException(string message) : base(message)
|
|
{
|
|
}
|
|
|
|
public SzfParseException(string message, Exception innerException) : base(message, innerException)
|
|
{
|
|
}
|
|
} |