Created character szf object. More seperation by removing the enum from ZsfObject and using reflection for szf object types.
This commit is contained in:
parent
678c10a872
commit
858a30d531
254
SessionZero/Data/Character.cs
Normal file
254
SessionZero/Data/Character.cs
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
|
// Helper classes to represent the structured character data
|
||||||
|
public class CharacterField
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Type { get; set; } = string.Empty; // Store as string for writing (e.g., "text", "number")
|
||||||
|
public object? Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CharacterSection
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public List<CharacterField> Fields { get; set; } = new();
|
||||||
|
public List<CharacterSection> Subsections { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SzfObject("character")]
|
||||||
|
public class Character : SzfObject
|
||||||
|
{
|
||||||
|
public Guid TemplateGuid { get; private set; }
|
||||||
|
public Version TemplateVersion { get; private set; } = new Version(1, 0, 0);
|
||||||
|
|
||||||
|
// This will hold the actual character data organized in sections,
|
||||||
|
// using our custom hierarchical CharacterSection objects.
|
||||||
|
public List<CharacterSection> CharacterSections { get; private set; } = new List<CharacterSection>();
|
||||||
|
|
||||||
|
public Character()
|
||||||
|
{
|
||||||
|
// Default constructor for deserialization/initialization.
|
||||||
|
// Base SzfObject constructor handles its own Metadata initialization.
|
||||||
|
}
|
||||||
|
|
||||||
|
public Character(
|
||||||
|
Guid templateGuid,
|
||||||
|
Version templateVersion,
|
||||||
|
string name,
|
||||||
|
string version,
|
||||||
|
string? description = null,
|
||||||
|
string? author = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
TemplateGuid = templateGuid;
|
||||||
|
TemplateVersion = templateVersion;
|
||||||
|
// Do NOT directly set Metadata here. The SzfObject base class manages its own Metadata.
|
||||||
|
// If constructing a new object, you would typically set its Name, Version, etc.
|
||||||
|
// via the SzfObject's own properties or methods after construction, or through a
|
||||||
|
// dedicated constructor in SzfObject that passes to its Metadata.
|
||||||
|
// For existing SzfObjects, PopulateFromMetadata handles reading.
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
||||||
|
{
|
||||||
|
// Call the base method to populate common SzfObject metadata (Name, Version, Description, Author)
|
||||||
|
base.PopulateFromMetadata(metadata);
|
||||||
|
|
||||||
|
// Populate Character-specific metadata from the "Metadata" section
|
||||||
|
if (metadata.TryGetValue("SourceTemplateGuid", out var guidField))
|
||||||
|
{
|
||||||
|
if (Guid.TryParse(guidField.Value?.ToString(), out var guid))
|
||||||
|
{
|
||||||
|
TemplateGuid = guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.TryGetValue("SourceTemplateVersion", out var versionField))
|
||||||
|
{
|
||||||
|
// Ensure the value is converted to a string and explicitly call System.Version.TryParse
|
||||||
|
if (System.Version.TryParse(Convert.ToString(versionField.Value), out var ver))
|
||||||
|
{
|
||||||
|
TemplateVersion = ver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ParseSections(List<SzfSection> sections)
|
||||||
|
{
|
||||||
|
CharacterSections.Clear(); // Clear existing sections to ensure a fresh parse
|
||||||
|
|
||||||
|
// A temporary list to hold top-level CharacterSections as we parse
|
||||||
|
var tempTopLevelCharacterSections = new List<CharacterSection>();
|
||||||
|
|
||||||
|
// A map to quickly find parent sections by their name (simplified for lookup)
|
||||||
|
// e.g., "Character Information" -> CharacterSection object
|
||||||
|
var sectionNameMap = new Dictionary<string, CharacterSection>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var szfSection in sections)
|
||||||
|
{
|
||||||
|
if (szfSection.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Metadata is handled by PopulateFromMetadata, so skip here
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// This is a top-level section (e.g., [Section: Character Information])
|
||||||
|
var sectionName = szfSection.Name.Replace("Section:", "").Trim();
|
||||||
|
var characterSection = new CharacterSection { Name = sectionName };
|
||||||
|
|
||||||
|
foreach (var field in szfSection.Fields)
|
||||||
|
{
|
||||||
|
characterSection.Fields.Add(new CharacterField
|
||||||
|
{
|
||||||
|
Name = field.Name,
|
||||||
|
Type = field.Type, // SzfField.Type is expected to be a string (e.g., "text", "number")
|
||||||
|
Value = field.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tempTopLevelCharacterSections.Add(characterSection);
|
||||||
|
sectionNameMap[sectionName] = characterSection; // Store by its simplified name for easy lookup
|
||||||
|
}
|
||||||
|
else if (szfSection.Name.StartsWith("Section.", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// This is a subsection (e.g., [Section.Ability Scores.Modifiers])
|
||||||
|
var parts = szfSection.Name.Split('.');
|
||||||
|
if (parts.Length < 2) continue; // Malformed section name
|
||||||
|
|
||||||
|
// The path to the immediate parent (e.g., "Ability Scores" for "Ability Scores.Modifiers")
|
||||||
|
var parentPathSegments = new List<string>();
|
||||||
|
// Start from the first segment after "Section." and go up to the segment before the current section's name
|
||||||
|
for (int i = 1; i < parts.Length - 1; i++)
|
||||||
|
{
|
||||||
|
parentPathSegments.Add(parts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var parentLookupKey = string.Join(".", parentPathSegments);
|
||||||
|
|
||||||
|
// Find the direct parent section within the already processed hierarchy
|
||||||
|
CharacterSection? parentSection =
|
||||||
|
FindCharacterSectionByPath(tempTopLevelCharacterSections, parentLookupKey);
|
||||||
|
|
||||||
|
if (parentSection != null)
|
||||||
|
{
|
||||||
|
var subSection = new CharacterSection
|
||||||
|
{
|
||||||
|
Name = parts.Last().Trim() // The last part of the name is the subsection's own name
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var field in szfSection.Fields)
|
||||||
|
{
|
||||||
|
subSection.Fields.Add(new CharacterField
|
||||||
|
{
|
||||||
|
Name = field.Name,
|
||||||
|
Type = field.Type,
|
||||||
|
Value = field.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parentSection.Subsections.Add(subSection);
|
||||||
|
}
|
||||||
|
// If parent not found, it implies an issue with SZF structure or order.
|
||||||
|
// For now, we silently skip it. In a robust system, you might log an error.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle any other root-level sections that are not "Metadata"
|
||||||
|
// and don't conform to the "Section:..." or "Section...." patterns.
|
||||||
|
// These are treated as flat character sections.
|
||||||
|
var characterSection = new CharacterSection { Name = szfSection.Name.Trim() };
|
||||||
|
foreach (var field in szfSection.Fields)
|
||||||
|
{
|
||||||
|
characterSection.Fields.Add(new CharacterField
|
||||||
|
{
|
||||||
|
Name = field.Name,
|
||||||
|
Type = field.Type,
|
||||||
|
Value = field.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
tempTopLevelCharacterSections.Add(characterSection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterSections.AddRange(tempTopLevelCharacterSections);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to find a section by its hierarchical path (e.g., "ParentName.SubName")
|
||||||
|
private CharacterSection? FindCharacterSectionByPath(List<CharacterSection> sections, string path)
|
||||||
|
{
|
||||||
|
var segments = path.Split('.');
|
||||||
|
CharacterSection? currentSection = null;
|
||||||
|
List<CharacterSection> currentSearchList = sections;
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
currentSection =
|
||||||
|
currentSearchList.FirstOrDefault(s => s.Name.Equals(segment, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (currentSection == null) return null; // Segment not found in the current level
|
||||||
|
|
||||||
|
currentSearchList = currentSection.Subsections; // Move to subsections for the next segment
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentSection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void GenerateMetadata(SzfFieldWriter writer)
|
||||||
|
{
|
||||||
|
// Ensure common SzfObject metadata (Name, Version, Description, Author) is written first
|
||||||
|
base.GenerateMetadata(writer);
|
||||||
|
|
||||||
|
// Write Character-specific metadata fields
|
||||||
|
writer.WriteField("SourceTemplateGuid", "text", TemplateGuid.ToString());
|
||||||
|
writer.WriteField("SourceTemplateVersion", "text", TemplateVersion.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void GenerateContent(SzfFieldWriter writer)
|
||||||
|
{
|
||||||
|
// Generate all the character data sections
|
||||||
|
foreach (var section in CharacterSections)
|
||||||
|
{
|
||||||
|
// For top-level sections, the prefix is "Section". This will result in [Section: Name]
|
||||||
|
GenerateCharacterSection(writer, section, "Section");
|
||||||
|
writer.AppendLine(); // Add a blank line after each top-level section for readability
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateCharacterSection(SzfFieldWriter writer, CharacterSection section,
|
||||||
|
string parentSectionHeaderPrefix)
|
||||||
|
{
|
||||||
|
// Construct the full section header string based on the current nesting level
|
||||||
|
string currentSectionHeader;
|
||||||
|
if (parentSectionHeaderPrefix == "Section")
|
||||||
|
{
|
||||||
|
// Top-level section, e.g., [Section: Character Information]
|
||||||
|
currentSectionHeader = $"Section: {section.Name}";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Nested section, e.g., [Section.Ability Scores.Modifiers]
|
||||||
|
currentSectionHeader = $"{parentSectionHeaderPrefix}.{section.Name}";
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.AppendSectionHeader(currentSectionHeader);
|
||||||
|
|
||||||
|
// Write all fields within the current section
|
||||||
|
foreach (var field in section.Fields)
|
||||||
|
{
|
||||||
|
// Use the string representation of the type as required by SzfFieldWriter
|
||||||
|
writer.WriteField(field.Name, field.Type, field.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively generate all subsections
|
||||||
|
foreach (var subSection in section.Subsections)
|
||||||
|
{
|
||||||
|
GenerateCharacterSection(writer, subSection, currentSectionHeader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,13 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
[SzfObject(SzfObjectType.CharacterTemplate, "character_template")]
|
[SzfObject("character_template")]
|
||||||
public class CharacterTemplate : SzfObject
|
public class CharacterTemplate : SzfObject
|
||||||
{
|
{
|
||||||
public string GameSystem { get; set; } = string.Empty;
|
public string GameSystem { get; set; } = string.Empty;
|
||||||
public List<RequiredDatasetReference> RequiredDatasets { get; set; } = new();
|
public List<RequiredDatasetReference> RequiredDatasets { get; set; } = new();
|
||||||
public List<TemplateSection> Sections { get; set; } = new();
|
public List<TemplateSection> Sections { get; set; } = new();
|
||||||
|
|
||||||
public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate;
|
|
||||||
|
|
||||||
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
||||||
{
|
{
|
||||||
base.PopulateFromMetadata(metadata);
|
base.PopulateFromMetadata(metadata);
|
||||||
|
@ -4,7 +4,7 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
[SzfObject(SzfObjectType.Dataset, "dataset")]
|
[SzfObject("dataset")]
|
||||||
public class Dataset : SzfObject
|
public class Dataset : SzfObject
|
||||||
{
|
{
|
||||||
public string DatasetType { get; set; } = string.Empty;
|
public string DatasetType { get; set; } = string.Empty;
|
||||||
@ -12,7 +12,6 @@ public class Dataset : SzfObject
|
|||||||
public List<DatasetEntry> Entries { get; set; } = new();
|
public List<DatasetEntry> Entries { get; set; } = new();
|
||||||
public Dictionary<string, object> Metadata { get; set; } = new(); // To store other metadata fields
|
public Dictionary<string, object> Metadata { get; set; } = new(); // To store other metadata fields
|
||||||
|
|
||||||
public override SzfObjectType ObjectType => SzfObjectType.Dataset;
|
|
||||||
|
|
||||||
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System; // Required for Exception
|
||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
@ -23,11 +24,11 @@ public class SzfGenerator
|
|||||||
// Generate header
|
// Generate header
|
||||||
GenerateHeader(writer, obj);
|
GenerateHeader(writer, obj);
|
||||||
|
|
||||||
// Generate metadata section
|
// Generate type-specific metadata and content
|
||||||
GenerateMetadataSection(writer, obj);
|
// These methods delegate to the SzfObject itself,
|
||||||
|
// which now correctly accesses its own Metadata.
|
||||||
// Generate type-specific content
|
obj.GenerateMetadata(writer); // This now handles writing the standard metadata section
|
||||||
GenerateTypeSpecificContent(writer, obj);
|
obj.GenerateContent(writer); // This handles writing object-specific content sections
|
||||||
|
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
@ -37,47 +38,12 @@ public class SzfGenerator
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void GenerateHeader(SzfFieldWriter writer, SzfObject obj)
|
private void GenerateHeader(SzfFieldWriter writer, SzfObject obj)
|
||||||
{
|
{
|
||||||
var type = obj.GetType();
|
// TypeIdentifier is now directly accessible from the SzfObject base class
|
||||||
var attribute = type.GetCustomAttribute<SzfObjectAttribute>();
|
writer.Builder.AppendLine($"!type: {obj.TypeIdentifier}");
|
||||||
|
// Schema version is a fixed format version, not object-specific metadata
|
||||||
if (attribute == null)
|
writer.Builder.AppendLine($"!schema: 1.0.0"); // Assuming current schema version is 1.0.0
|
||||||
{
|
|
||||||
throw new SzfGenerateException(
|
|
||||||
$"Type {type.Name} does not have SzfObjectAttribute, cannot determine type string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Builder.AppendLine($"!type: {attribute.TypeString}");
|
|
||||||
writer.Builder.AppendLine($"!schema: {obj.SchemaVersion}");
|
|
||||||
writer.Builder.AppendLine();
|
writer.Builder.AppendLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate the common metadata section
|
|
||||||
/// </summary>
|
|
||||||
private void GenerateMetadataSection(SzfFieldWriter writer, SzfObject obj)
|
|
||||||
{
|
|
||||||
writer.AppendSectionHeader("Metadata");
|
|
||||||
|
|
||||||
// Standard metadata fields
|
|
||||||
writer.WriteField("Name", "text", obj.Name);
|
|
||||||
writer.WriteField("Version", "text", obj.Version);
|
|
||||||
writer.WriteField("GUID", "text", obj.Id.ToString());
|
|
||||||
writer.WriteField("Description", "text-field", obj.Description);
|
|
||||||
writer.WriteField("Author", "text", obj.Author);
|
|
||||||
|
|
||||||
// Type-specific metadata delegated to the object itself
|
|
||||||
obj.GenerateMetadata(writer);
|
|
||||||
|
|
||||||
writer.AppendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate type-specific content sections delegated to the object itself
|
|
||||||
/// </summary>
|
|
||||||
private void GenerateTypeSpecificContent(SzfFieldWriter writer, SzfObject obj)
|
|
||||||
{
|
|
||||||
obj.GenerateContent(writer);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -2,123 +2,142 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents common metadata for all SzfObjects.
|
||||||
|
/// </summary>
|
||||||
|
public class SzfMetadata
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public Version? Version { get; set; }
|
||||||
|
public Guid Guid { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Author { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class SzfObject
|
public abstract class SzfObject
|
||||||
{
|
{
|
||||||
public Guid Id { get; set; } = Guid.NewGuid();
|
/// <summary>
|
||||||
public string Name { get; set; } = string.Empty;
|
/// The unique string identifier for this SzfObject type, derived from its SzfObjectAttribute.
|
||||||
public string Version { get; set; } = "1.0.0";
|
/// </summary>
|
||||||
public string Description { get; set; } = string.Empty;
|
public string TypeIdentifier { get; }
|
||||||
public string Author { get; set; } = string.Empty;
|
|
||||||
public DateTime CreatedDate { get; set; } = DateTime.UtcNow;
|
|
||||||
public DateTime ModifiedDate { get; set; } = DateTime.UtcNow;
|
|
||||||
|
|
||||||
public string SchemaVersion { get; set; } = "1.0.0";
|
|
||||||
public Dictionary<string, object> ExtendedMetadata { get; set; } = new();
|
|
||||||
|
|
||||||
public abstract SzfObjectType ObjectType { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Abstract method for SzfObjects to parse their specific sections from the raw parsed structure.
|
/// Common metadata for all SzfObjects.
|
||||||
/// This promotes separation of concerns by moving type-specific parsing logic into the SzfObject itself.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="sections">A list of all parsed sections from the SZF file.</param>
|
public SzfMetadata Metadata { get; set; } = new SzfMetadata();
|
||||||
public abstract void ParseSections(List<SzfSection> sections);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Virtual method for SzfObjects to generate their specific metadata fields.
|
/// Default constructor that reads the TypeIdentifier from the SzfObjectAttribute.
|
||||||
/// Override this in derived classes to add custom metadata to the [Metadata] section.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The SzfFieldWriter instance to write fields.</param>
|
protected SzfObject()
|
||||||
public virtual void GenerateMetadata(SzfFieldWriter writer)
|
|
||||||
{
|
{
|
||||||
// Default implementation does nothing, derived classes can override.
|
// Read the SzfObjectAttribute from the concrete class type
|
||||||
|
var attribute = GetType().GetCustomAttribute<SzfObjectAttribute>();
|
||||||
|
if (attribute == null)
|
||||||
|
{
|
||||||
|
// This indicates a missing attribute, which means the class is not properly defined as an SzfObject.
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"SzfObject class '{GetType().Name}' is missing the SzfObjectAttribute. All concrete SzfObject types must have this attribute.");
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeIdentifier = attribute.TypeIdentifier;
|
||||||
|
|
||||||
|
// Initialize metadata with a new GUID, this is a default, can be overridden by parsing
|
||||||
|
Metadata.Guid = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Virtual method for SzfObjects to generate their specific content sections.
|
/// Populates the object's metadata from a dictionary of parsed SzfFields.
|
||||||
/// Override this in derived classes to add custom sections and fields.
|
/// This method is intended to be overridden by derived classes to handle their specific metadata.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The SzfFieldWriter instance to write fields and manage sections.</param>
|
/// <param name="metadata">A dictionary of metadata fields.</param>
|
||||||
public virtual void GenerateContent(SzfFieldWriter writer)
|
|
||||||
{
|
|
||||||
// Default implementation does nothing, derived classes can override.
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual SzfValidationResult Validate()
|
|
||||||
{
|
|
||||||
var result = new SzfValidationResult { IsValid = true };
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(Name))
|
|
||||||
result.AddError("Name is required");
|
|
||||||
|
|
||||||
if (Id == Guid.Empty)
|
|
||||||
result.AddError("Valid GUID is required");
|
|
||||||
|
|
||||||
if (!IsValidSemanticVersion(Version))
|
|
||||||
result.AddError($"Invalid version format: {Version}");
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse SZF content and auto-detect the type
|
|
||||||
/// </summary>
|
|
||||||
public static SzfObject ParseFromSzf(string szfContent)
|
|
||||||
{
|
|
||||||
var parser = new SzfParser();
|
|
||||||
return parser.Parse(szfContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse SZF content into a specific type
|
|
||||||
/// </summary>
|
|
||||||
public static T ParseFromSzf<T>(string szfContent) where T : SzfObject
|
|
||||||
{
|
|
||||||
var parser = new SzfParser();
|
|
||||||
return parser.Parse<T>(szfContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual string ToSzfString()
|
|
||||||
{
|
|
||||||
var generator = new SzfGenerator();
|
|
||||||
return generator.Generate(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
public virtual void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
|
||||||
{
|
{
|
||||||
if (metadata.TryGetValue("Name", out var nameField))
|
if (metadata.TryGetValue("Name", out var nameField))
|
||||||
Name = nameField.Value?.ToString() ?? string.Empty;
|
Metadata.Name = nameField.Value?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
if (metadata.TryGetValue("Version", out var versionField))
|
if (metadata.TryGetValue("Version", out var versionField))
|
||||||
Version = versionField.Value?.ToString() ?? "1.0.0";
|
{
|
||||||
|
if (Version.TryParse(versionField.Value?.ToString(), out var ver))
|
||||||
|
{
|
||||||
|
Metadata.Version = ver;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (metadata.TryGetValue("GUID", out var guidField) &&
|
if (metadata.TryGetValue("GUID", out var guidField))
|
||||||
Guid.TryParse(guidField.Value?.ToString(), out var guid))
|
{
|
||||||
Id = guid;
|
if (Guid.TryParse(guidField.Value?.ToString() ?? string.Empty, out var guid))
|
||||||
|
{
|
||||||
|
Metadata.Guid = guid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata.TryGetValue("Description", out var descriptionField))
|
||||||
|
Metadata.Description = descriptionField.Value?.ToString();
|
||||||
|
|
||||||
if (metadata.TryGetValue("Author", out var authorField))
|
if (metadata.TryGetValue("Author", out var authorField))
|
||||||
Author = authorField.Value?.ToString() ?? string.Empty;
|
Metadata.Author = authorField.Value?.ToString();
|
||||||
|
|
||||||
if (metadata.TryGetValue("Description", out var descField))
|
|
||||||
Description = descField.Value?.ToString() ?? string.Empty;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidSemanticVersion(string version)
|
/// <summary>
|
||||||
|
/// Parses the sections of the Szf object from a flat list of SzfSection objects
|
||||||
|
/// and constructs the object's internal data structure. This is an abstract method
|
||||||
|
/// that must be implemented by derived classes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sections">A list of parsed SzfSections.</param>
|
||||||
|
public abstract void ParseSections(List<SzfSection> sections);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the metadata section of the Szf file using the provided writer.
|
||||||
|
/// This method is virtual and can be extended by derived classes to add their specific metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">The SzfFieldWriter to use for writing.</param>
|
||||||
|
public virtual void GenerateMetadata(SzfFieldWriter writer)
|
||||||
{
|
{
|
||||||
return System.Text.RegularExpressions.Regex.IsMatch(
|
// Write standard metadata fields common to all SzfObjects
|
||||||
version, @"^\d+\.\d+\.\d+$");
|
writer.AppendSectionHeader("Metadata"); // Corrected method call
|
||||||
}
|
writer.WriteField("Name", "text", Metadata.Name);
|
||||||
}
|
writer.WriteField("Version", "text",
|
||||||
|
Metadata.Version?.ToString() ?? string.Empty); // Ensure Version is not null
|
||||||
|
writer.WriteField("GUID", "text", Metadata.Guid.ToString());
|
||||||
|
|
||||||
public enum SzfObjectType
|
if (!string.IsNullOrEmpty(Metadata.Description))
|
||||||
{
|
writer.WriteField("Description", "text-field", Metadata.Description);
|
||||||
Dataset,
|
if (!string.IsNullOrEmpty(Metadata.Author))
|
||||||
CharacterTemplate,
|
writer.WriteField("Author", "text", Metadata.Author);
|
||||||
Character,
|
|
||||||
Session
|
writer.AppendLine(); // Corrected method call to end section with a newline
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the content sections of the Szf file using the provided writer.
|
||||||
|
/// This is an abstract method that must be implemented by derived classes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="writer">The SzfFieldWriter to use for writing.</param>
|
||||||
|
public abstract void GenerateContent(SzfFieldWriter writer);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the current state of the Szf object.
|
||||||
|
/// This method is virtual and can be extended by derived classes to add their specific validation rules.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A SzfValidationResult indicating any errors or warnings.</returns>
|
||||||
|
public virtual SzfValidationResult Validate()
|
||||||
|
{
|
||||||
|
var result = new SzfValidationResult();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(Metadata.Name))
|
||||||
|
result.AddError("Name is required for SzfObject");
|
||||||
|
if (Metadata.Version == null)
|
||||||
|
result.AddError("Version is required for SzfObject");
|
||||||
|
if (Metadata.Guid == Guid.Empty)
|
||||||
|
result.AddError("GUID is required for SzfObject");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SzfValidationResult
|
public class SzfValidationResult
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
// SzfObjectAttribute.cs
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||||
public class SzfObjectAttribute : Attribute
|
public sealed class SzfObjectAttribute : Attribute
|
||||||
{
|
{
|
||||||
public SzfObjectType ObjectType { get; }
|
/// <summary>
|
||||||
public string TypeString { get; }
|
/// The unique string identifier for the SzfObject type (e.g., "dataset", "character_template", "character").
|
||||||
|
/// This is used in the `!type:` header of .szf files.
|
||||||
|
/// </summary>
|
||||||
|
public string TypeIdentifier { get; }
|
||||||
|
|
||||||
public SzfObjectAttribute(SzfObjectType objectType, string typeString)
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="SzfObjectAttribute"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="typeIdentifier">The unique string identifier for the SzfObject type.</param>
|
||||||
|
public SzfObjectAttribute(string typeIdentifier)
|
||||||
{
|
{
|
||||||
ObjectType = objectType;
|
if (string.IsNullOrWhiteSpace(typeIdentifier))
|
||||||
TypeString = typeString;
|
{
|
||||||
|
throw new ArgumentException("Type identifier cannot be null or whitespace.", nameof(typeIdentifier));
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeIdentifier = typeIdentifier;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,336 +1,350 @@
|
|||||||
using System.Reflection;
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection; // Required for Reflection
|
||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
public class SzfParser
|
public class SzfParser
|
||||||
{
|
{
|
||||||
private static readonly Regex HeaderRegex = new(@"^!(\w+):\s*(.+)$", RegexOptions.Compiled);
|
// A static map to cache SzfObject types by their TypeIdentifier
|
||||||
private static readonly Regex SectionRegex = new(@"^\[([^\]]+)\]$", RegexOptions.Compiled);
|
private static readonly Dictionary<string, Type> _szfObjectTypeMap =
|
||||||
private static readonly Regex FieldRegex = new(@"^(\w+)\s*\(([^)]+)\)\s*=\s*(.*)$", RegexOptions.Compiled);
|
new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
// Static dictionaries to store type mappings, initialized once
|
private static readonly object _typeMapLock = new object(); // For thread safety during map population
|
||||||
private static readonly Dictionary<SzfObjectType, Type> s_objectTypeToClassMap;
|
|
||||||
private static readonly Dictionary<string, SzfObjectType> s_stringToObjectTypeMap;
|
|
||||||
|
|
||||||
static SzfParser()
|
public SzfParser()
|
||||||
{
|
{
|
||||||
s_objectTypeToClassMap = new Dictionary<SzfObjectType, Type>();
|
// Ensure the type map is populated when a parser instance is created
|
||||||
s_stringToObjectTypeMap = new Dictionary<string, SzfObjectType>();
|
EnsureTypeMapPopulated();
|
||||||
|
}
|
||||||
|
|
||||||
// Discover types with SzfObjectAttribute in the current assembly
|
/// <summary>
|
||||||
var szfObjectTypes = Assembly.GetExecutingAssembly().GetTypes()
|
/// Ensures the internal map of SzfObject types and their string identifiers is populated.
|
||||||
.Where(type => typeof(SzfObject).IsAssignableFrom(type) && !type.IsAbstract)
|
/// This uses reflection to find all concrete SzfObject classes with the SzfObjectAttribute.
|
||||||
.Select(type => new
|
/// </summary>
|
||||||
|
private static void EnsureTypeMapPopulated()
|
||||||
{
|
{
|
||||||
Type = type,
|
if (_szfObjectTypeMap.Any()) return; // Already populated
|
||||||
Attribute = type.GetCustomAttribute<SzfObjectAttribute>()
|
|
||||||
})
|
|
||||||
.Where(x => x.Attribute != null);
|
|
||||||
|
|
||||||
foreach (var item in szfObjectTypes)
|
lock (_typeMapLock)
|
||||||
{
|
{
|
||||||
s_objectTypeToClassMap[item.Attribute!.ObjectType] = item.Type;
|
if (_szfObjectTypeMap.Any()) return; // Double-check after acquiring lock
|
||||||
s_stringToObjectTypeMap[item.Attribute.TypeString.ToLower()] = item.Attribute.ObjectType;
|
|
||||||
|
// Find all concrete (non-abstract) types that inherit from SzfObject
|
||||||
|
// and have the SzfObjectAttribute defined.
|
||||||
|
var szfObjectTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(s => s.GetTypes())
|
||||||
|
.Where(p => typeof(SzfObject).IsAssignableFrom(p) && !p.IsAbstract &&
|
||||||
|
p.IsDefined(typeof(SzfObjectAttribute), false));
|
||||||
|
|
||||||
|
foreach (var type in szfObjectTypes)
|
||||||
|
{
|
||||||
|
var attribute = type.GetCustomAttribute<SzfObjectAttribute>();
|
||||||
|
if (attribute != null)
|
||||||
|
{
|
||||||
|
// Add to the map using the TypeIdentifier from the attribute
|
||||||
|
_szfObjectTypeMap[attribute.TypeIdentifier] = type;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse a SZF file content and return the appropriate SzfObject type
|
/// Parses an SZF content string into a specific SzfObject type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The expected type of SzfObject.</typeparam>
|
||||||
|
/// <param name="szfContent">The SZF content string.</param>
|
||||||
|
/// <returns>An instance of the parsed SzfObject.</returns>
|
||||||
|
/// <exception cref="SzfParseException">Thrown if parsing fails or types mismatch.</exception>
|
||||||
public T Parse<T>(string szfContent) where T : SzfObject
|
public T Parse<T>(string szfContent) where T : SzfObject
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(szfContent))
|
var parseResult =
|
||||||
throw new SzfParseException("SZF content cannot be empty");
|
ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList());
|
||||||
|
|
||||||
var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
// Validate that the parsed type identifier matches the expected type T
|
||||||
.Select(line => line.Trim())
|
ValidateExpectedType<T>(parseResult.ObjectTypeIdentifier);
|
||||||
.Where(line => !string.IsNullOrEmpty(line))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var parseResult = ParseSzfStructure(lines);
|
// Create an instance of the specific SzfObject type using the parsed identifier
|
||||||
|
var obj = CreateInstance<T>(parseResult.ObjectTypeIdentifier);
|
||||||
// Validate that the parsed type matches the requested type
|
|
||||||
ValidateObjectType<T>(parseResult.ObjectType);
|
|
||||||
|
|
||||||
var obj = CreateInstance<T>(parseResult.ObjectType);
|
|
||||||
PopulateSzfObject(obj, parseResult);
|
PopulateSzfObject(obj, parseResult);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse SZF content without specifying the exact type - auto-detects type
|
/// Parses an SZF content string into a generic SzfObject. The concrete type will be determined from the content.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="szfContent">The SZF content string.</param>
|
||||||
|
/// <returns>An instance of the parsed SzfObject.</returns>
|
||||||
|
/// <exception cref="SzfParseException">Thrown if parsing fails.</exception>
|
||||||
public SzfObject Parse(string szfContent)
|
public SzfObject Parse(string szfContent)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(szfContent))
|
var parseResult =
|
||||||
throw new SzfParseException("SZF content cannot be empty");
|
ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList());
|
||||||
|
|
||||||
var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries)
|
// Create an instance of the specific SzfObject type using the parsed identifier
|
||||||
.Select(line => line.Trim())
|
var obj = CreateInstanceFromTypeIdentifier(parseResult.ObjectTypeIdentifier);
|
||||||
.Where(line => !string.IsNullOrEmpty(line))
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var parseResult = ParseSzfStructure(lines);
|
|
||||||
|
|
||||||
var obj = CreateInstanceFromType(parseResult.ObjectType);
|
|
||||||
PopulateSzfObject(obj, parseResult);
|
PopulateSzfObject(obj, parseResult);
|
||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an instance of the appropriate type based on SzfObjectType
|
/// Creates an instance of the specified SzfObject type, ensuring it's assignable to T.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private T CreateInstance<T>(SzfObjectType objectType) where T : SzfObject
|
/// <typeparam name="T">The type to cast the created instance to.</typeparam>
|
||||||
|
/// <param name="typeIdentifier">The string identifier of the SzfObject type.</param>
|
||||||
|
/// <returns>A new instance of the SzfObject.</returns>
|
||||||
|
/// <exception cref="SzfParseException">Thrown if the type is unknown or cannot be instantiated.</exception>
|
||||||
|
private T CreateInstance<T>(string typeIdentifier) where T : SzfObject
|
||||||
{
|
{
|
||||||
var instance = CreateInstanceFromType(objectType);
|
EnsureTypeMapPopulated();
|
||||||
if (instance is T typedInstance)
|
if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type))
|
||||||
return typedInstance;
|
{
|
||||||
|
throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
|
||||||
|
}
|
||||||
|
|
||||||
throw new SzfParseException($"Cannot cast {instance.GetType().Name} to {typeof(T).Name}");
|
if (!typeof(T).IsAssignableFrom(type))
|
||||||
|
{
|
||||||
|
throw new SzfParseException(
|
||||||
|
$"Requested type '{typeof(T).Name}' is not assignable from parsed type '{type.Name}'.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Activator.CreateInstance to create an instance of the found type
|
||||||
|
return (T)Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an instance based on the object type using the dynamically built map
|
/// Creates a generic SzfObject instance from its string identifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private SzfObject CreateInstanceFromType(SzfObjectType objectType)
|
/// <param name="typeIdentifier">The string identifier of the SzfObject type.</param>
|
||||||
|
/// <returns>A new instance of SzfObject.</returns>
|
||||||
|
/// <exception cref="SzfParseException">Thrown if the type is unknown or cannot be instantiated.</exception>
|
||||||
|
public SzfObject CreateInstanceFromTypeIdentifier(string typeIdentifier)
|
||||||
{
|
{
|
||||||
if (s_objectTypeToClassMap.TryGetValue(objectType, out var type))
|
EnsureTypeMapPopulated();
|
||||||
|
if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type))
|
||||||
{
|
{
|
||||||
|
throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
|
||||||
|
}
|
||||||
|
|
||||||
return (SzfObject)Activator.CreateInstance(type)!;
|
return (SzfObject)Activator.CreateInstance(type)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new SzfParseException($"Unsupported object type: {objectType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse SZF content into a generic structure before type-specific processing
|
/// Parses the raw SZF content lines into a structured SzfParseResult.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private SzfParseResult ParseSzfStructure(List<string> lines)
|
public SzfParseResult ParseSzfStructure(List<string> lines)
|
||||||
{
|
{
|
||||||
var result = new SzfParseResult();
|
var result = new SzfParseResult();
|
||||||
var currentSection = new List<string>();
|
var currentSectionLines = new List<string>();
|
||||||
var currentSectionName = string.Empty;
|
string? currentSectionName = null;
|
||||||
|
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
// Skip comments
|
var trimmedLine = line.Trim();
|
||||||
if (line.StartsWith("//") || line.StartsWith("#"))
|
|
||||||
|
// Handle headers (!type:, !schema:)
|
||||||
|
if (trimmedLine.StartsWith("!type:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
result.ObjectTypeIdentifier = trimmedLine.Substring("!type:".Length).Trim();
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse headers (!type:, !schema:)
|
if (trimmedLine.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase))
|
||||||
var headerMatch = HeaderRegex.Match(line);
|
|
||||||
if (headerMatch.Success)
|
|
||||||
{
|
{
|
||||||
var headerName = headerMatch.Groups[1].Value.ToLower();
|
result.SchemaVersion = trimmedLine.Substring("!schema:".Length).Trim();
|
||||||
var headerValue = headerMatch.Groups[2].Value.Trim();
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch (headerName)
|
if (trimmedLine.StartsWith("!", StringComparison.Ordinal)) // Other !headers
|
||||||
{
|
{
|
||||||
case "type":
|
var parts = trimmedLine.Split(new[] { ':' }, 2);
|
||||||
result.ObjectType = ParseObjectType(headerValue);
|
if (parts.Length == 2)
|
||||||
break;
|
{
|
||||||
case "schema":
|
result.Headers[parts[0].TrimStart('!').Trim()] = parts[1].Trim();
|
||||||
result.SchemaVersion = headerValue;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
result.Headers[headerName] = headerValue;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse section headers [Section Name]
|
// Handle section headers ([Section Name])
|
||||||
var sectionMatch = SectionRegex.Match(line);
|
if (trimmedLine.StartsWith("[") && trimmedLine.EndsWith("]"))
|
||||||
if (sectionMatch.Success)
|
|
||||||
{
|
{
|
||||||
// Process previous section if exists
|
// If there's an active section, process it before starting a new one
|
||||||
if (!string.IsNullOrEmpty(currentSectionName))
|
if (currentSectionName != null)
|
||||||
{
|
{
|
||||||
ProcessSection(result, currentSectionName, currentSection);
|
ProcessSection(result, currentSectionName, currentSectionLines);
|
||||||
|
currentSectionLines.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start new section
|
currentSectionName = trimmedLine.Substring(1, trimmedLine.Length - 2).Trim();
|
||||||
currentSectionName = sectionMatch.Groups[1].Value.Trim();
|
|
||||||
currentSection.Clear();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add line to current section
|
// Handle empty lines (ignored within sections for parsing structure)
|
||||||
if (!string.IsNullOrEmpty(currentSectionName))
|
if (string.IsNullOrWhiteSpace(trimmedLine))
|
||||||
{
|
{
|
||||||
currentSection.Add(line);
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, it's a field line within the current section
|
||||||
|
if (currentSectionName != null)
|
||||||
|
{
|
||||||
|
currentSectionLines.Add(trimmedLine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fields found before any section header, treat as global/unassigned, or throw error
|
||||||
|
// For now, we'll ignore them or consider them an error
|
||||||
|
// You might want to throw an exception here depending on strictness
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process final section
|
// Process the last section if any
|
||||||
if (!string.IsNullOrEmpty(currentSectionName))
|
if (currentSectionName != null)
|
||||||
{
|
{
|
||||||
ProcessSection(result, currentSectionName, currentSection);
|
ProcessSection(result, currentSectionName, currentSectionLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process an individual section and its fields
|
/// Processes lines belonging to a single section, parsing fields and adding to the result.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void ProcessSection(SzfParseResult result, string sectionName, List<string> sectionLines)
|
private void ProcessSection(SzfParseResult result, string sectionName, List<string> sectionLines)
|
||||||
{
|
{
|
||||||
var section = new SzfSection { Name = sectionName };
|
var szfSection = new SzfSection { Name = sectionName };
|
||||||
|
|
||||||
foreach (var line in sectionLines)
|
foreach (var line in sectionLines)
|
||||||
{
|
{
|
||||||
var fieldMatch = FieldRegex.Match(line);
|
// Expected format: Name (type) = Value
|
||||||
if (fieldMatch.Success)
|
var parts = line.Split(new[] { '=' }, 2);
|
||||||
|
if (parts.Length == 2)
|
||||||
{
|
{
|
||||||
var field = new SzfField
|
var nameAndTypePart = parts[0].Trim();
|
||||||
{
|
var valueString = parts[1].Trim();
|
||||||
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);
|
string fieldName;
|
||||||
|
string fieldType = "text"; // Default type if not specified
|
||||||
|
|
||||||
|
// Check for (type) in name part
|
||||||
|
var typeStart = nameAndTypePart.LastIndexOf('(');
|
||||||
|
var typeEnd = nameAndTypePart.LastIndexOf(')');
|
||||||
|
if (typeStart != -1 && typeEnd != -1 && typeEnd > typeStart)
|
||||||
|
{
|
||||||
|
fieldName = nameAndTypePart.Substring(0, typeStart).Trim();
|
||||||
|
fieldType = nameAndTypePart.Substring(typeStart + 1, typeEnd - typeStart - 1).Trim();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fieldName = nameAndTypePart;
|
||||||
}
|
}
|
||||||
|
|
||||||
result.Sections.Add(section);
|
szfSection.Fields.Add(new SzfField
|
||||||
|
{
|
||||||
|
Name = fieldName,
|
||||||
|
Type = fieldType,
|
||||||
|
Value = ParseFieldValue(valueString, fieldType)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Else, malformed field line, ignore or log error
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Sections.Add(szfSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse field value based on its declared type
|
/// Parses a field value string into an appropriate object type based on the field type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private object? ParseFieldValue(string valueString, string fieldType)
|
private object? ParseFieldValue(string valueString, string fieldType)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(valueString))
|
return fieldType.ToLowerInvariant() switch
|
||||||
return null;
|
|
||||||
|
|
||||||
return fieldType.ToLower() switch
|
|
||||||
{
|
{
|
||||||
"text" => valueString,
|
|
||||||
"text-field" => valueString,
|
|
||||||
"number" => ParseNumber(valueString),
|
"number" => ParseNumber(valueString),
|
||||||
"bool" => ParseBoolean(valueString),
|
"bool" => ParseBoolean(valueString),
|
||||||
"calculated" => valueString, // Store formula as string
|
"text-field" => valueString, // Text-fields are multi-line, keep as string
|
||||||
"system" => valueString,
|
_ => valueString // Default to string for "text" and unknown types
|
||||||
"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)
|
private object ParseNumber(string value)
|
||||||
{
|
{
|
||||||
if (int.TryParse(value, out var intResult))
|
if (int.TryParse(value, out var i)) return i;
|
||||||
return intResult;
|
if (double.TryParse(value, out var d)) return d;
|
||||||
|
return 0; // Default or throw error
|
||||||
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)
|
private bool ParseBoolean(string value)
|
||||||
{
|
{
|
||||||
return value.ToLower() switch
|
return bool.TryParse(value, out var b) && b;
|
||||||
{
|
|
||||||
"true" => true,
|
|
||||||
"false" => false,
|
|
||||||
"1" => true,
|
|
||||||
"0" => false,
|
|
||||||
_ => throw new SzfParseException($"Invalid boolean format: {value}")
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parse object type from string using the dynamically built map
|
/// Validates that the parsed object type identifier matches the expected type T.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private SzfObjectType ParseObjectType(string typeString)
|
/// <typeparam name="T">The expected SzfObject type.</typeparam>
|
||||||
|
/// <param name="parsedTypeIdentifier">The string identifier parsed from the SZF content.</param>
|
||||||
|
/// <exception cref="SzfParseException">Thrown if the types do not match.</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">Thrown if T is not properly decorated with SzfObjectAttribute.</exception>
|
||||||
|
private void ValidateExpectedType<T>(string parsedTypeIdentifier) where T : SzfObject
|
||||||
{
|
{
|
||||||
if (s_stringToObjectTypeMap.TryGetValue(typeString.ToLower(), out var objectType))
|
var targetAttribute = typeof(T).GetCustomAttribute<SzfObjectAttribute>();
|
||||||
|
if (targetAttribute == null)
|
||||||
{
|
{
|
||||||
return objectType;
|
throw new InvalidOperationException(
|
||||||
|
$"Type '{typeof(T).Name}' is missing the SzfObjectAttribute. Cannot validate expected type.");
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new SzfParseException($"Unknown object type: {typeString}");
|
if (!string.Equals(targetAttribute.TypeIdentifier, parsedTypeIdentifier, StringComparison.OrdinalIgnoreCase))
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validate that the parsed type matches the requested generic type using the attribute
|
|
||||||
/// </summary>
|
|
||||||
private void ValidateObjectType<T>(SzfObjectType parsedType) where T : SzfObject
|
|
||||||
{
|
{
|
||||||
var targetType = typeof(T);
|
throw new SzfParseException(
|
||||||
var attribute = targetType.GetCustomAttribute<SzfObjectAttribute>();
|
$"Mismatched SzfObject type. Expected '{targetAttribute.TypeIdentifier}' but found '{parsedTypeIdentifier}'.");
|
||||||
|
|
||||||
if (attribute == null)
|
|
||||||
{
|
|
||||||
throw new SzfParseException($"Type {targetType.Name} does not have SzfObjectAttribute.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsedType != attribute.ObjectType)
|
|
||||||
{
|
|
||||||
throw new SzfParseException($"Type mismatch: Expected {attribute.ObjectType}, found {parsedType}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Populate the SzfObject with parsed data
|
/// Populates an SzfObject instance with the data from a parsed SzfParseResult.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult)
|
public void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult)
|
||||||
{
|
{
|
||||||
// Set schema version
|
// Extract metadata fields into a dictionary for PopulateFromMetadata
|
||||||
obj.SchemaVersion = parseResult.SchemaVersion;
|
var metadataFields = parseResult.Sections
|
||||||
|
.FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))?
|
||||||
|
.Fields.ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase)
|
||||||
|
?? new Dictionary<string, SzfField>();
|
||||||
|
|
||||||
// Find and process Metadata section
|
obj.PopulateFromMetadata(metadataFields);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delegate type-specific section parsing to the SzfObject itself
|
// Pass all sections (excluding Metadata, which PopulateFromMetadata might handle) to ParseSections
|
||||||
|
// ParseSections is responsible for building the object's specific internal structure.
|
||||||
obj.ParseSections(parseResult.Sections);
|
obj.ParseSections(parseResult.Sections);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal class to hold parsed SZF structure
|
/// Represents the parsed structure of an SZF file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SzfParseResult // Changed from internal to public for broader accessibility if needed
|
public class SzfParseResult
|
||||||
{
|
{
|
||||||
public SzfObjectType ObjectType { get; set; }
|
// Changed from SzfObjectType to string
|
||||||
public string SchemaVersion { get; set; } = "1.0.0";
|
public string ObjectTypeIdentifier { get; set; } = string.Empty;
|
||||||
public Dictionary<string, string> Headers { get; set; } = new();
|
public string SchemaVersion { get; set; } = string.Empty;
|
||||||
public List<SzfSection> Sections { get; set; } = new();
|
public Dictionary<string, string> Headers { get; set; } = new Dictionary<string, string>();
|
||||||
|
public List<SzfSection> Sections { get; set; } = new List<SzfSection>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Internal class representing a parsed section
|
/// Represents a section within an SZF file.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SzfSection // Changed from internal to public for broader accessibility if needed
|
public class SzfSection
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public List<SzfField> Fields { get; set; } = new();
|
public List<SzfField> Fields { get; set; } = new List<SzfField>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exception thrown during SZF parsing
|
/// Exception thrown during SZF parsing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SzfParseException : Exception
|
public class SzfParseException : Exception
|
||||||
{
|
{
|
||||||
|
@ -25,19 +25,25 @@
|
|||||||
{
|
{
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr><th>Object Type</th><td>@parsedObject.ObjectType</td></tr>
|
<tr><th>Object Type</th><td>@parsedObject.TypeIdentifier</td></tr>
|
||||||
<tr><th>Schema Version</th><td>@parsedObject.SchemaVersion</td></tr>
|
@if (!string.IsNullOrEmpty(parsedSchemaVersion))
|
||||||
<tr><th>Name</th><td>@parsedObject.Name</td></tr>
|
{
|
||||||
<tr><th>Version</th><td>@parsedObject.Version</td></tr>
|
<tr><th>Schema Version</th><td>@parsedSchemaVersion</td></tr>
|
||||||
<tr><th>ID</th><td>@parsedObject.Id</td></tr>
|
}
|
||||||
<tr><th>Author</th><td>@parsedObject.Author</td></tr>
|
<tr><th>Name</th><td>@parsedObject.Metadata.Name</td></tr>
|
||||||
<tr><th>Description</th><td>@parsedObject.Description</td></tr>
|
<tr><th>Version</th><td>@parsedObject.Metadata.Version</td></tr>
|
||||||
|
<tr><th>ID</th><td>@parsedObject.Metadata.Guid</td></tr>
|
||||||
|
<tr><th>Author</th><td>@parsedObject.Metadata.Author</td></tr>
|
||||||
|
<tr><th>Description</th><td>@parsedObject.Metadata.Description</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@if (parsedObject.ExtendedMetadata.Any())
|
@* Display Dataset specific data *@
|
||||||
|
@if (parsedObject is Dataset dataset)
|
||||||
{
|
{
|
||||||
<h3>Extended Metadata</h3>
|
@if (dataset.Metadata.Any()) // Display Dataset's specific metadata
|
||||||
|
{
|
||||||
|
<h3>Dataset Specific Metadata</h3>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -46,7 +52,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@foreach (var meta in parsedObject.ExtendedMetadata)
|
@foreach (var meta in dataset.Metadata)
|
||||||
{
|
{
|
||||||
<tr>
|
<tr>
|
||||||
<td>@meta.Key</td>
|
<td>@meta.Key</td>
|
||||||
@ -57,9 +63,6 @@
|
|||||||
</table>
|
</table>
|
||||||
}
|
}
|
||||||
|
|
||||||
@* Display Dataset specific data *@
|
|
||||||
@if (parsedObject is Dataset dataset)
|
|
||||||
{
|
|
||||||
@if (dataset.Entries.Any())
|
@if (dataset.Entries.Any())
|
||||||
{
|
{
|
||||||
<h3>Dataset Entries</h3>
|
<h3>Dataset Entries</h3>
|
||||||
@ -128,6 +131,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@* Display Character specific data *@
|
||||||
|
@if (parsedObject is Character character)
|
||||||
|
{
|
||||||
|
@if (character.CharacterSections.Any())
|
||||||
|
{
|
||||||
|
<h3>Character Sections</h3>
|
||||||
|
@foreach (var section in character.CharacterSections)
|
||||||
|
{
|
||||||
|
@RenderCharacterSection(section)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@if (errorMessages.Any())
|
@if (errorMessages.Any())
|
||||||
{
|
{
|
||||||
@ -210,6 +226,7 @@
|
|||||||
@code {
|
@code {
|
||||||
private string szfCode = string.Empty;
|
private string szfCode = string.Empty;
|
||||||
private SzfObject? parsedObject;
|
private SzfObject? parsedObject;
|
||||||
|
private string? parsedSchemaVersion; // To store the schema version from parsing
|
||||||
private string generatedSzfOutput = string.Empty; // New field for generated output
|
private string generatedSzfOutput = string.Empty; // New field for generated output
|
||||||
private List<string> logMessages = new List<string>();
|
private List<string> logMessages = new List<string>();
|
||||||
private List<string> errorMessages = new List<string>();
|
private List<string> errorMessages = new List<string>();
|
||||||
@ -229,10 +246,17 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
parsedObject = SzfParser.Parse(szfCode);
|
var parseResult = SzfParser.ParseSzfStructure(szfCode.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList());
|
||||||
|
|
||||||
|
// Create the SzfObject instance from the parseResult
|
||||||
|
parsedObject = SzfParser.CreateInstanceFromTypeIdentifier(parseResult.ObjectTypeIdentifier);
|
||||||
|
SzfParser.PopulateSzfObject(parsedObject, parseResult);
|
||||||
|
|
||||||
|
parsedSchemaVersion = parseResult.SchemaVersion; // Store the parsed schema version
|
||||||
|
|
||||||
Log("SZF content parsed successfully.");
|
Log("SZF content parsed successfully.");
|
||||||
Log($"Object Type: {parsedObject.ObjectType}");
|
Log($"Object Type: {parsedObject.TypeIdentifier}");
|
||||||
Log($"Schema Version: {parsedObject.SchemaVersion}");
|
Log($"Schema Version: {parsedSchemaVersion}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -254,8 +278,8 @@
|
|||||||
Log("Starting SZF generation...");
|
Log("Starting SZF generation...");
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Use the ToSzfString() method on the parsed object, which uses SzfGenerator internally
|
// Use the SzfGenerator to generate the string
|
||||||
generatedSzfOutput = parsedObject.ToSzfString();
|
generatedSzfOutput = SzfGenerator.Generate(parsedObject);
|
||||||
Log("SZF content generated successfully.");
|
Log("SZF content generated successfully.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -271,6 +295,7 @@
|
|||||||
logMessages.Clear();
|
logMessages.Clear();
|
||||||
errorMessages.Clear();
|
errorMessages.Clear();
|
||||||
parsedObject = null;
|
parsedObject = null;
|
||||||
|
parsedSchemaVersion = null; // Clear parsed schema version
|
||||||
generatedSzfOutput = string.Empty;
|
generatedSzfOutput = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,7 +334,7 @@
|
|||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
// New helper for rendering CharacterTemplate sections
|
// Helper for rendering CharacterTemplate sections
|
||||||
private RenderFragment RenderTemplateSection(TemplateSection templateSection) =>@<div>
|
private RenderFragment RenderTemplateSection(TemplateSection templateSection) =>@<div>
|
||||||
<strong>@templateSection.Name</strong>
|
<strong>@templateSection.Name</strong>
|
||||||
@if (templateSection.Fields.Any())
|
@if (templateSection.Fields.Any())
|
||||||
@ -331,4 +356,27 @@
|
|||||||
@RenderTemplateSection(subSection)
|
@RenderTemplateSection(subSection)
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
// New helper for rendering Character sections
|
||||||
|
private RenderFragment RenderCharacterSection(CharacterSection characterSection) =>@<div>
|
||||||
|
<h4>@characterSection.Name</h4>
|
||||||
|
@if (characterSection.Fields.Any())
|
||||||
|
{
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var field in characterSection.Fields)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td><strong>@field.Name</strong></td>
|
||||||
|
<td>@field.Value</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
@foreach (var subSection in characterSection.Subsections)
|
||||||
|
{
|
||||||
|
@RenderCharacterSection(subSection)
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
}
|
}
|
70
test_character.szf
Normal file
70
test_character.szf
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
!type: character
|
||||||
|
!schema: 1.0.0
|
||||||
|
|
||||||
|
[Metadata]
|
||||||
|
Name (text) = Elara Moonwhisper
|
||||||
|
Version (text) = 1.0.0
|
||||||
|
GUID (text) = 1a2b3c4d-5e6f-7890-1234-567890abcdef
|
||||||
|
Description (text-field) = A young elven rogue with a mysterious past.
|
||||||
|
Author (text) = Player One
|
||||||
|
TemplateRef (dataset-reference) = Advanced Fantasy Character Sheet|f9e8d7c6-b5a4-3210-9876-543210fedcba|2.1.0
|
||||||
|
|
||||||
|
[Section: Core Identity]
|
||||||
|
CharacterName (text) = Elara Moonwhisper
|
||||||
|
PlayerName (text) = John Doe
|
||||||
|
Race (dataset-reference) = Human|abcd1234-5678-90ab-cdef-1234567890ab|1.0.0
|
||||||
|
Class (dataset-reference) = Rogue|00001111-2222-3333-4444-555566667777|1.0.0
|
||||||
|
Level (number) = 3
|
||||||
|
Experience (number) = 1500
|
||||||
|
Alignment (text) = Chaotic Neutral
|
||||||
|
Background (text) = Urchin
|
||||||
|
Age (number) = 28
|
||||||
|
Height (text) = 5'6"
|
||||||
|
Weight (text) = 120 lbs
|
||||||
|
Gender (text) = Female
|
||||||
|
|
||||||
|
[Section: Ability Scores]
|
||||||
|
Strength (number) = 10
|
||||||
|
Dexterity (number) = 16
|
||||||
|
Constitution (number) = 14
|
||||||
|
Intelligence (number) = 12
|
||||||
|
Wisdom (number) = 10
|
||||||
|
Charisma (number) = 8
|
||||||
|
|
||||||
|
[Section: Combat Statistics]
|
||||||
|
HitPoints (number) = 22
|
||||||
|
BaseAttackBonus (number) = 2
|
||||||
|
|
||||||
|
[Section: Skills]
|
||||||
|
Acrobatics (number) = 3
|
||||||
|
Athletics (number) = 0
|
||||||
|
Deception (number) = 0
|
||||||
|
History (number) = 1
|
||||||
|
Insight (number) = 0
|
||||||
|
Intimidation (number) = 0
|
||||||
|
Investigation (number) = 2
|
||||||
|
Medicine (number) = 0
|
||||||
|
Nature (number) = 0
|
||||||
|
Perception (number) = 2
|
||||||
|
Performance (number) = 0
|
||||||
|
Persuasion (number) = 0
|
||||||
|
Religion (number) = 0
|
||||||
|
SleightOfHand (number) = 3
|
||||||
|
Stealth (number) = 3
|
||||||
|
Survival (number) = 0
|
||||||
|
|
||||||
|
[Section.Equipment.Currency]
|
||||||
|
PlatinumPieces (number) = 0
|
||||||
|
GoldPieces (number) = 5
|
||||||
|
SilverPieces (number) = 12
|
||||||
|
CopperPieces (number) = 50
|
||||||
|
|
||||||
|
[Section: Personal Journal]
|
||||||
|
CharacterDescription (text-field) = Elara has sharp, emerald eyes and dark, flowing hair. She carries herself with a quiet confidence.
|
||||||
|
Personality (text-field) = Resourceful and independent, but distrustful of authority.
|
||||||
|
Ideals (text-field) = Freedom. Everyone should be free to live their own life.
|
||||||
|
Bonds (text-field) = She owes a debt to an old beggar who saved her life.
|
||||||
|
Flaws (text-field) = Impulsive and prone to taking unnecessary risks.
|
||||||
|
Backstory (text-field) = Orphaned at a young age, Elara survived on the streets, learning to pick pockets and locks.
|
||||||
|
Goals (text-field) = To uncover the truth about her parents' disappearance.
|
||||||
|
Notes (text-field) = Has a small, intricately carved wooden bird she keeps as a good luck charm.
|
106
test_dataset.szf
106
test_dataset.szf
@ -1,106 +0,0 @@
|
|||||||
!type: dataset
|
|
||||||
!schema: 1.0.0
|
|
||||||
|
|
||||||
[Metadata]
|
|
||||||
Name (text) = Complete Fantasy Arsenal
|
|
||||||
Type (text) = items
|
|
||||||
Version (text) = 1.2.0
|
|
||||||
GUID (text) = a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
||||||
Description (text-field) = Comprehensive collection of fantasy weapons, armor, and magical items with full stat blocks and lore
|
|
||||||
Author (text) = Fantasy Content Creator
|
|
||||||
Tags (text) = fantasy, weapons, armor, magic, core
|
|
||||||
|
|
||||||
[Entry: Flamebrand Longsword]
|
|
||||||
Name (text) = Flamebrand Longsword
|
|
||||||
Description (text-field) = An ancient blade wreathed in perpetual flames, forged in the heart of a dying star. The crossguard bears draconic runes that pulse with inner fire.
|
|
||||||
Category (text) = Weapon
|
|
||||||
ItemType (text) = Sword
|
|
||||||
Rarity (text) = Legendary
|
|
||||||
Value (number) = 5000
|
|
||||||
Weight (number) = 3.5
|
|
||||||
IsStackable (bool) = false
|
|
||||||
RequiresAttunement (bool) = true
|
|
||||||
|
|
||||||
[Entry.Flamebrand Longsword.Combat Stats]
|
|
||||||
Damage (text) = 1d8 + 2
|
|
||||||
DamageType (text) = Slashing + Fire
|
|
||||||
AttackBonus (number) = 2
|
|
||||||
CriticalRange (text) = 19-20
|
|
||||||
CriticalMultiplier (text) = x2
|
|
||||||
Reach (number) = 5
|
|
||||||
WeaponType (text) = Martial
|
|
||||||
|
|
||||||
[Entry.Flamebrand Longsword.Properties]
|
|
||||||
IsMagical (bool) = true
|
|
||||||
IsCursed (bool) = false
|
|
||||||
IsArtifact (bool) = false
|
|
||||||
School (text) = Evocation
|
|
||||||
CasterLevel (number) = 12
|
|
||||||
|
|
||||||
[Entry.Flamebrand Longsword.Special Abilities]
|
|
||||||
FlameStrike (text-field) = Once per day, wielder can activate to deal additional 2d6 fire damage on next successful hit
|
|
||||||
FireImmunity (bool) = true
|
|
||||||
LightSource (text) = Sheds bright light in 20-foot radius, dim light for additional 20 feet
|
|
||||||
IgniteFlammables (bool) = true
|
|
||||||
|
|
||||||
[Entry.Flamebrand Longsword.Lore]
|
|
||||||
Origin (text-field) = Created by the legendary smith Valdris Fireforge during the War of Burning Skies
|
|
||||||
History (text-field) = Wielded by three kings before disappearing into the Tomb of Echoes. Recently recovered by the Sunblade Company.
|
|
||||||
Legends (text-field) = Said to grow stronger when facing creatures of darkness and cold
|
|
||||||
Identification (text) = DC 20 Arcana check reveals basic properties, DC 25 reveals special abilities
|
|
||||||
|
|
||||||
[Entry: Dragonscale Plate]
|
|
||||||
Name (text) = Dragonscale Plate Armor
|
|
||||||
Description (text-field) = Masterwork plate armor crafted from the scales of an ancient red dragon, providing exceptional protection and resistance to elemental damage.
|
|
||||||
Category (text) = Armor
|
|
||||||
ItemType (text) = Heavy Armor
|
|
||||||
Rarity (text) = Very Rare
|
|
||||||
Value (number) = 8000
|
|
||||||
Weight (number) = 55
|
|
||||||
IsStackable (bool) = false
|
|
||||||
RequiresAttunement (bool) = false
|
|
||||||
|
|
||||||
[Entry.Dragonscale Plate.Armor Stats]
|
|
||||||
ArmorClass (number) = 18
|
|
||||||
ArmorType (text) = Plate
|
|
||||||
MaxDexBonus (number) = 0
|
|
||||||
ArmorCheckPenalty (number) = -6
|
|
||||||
ArcaneSpellFailure (number) = 35
|
|
||||||
SpeedReduction (number) = 10
|
|
||||||
|
|
||||||
[Entry.Dragonscale Plate.Resistances]
|
|
||||||
FireResistance (bool) = true
|
|
||||||
ColdResistance (bool) = false
|
|
||||||
AcidResistance (bool) = true
|
|
||||||
ElectricResistance (bool) = false
|
|
||||||
SonicResistance (bool) = false
|
|
||||||
|
|
||||||
[Entry.Dragonscale Plate.Special Properties]
|
|
||||||
DragonAura (text-field) = Intimidation checks gain +2 circumstance bonus
|
|
||||||
ScaleRegeneration (text-field) = Armor self-repairs minor damage over time
|
|
||||||
BreathWeaponProtection (number) = 50
|
|
||||||
|
|
||||||
[Entry: Healing Potion Lesser]
|
|
||||||
Name (text) = Potion of Lesser Healing
|
|
||||||
Description (text-field) = A crimson liquid that glows faintly with restorative magic, contained in a crystal vial with a cork stopper.
|
|
||||||
Category (text) = Consumable
|
|
||||||
ItemType (text) = Potion
|
|
||||||
Rarity (text) = Common
|
|
||||||
Value (number) = 50
|
|
||||||
Weight (number) = 0.5
|
|
||||||
IsStackable (bool) = true
|
|
||||||
MaxStack (number) = 10
|
|
||||||
|
|
||||||
[Entry.Healing Potion Lesser.Effects]
|
|
||||||
HealingAmount (text) = 2d4 + 2
|
|
||||||
Duration (text) = Instantaneous
|
|
||||||
ActionType (text) = Standard Action
|
|
||||||
UsageLimit (number) = 1
|
|
||||||
DestroyOnUse (bool) = true
|
|
||||||
|
|
||||||
[Entry.Healing Potion Lesser.Crafting]
|
|
||||||
CraftDC (number) = 15
|
|
||||||
CraftSkill (text) = Alchemy
|
|
||||||
MaterialCost (number) = 25
|
|
||||||
CraftTime (text) = 4 hours
|
|
||||||
RequiredLevel (number) = 3
|
|
60
todo.md
Normal file
60
todo.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
### TODO List: Local Storage and Data Management for .szf Data
|
||||||
|
**Phase 1: Local Storage and SZF Serialization Foundation**
|
||||||
|
- **Task: Create a Local Storage Service**
|
||||||
|
- **Description**: Implement a service to abstract browser `localStorage` interactions for storing and retrieving string data. This will likely involve Blazor JavaScript interop.
|
||||||
|
- **Files Affected/Created**:
|
||||||
|
- `Services/ILocalStorageService.cs` (new interface)
|
||||||
|
- `Services/LocalStorageService.cs` (new implementation)
|
||||||
|
|
||||||
|
- **Context**: This service will be the low-level API for `localStorage`. It will handle saving and loading the raw .szf string content.
|
||||||
|
|
||||||
|
- **Task: Develop SZF Object Converter/Serializer Helper**
|
||||||
|
- **Description**: Create a utility class or static methods to convert `Character`, `Dataset`, and `CharacterTemplate` objects to and from their string representations. This should leverage the existing `PopulateFromMetadata`, `ParseSections`, `GenerateMetadata`, and `GenerateContent` methods within these classes. `.szf`
|
||||||
|
- **Files Affected/Created**:
|
||||||
|
- `Utils/SzfConverter.cs` (new static class)
|
||||||
|
|
||||||
|
- **Context**: , , all have methods for processing SZF sections. This helper will orchestrate using those methods for full object serialization/deserialization to/from a single string. The will be crucial here for understanding the file structure. `Character.cs``Dataset.cs``CharacterTemplate.cs``szf-documentation.md`
|
||||||
|
|
||||||
|
**Phase 2: SZF Data Repository and Retrieval Logic**
|
||||||
|
- **Task: Design and Implement SZF Data Repository**
|
||||||
|
- **Description**: Create a central service that manages all loaded entities (`Character`, `Dataset`, `CharacterTemplate`). It will use the `ILocalStorageService` to persist data and the `SzfConverter` to handle object-to-string conversions. `.szf`
|
||||||
|
- **Files Affected/Created**:
|
||||||
|
- `Services/ISzfDataRepository.cs` (new interface)
|
||||||
|
- `Services/SzfDataRepository.cs` (new implementation)
|
||||||
|
|
||||||
|
- **Context**: This is the core of the data management. It will hold collections of all available entities and provide methods for querying them.
|
||||||
|
|
||||||
|
- **Task: Implement Retrieval Methods in `SzfDataRepository`**
|
||||||
|
- **Description**: Add methods to `SzfDataRepository` for retrieving stored entities based on various criteria.
|
||||||
|
- **Methods to Implement**:
|
||||||
|
- `Save<T>(T entity)`: Persists a `Character`, `Dataset`, or `CharacterTemplate` instance to local storage.
|
||||||
|
- `LoadAllSzfEntities()`: Loads all known data (characters, datasets, templates) from local storage into the repository's in-memory collections on application startup. `.szf`
|
||||||
|
- `GetCharacter(Guid guid, Version version)`: Retrieves a specific `Character` by its template GUID and version.
|
||||||
|
- `GetDataset(Guid guid, Version version)`: Retrieves a specific `Dataset` by its GUID and version (from metadata).
|
||||||
|
- `GetCharacterTemplate(Guid guid, Version version)`: Retrieves a specific `CharacterTemplate` by its GUID and version (from metadata).
|
||||||
|
- `GetDatasetsByType(string datasetType)`: Retrieves all `Dataset` instances of a specific type (e.g., "items", "spells").
|
||||||
|
- `FindCharactersByName(string name)`: Finds characters by name (may return multiple).
|
||||||
|
- `FindDatasetsByName(string name)`: Finds datasets by name (may return multiple).
|
||||||
|
- `FindCharacterTemplatesByName(string name)`: Finds character templates by name (may return multiple).
|
||||||
|
|
||||||
|
- **Files Affected/Created**: `Services/SzfDataRepository.cs`
|
||||||
|
- **Context**: The explicitly mentions GUID, version, name, and type for data management. , , and all contain the necessary metadata fields (like `Guid`, `TemplateGuid`, `Version`, `Name`, `DatasetType`) to support these retrieval methods. `technical-specifications.md``Character.cs``Dataset.cs``CharacterTemplate.cs`
|
||||||
|
|
||||||
|
**Phase 3: Integration and Relationship Resolution**
|
||||||
|
- **Task: Integrate Data Loading with Core Models**
|
||||||
|
- **Description**: Modify `Character`, `CharacterTemplate`, and `Dataset` classes, or create factory methods for them, to use the `SzfDataRepository` for loading.
|
||||||
|
- **Files Affected/Created**:
|
||||||
|
- `Character.cs`
|
||||||
|
- `Dataset.cs`
|
||||||
|
- `CharacterTemplate.cs`
|
||||||
|
|
||||||
|
- **Context**: When a `Character` is loaded, it will need to retrieve its associated `CharacterTemplate`. When a `CharacterTemplate` is loaded, it will need to resolve its `RequiredDatasets`. This is where the relationships outlined in and come into play. The `DatasetReference` class in and `RequiredDatasetReference` in are key for this. `technical-specifications.md``szf-documentation.md``Dataset.cs``CharacterTemplate.cs`
|
||||||
|
|
||||||
|
- **Task: Implement Relationship Resolution Logic**
|
||||||
|
- **Description**: Ensure that when a `Character` is loaded, its `TemplateGuid` and `TemplateVersion` are used to load the correct `CharacterTemplate` from the `SzfDataRepository`. Similarly, ensure `CharacterTemplate` objects correctly resolve their `RequiredDatasets`.
|
||||||
|
- **Files Affected/Created**:
|
||||||
|
- `Character.cs`
|
||||||
|
- `CharacterTemplate.cs`
|
||||||
|
- `Services/SzfDataRepository.cs` (might need helper methods for resolution)
|
||||||
|
|
||||||
|
- **Context**: The user explicitly mentioned "relationship between character>character_template>dataset". This task addresses that. The `DatasetReference` class will be crucial for parsing the strings from the .szf files and then using the `SzfDataRepository` to fetch the actual `Dataset` objects. `Name|GUID|Version`
|
Loading…
Reference in New Issue
Block a user