Created character szf object. More seperation by removing the enum from ZsfObject and using reflection for szf object types.

This commit is contained in:
Chris Bell 2025-07-01 09:35:28 -05:00
parent 678c10a872
commit 858a30d531
11 changed files with 817 additions and 486 deletions

View 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);
}
}
}

View File

@ -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);

View File

@ -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)
{ {

View File

@ -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>

View File

@ -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

View File

@ -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;
} }
} }

View File

@ -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, {
Attribute = type.GetCustomAttribute<SzfObjectAttribute>() if (_szfObjectTypeMap.Any()) return; // Already populated
})
.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($"Cannot cast {instance.GetType().Name} to {typeof(T).Name}");
}
/// <summary>
/// Create an instance based on the object type using the dynamically built map
/// </summary>
private SzfObject CreateInstanceFromType(SzfObjectType objectType)
{
if (s_objectTypeToClassMap.TryGetValue(objectType, out var type))
{ {
return (SzfObject)Activator.CreateInstance(type)!; throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
} }
throw new SzfParseException($"Unsupported object type: {objectType}"); 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>
/// Parse SZF content into a generic structure before type-specific processing /// Creates a generic SzfObject instance from its string identifier.
/// </summary> /// </summary>
private SzfParseResult ParseSzfStructure(List<string> lines) /// <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)
{
EnsureTypeMapPopulated();
if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, out var type))
{
throw new SzfParseException($"Unknown SzfObject type identifier: '{typeIdentifier}'.");
}
return (SzfObject)Activator.CreateInstance(type)!;
}
/// <summary>
/// Parses the raw SZF content lines into a structured SzfParseResult.
/// </summary>
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("#"))
continue;
// Parse headers (!type:, !schema:) // Handle headers (!type:, !schema:)
var headerMatch = HeaderRegex.Match(line); if (trimmedLine.StartsWith("!type:", StringComparison.OrdinalIgnoreCase))
if (headerMatch.Success)
{ {
var headerName = headerMatch.Groups[1].Value.ToLower(); result.ObjectTypeIdentifier = trimmedLine.Substring("!type:".Length).Trim();
var headerValue = headerMatch.Groups[2].Value.Trim(); continue;
}
switch (headerName) if (trimmedLine.StartsWith("!schema:", StringComparison.OrdinalIgnoreCase))
{
result.SchemaVersion = trimmedLine.Substring("!schema:".Length).Trim();
continue;
}
if (trimmedLine.StartsWith("!", StringComparison.Ordinal)) // Other !headers
{
var parts = trimmedLine.Split(new[] { ':' }, 2);
if (parts.Length == 2)
{ {
case "type": result.Headers[parts[0].TrimStart('!').Trim()] = parts[1].Trim();
result.ObjectType = ParseObjectType(headerValue);
break;
case "schema":
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;
}
szfSection.Fields.Add(new SzfField
{
Name = fieldName,
Type = fieldType,
Value = ParseFieldValue(valueString, fieldType)
});
} }
// Else, malformed field line, ignore or log error
} }
result.Sections.Add(section); 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);
var attribute = targetType.GetCustomAttribute<SzfObjectAttribute>();
if (attribute == null)
{ {
throw new SzfParseException($"Type {targetType.Name} does not have SzfObjectAttribute."); throw new SzfParseException(
} $"Mismatched SzfObject type. Expected '{targetAttribute.TypeIdentifier}' but found '{parsedTypeIdentifier}'.");
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
{ {

View File

@ -25,41 +25,44 @@
{ {
<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())
{
<h3>Extended Metadata</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
@foreach (var meta in parsedObject.ExtendedMetadata)
{
<tr>
<td>@meta.Key</td>
<td>@meta.Value</td>
</tr>
}
</tbody>
</table>
}
@* Display Dataset specific data *@ @* Display Dataset specific data *@
@if (parsedObject is Dataset dataset) @if (parsedObject is Dataset dataset)
{ {
@if (dataset.Metadata.Any()) // Display Dataset's specific metadata
{
<h3>Dataset Specific Metadata</h3>
<table>
<thead>
<tr>
<th>Key</th>
<th>Value</th>
</tr>
</thead>
<tbody>
@foreach (var meta in dataset.Metadata)
{
<tr>
<td>@meta.Key</td>
<td>@meta.Value</td>
</tr>
}
</tbody>
</table>
}
@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
View 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.

View File

@ -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
View 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`