From 858a30d531c92df122e369669320072e6bc33d25 Mon Sep 17 00:00:00 2001 From: Chris Bell Date: Tue, 1 Jul 2025 09:35:28 -0500 Subject: [PATCH] Created character szf object. More seperation by removing the enum from ZsfObject and using reflection for szf object types. --- SessionZero/Data/Character.cs | 254 ++++++++++++++++ SessionZero/Data/CharacterTemplate.cs | 6 +- SessionZero/Data/Dataset.cs | 5 +- SessionZero/Data/SzfGenerator.cs | 54 +--- SessionZero/Data/SzfObject.cs | 197 ++++++------ SessionZero/Data/SzfObjectAttribute.cs | 27 +- SessionZero/Data/SzfParser.cs | 406 +++++++++++++------------ SessionZero/Pages/SzfParseTest.razor | 118 ++++--- test_character.szf | 70 +++++ test_dataset.szf | 106 ------- todo.md | 60 ++++ 11 files changed, 817 insertions(+), 486 deletions(-) create mode 100644 SessionZero/Data/Character.cs create mode 100644 test_character.szf create mode 100644 todo.md diff --git a/SessionZero/Data/Character.cs b/SessionZero/Data/Character.cs new file mode 100644 index 0000000..6612363 --- /dev/null +++ b/SessionZero/Data/Character.cs @@ -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 Fields { get; set; } = new(); + public List 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 CharacterSections { get; private set; } = new List(); + + 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 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 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(); + + // A map to quickly find parent sections by their name (simplified for lookup) + // e.g., "Character Information" -> CharacterSection object + var sectionNameMap = new Dictionary(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(); + // 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 sections, string path) + { + var segments = path.Split('.'); + CharacterSection? currentSection = null; + List 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); + } + } +} \ No newline at end of file diff --git a/SessionZero/Data/CharacterTemplate.cs b/SessionZero/Data/CharacterTemplate.cs index ba0a59c..4aa452c 100644 --- a/SessionZero/Data/CharacterTemplate.cs +++ b/SessionZero/Data/CharacterTemplate.cs @@ -3,15 +3,13 @@ using System.Linq; namespace SessionZero.Data; -[SzfObject(SzfObjectType.CharacterTemplate, "character_template")] +[SzfObject("character_template")] public class CharacterTemplate : SzfObject { public string GameSystem { get; set; } = string.Empty; public List RequiredDatasets { get; set; } = new(); public List Sections { get; set; } = new(); - - public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate; - + public override void PopulateFromMetadata(Dictionary metadata) { base.PopulateFromMetadata(metadata); diff --git a/SessionZero/Data/Dataset.cs b/SessionZero/Data/Dataset.cs index dc0eaf7..793e06f 100644 --- a/SessionZero/Data/Dataset.cs +++ b/SessionZero/Data/Dataset.cs @@ -4,15 +4,14 @@ using System.Linq; namespace SessionZero.Data; -[SzfObject(SzfObjectType.Dataset, "dataset")] +[SzfObject("dataset")] public class Dataset : SzfObject { public string DatasetType { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; // New field for dataset image public List Entries { get; set; } = new(); public Dictionary Metadata { get; set; } = new(); // To store other metadata fields - - public override SzfObjectType ObjectType => SzfObjectType.Dataset; + public override void PopulateFromMetadata(Dictionary metadata) { diff --git a/SessionZero/Data/SzfGenerator.cs b/SessionZero/Data/SzfGenerator.cs index fe35089..4ac14d8 100644 --- a/SessionZero/Data/SzfGenerator.cs +++ b/SessionZero/Data/SzfGenerator.cs @@ -1,5 +1,6 @@ using System.Reflection; using System.Text; +using System; // Required for Exception namespace SessionZero.Data; @@ -23,11 +24,11 @@ public class SzfGenerator // Generate header GenerateHeader(writer, obj); - // Generate metadata section - GenerateMetadataSection(writer, obj); - - // Generate type-specific content - GenerateTypeSpecificContent(writer, obj); + // Generate type-specific metadata and content + // These methods delegate to the SzfObject itself, + // which now correctly accesses its own Metadata. + obj.GenerateMetadata(writer); // This now handles writing the standard metadata section + obj.GenerateContent(writer); // This handles writing object-specific content sections return builder.ToString(); } @@ -37,47 +38,12 @@ public class SzfGenerator /// private void GenerateHeader(SzfFieldWriter writer, SzfObject obj) { - var type = obj.GetType(); - var attribute = type.GetCustomAttribute(); - - if (attribute == null) - { - 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}"); + // TypeIdentifier is now directly accessible from the SzfObject base class + writer.Builder.AppendLine($"!type: {obj.TypeIdentifier}"); + // Schema version is a fixed format version, not object-specific metadata + writer.Builder.AppendLine($"!schema: 1.0.0"); // Assuming current schema version is 1.0.0 writer.Builder.AppendLine(); } - - /// - /// Generate the common metadata section - /// - 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(); - } - - /// - /// Generate type-specific content sections delegated to the object itself - /// - private void GenerateTypeSpecificContent(SzfFieldWriter writer, SzfObject obj) - { - obj.GenerateContent(writer); - } } /// diff --git a/SessionZero/Data/SzfObject.cs b/SessionZero/Data/SzfObject.cs index b7140be..7f05c94 100644 --- a/SessionZero/Data/SzfObject.cs +++ b/SessionZero/Data/SzfObject.cs @@ -2,123 +2,142 @@ using System; using System.Collections.Generic; +using System.Reflection; namespace SessionZero.Data; +/// +/// Represents common metadata for all SzfObjects. +/// +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 Guid Id { get; set; } = Guid.NewGuid(); - public string Name { get; set; } = string.Empty; - public string Version { get; set; } = "1.0.0"; - public string Description { get; set; } = string.Empty; - 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 ExtendedMetadata { get; set; } = new(); - - public abstract SzfObjectType ObjectType { get; } + /// + /// The unique string identifier for this SzfObject type, derived from its SzfObjectAttribute. + /// + public string TypeIdentifier { get; } /// - /// Abstract method for SzfObjects to parse their specific sections from the raw parsed structure. - /// This promotes separation of concerns by moving type-specific parsing logic into the SzfObject itself. + /// Common metadata for all SzfObjects. /// - /// A list of all parsed sections from the SZF file. - public abstract void ParseSections(List sections); + public SzfMetadata Metadata { get; set; } = new SzfMetadata(); /// - /// Virtual method for SzfObjects to generate their specific metadata fields. - /// Override this in derived classes to add custom metadata to the [Metadata] section. + /// Default constructor that reads the TypeIdentifier from the SzfObjectAttribute. /// - /// The SzfFieldWriter instance to write fields. - public virtual void GenerateMetadata(SzfFieldWriter writer) + protected SzfObject() { - // Default implementation does nothing, derived classes can override. + // Read the SzfObjectAttribute from the concrete class type + var attribute = GetType().GetCustomAttribute(); + 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(); } /// - /// Virtual method for SzfObjects to generate their specific content sections. - /// Override this in derived classes to add custom sections and fields. + /// Populates the object's metadata from a dictionary of parsed SzfFields. + /// This method is intended to be overridden by derived classes to handle their specific metadata. /// - /// The SzfFieldWriter instance to write fields and manage sections. - 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; - } - - /// - /// Parse SZF content and auto-detect the type - /// - public static SzfObject ParseFromSzf(string szfContent) - { - var parser = new SzfParser(); - return parser.Parse(szfContent); - } - - /// - /// Parse SZF content into a specific type - /// - public static T ParseFromSzf(string szfContent) where T : SzfObject - { - var parser = new SzfParser(); - return parser.Parse(szfContent); - } - - public virtual string ToSzfString() - { - var generator = new SzfGenerator(); - return generator.Generate(this); - } - + /// A dictionary of metadata fields. public virtual void PopulateFromMetadata(Dictionary metadata) { 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)) - 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) && - Guid.TryParse(guidField.Value?.ToString(), out var guid)) - Id = guid; + if (metadata.TryGetValue("GUID", out var guidField)) + { + 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)) - Author = authorField.Value?.ToString() ?? string.Empty; - - if (metadata.TryGetValue("Description", out var descField)) - Description = descField.Value?.ToString() ?? string.Empty; + Metadata.Author = authorField.Value?.ToString(); } - private static bool IsValidSemanticVersion(string version) + /// + /// 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. + /// + /// A list of parsed SzfSections. + public abstract void ParseSections(List sections); + + /// + /// 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. + /// + /// The SzfFieldWriter to use for writing. + public virtual void GenerateMetadata(SzfFieldWriter writer) { - return System.Text.RegularExpressions.Regex.IsMatch( - version, @"^\d+\.\d+\.\d+$"); - } -} + // Write standard metadata fields common to all SzfObjects + 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 -{ - Dataset, - CharacterTemplate, - Character, - Session + if (!string.IsNullOrEmpty(Metadata.Description)) + writer.WriteField("Description", "text-field", Metadata.Description); + if (!string.IsNullOrEmpty(Metadata.Author)) + writer.WriteField("Author", "text", Metadata.Author); + + writer.AppendLine(); // Corrected method call to end section with a newline + } + + /// + /// Generates the content sections of the Szf file using the provided writer. + /// This is an abstract method that must be implemented by derived classes. + /// + /// The SzfFieldWriter to use for writing. + public abstract void GenerateContent(SzfFieldWriter writer); + + /// + /// 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. + /// + /// A SzfValidationResult indicating any errors or warnings. + 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 diff --git a/SessionZero/Data/SzfObjectAttribute.cs b/SessionZero/Data/SzfObjectAttribute.cs index 8bb9f8e..4e7788c 100644 --- a/SessionZero/Data/SzfObjectAttribute.cs +++ b/SessionZero/Data/SzfObjectAttribute.cs @@ -1,18 +1,27 @@ -// SzfObjectAttribute.cs - using System; namespace SessionZero.Data; -[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] -public class SzfObjectAttribute : Attribute +[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class SzfObjectAttribute : Attribute { - public SzfObjectType ObjectType { get; } - 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. + /// + public string TypeIdentifier { get; } - public SzfObjectAttribute(SzfObjectType objectType, string typeString) + /// + /// Initializes a new instance of the class. + /// + /// The unique string identifier for the SzfObject type. + public SzfObjectAttribute(string typeIdentifier) { - ObjectType = objectType; - TypeString = typeString; + if (string.IsNullOrWhiteSpace(typeIdentifier)) + { + throw new ArgumentException("Type identifier cannot be null or whitespace.", nameof(typeIdentifier)); + } + + TypeIdentifier = typeIdentifier; } } \ No newline at end of file diff --git a/SessionZero/Data/SzfParser.cs b/SessionZero/Data/SzfParser.cs index 8b8eaba..8acc35f 100644 --- a/SessionZero/Data/SzfParser.cs +++ b/SessionZero/Data/SzfParser.cs @@ -1,336 +1,350 @@ -using System.Reflection; -using System.Text.RegularExpressions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; // Required for Reflection namespace SessionZero.Data; public class SzfParser { - private static readonly Regex HeaderRegex = new(@"^!(\w+):\s*(.+)$", RegexOptions.Compiled); - private static readonly Regex SectionRegex = new(@"^\[([^\]]+)\]$", RegexOptions.Compiled); - private static readonly Regex FieldRegex = new(@"^(\w+)\s*\(([^)]+)\)\s*=\s*(.*)$", RegexOptions.Compiled); + // A static map to cache SzfObject types by their TypeIdentifier + private static readonly Dictionary _szfObjectTypeMap = + new Dictionary(StringComparer.OrdinalIgnoreCase); - // Static dictionaries to store type mappings, initialized once - private static readonly Dictionary s_objectTypeToClassMap; - private static readonly Dictionary s_stringToObjectTypeMap; + private static readonly object _typeMapLock = new object(); // For thread safety during map population - static SzfParser() + public SzfParser() { - s_objectTypeToClassMap = new Dictionary(); - s_stringToObjectTypeMap = new Dictionary(); + // Ensure the type map is populated when a parser instance is created + EnsureTypeMapPopulated(); + } - // Discover types with SzfObjectAttribute in the current assembly - var szfObjectTypes = Assembly.GetExecutingAssembly().GetTypes() - .Where(type => typeof(SzfObject).IsAssignableFrom(type) && !type.IsAbstract) - .Select(type => new - { - Type = type, - Attribute = type.GetCustomAttribute() - }) - .Where(x => x.Attribute != null); + /// + /// Ensures the internal map of SzfObject types and their string identifiers is populated. + /// This uses reflection to find all concrete SzfObject classes with the SzfObjectAttribute. + /// + private static void EnsureTypeMapPopulated() + { + if (_szfObjectTypeMap.Any()) return; // Already populated - foreach (var item in szfObjectTypes) + lock (_typeMapLock) { - s_objectTypeToClassMap[item.Attribute!.ObjectType] = item.Type; - s_stringToObjectTypeMap[item.Attribute.TypeString.ToLower()] = item.Attribute.ObjectType; + if (_szfObjectTypeMap.Any()) return; // Double-check after acquiring lock + + // 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(); + if (attribute != null) + { + // Add to the map using the TypeIdentifier from the attribute + _szfObjectTypeMap[attribute.TypeIdentifier] = type; + } + } } } /// - /// Parse a SZF file content and return the appropriate SzfObject type + /// Parses an SZF content string into a specific SzfObject type. /// + /// The expected type of SzfObject. + /// The SZF content string. + /// An instance of the parsed SzfObject. + /// Thrown if parsing fails or types mismatch. public T Parse(string szfContent) where T : SzfObject { - if (string.IsNullOrWhiteSpace(szfContent)) - throw new SzfParseException("SZF content cannot be empty"); + var parseResult = + ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList()); - var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .Where(line => !string.IsNullOrEmpty(line)) - .ToList(); + // Validate that the parsed type identifier matches the expected type T + ValidateExpectedType(parseResult.ObjectTypeIdentifier); - var parseResult = ParseSzfStructure(lines); - - // Validate that the parsed type matches the requested type - ValidateObjectType(parseResult.ObjectType); - - var obj = CreateInstance(parseResult.ObjectType); + // Create an instance of the specific SzfObject type using the parsed identifier + var obj = CreateInstance(parseResult.ObjectTypeIdentifier); PopulateSzfObject(obj, parseResult); - return obj; } /// - /// 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. /// + /// The SZF content string. + /// An instance of the parsed SzfObject. + /// Thrown if parsing fails. public SzfObject Parse(string szfContent) { - if (string.IsNullOrWhiteSpace(szfContent)) - throw new SzfParseException("SZF content cannot be empty"); + var parseResult = + ParseSzfStructure(szfContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).ToList()); - var lines = szfContent.Split('\n', StringSplitOptions.RemoveEmptyEntries) - .Select(line => line.Trim()) - .Where(line => !string.IsNullOrEmpty(line)) - .ToList(); - - var parseResult = ParseSzfStructure(lines); - - var obj = CreateInstanceFromType(parseResult.ObjectType); + // Create an instance of the specific SzfObject type using the parsed identifier + var obj = CreateInstanceFromTypeIdentifier(parseResult.ObjectTypeIdentifier); PopulateSzfObject(obj, parseResult); - return obj; } /// - /// Create an instance of the appropriate type based on SzfObjectType + /// Creates an instance of the specified SzfObject type, ensuring it's assignable to T. /// - private T CreateInstance(SzfObjectType objectType) where T : SzfObject + /// The type to cast the created instance to. + /// The string identifier of the SzfObject type. + /// A new instance of the SzfObject. + /// Thrown if the type is unknown or cannot be instantiated. + private T CreateInstance(string typeIdentifier) where T : SzfObject { - var instance = CreateInstanceFromType(objectType); - if (instance is T typedInstance) - return typedInstance; - - throw new SzfParseException($"Cannot cast {instance.GetType().Name} to {typeof(T).Name}"); - } - - /// - /// Create an instance based on the object type using the dynamically built map - /// - private SzfObject CreateInstanceFromType(SzfObjectType objectType) - { - if (s_objectTypeToClassMap.TryGetValue(objectType, out var type)) + EnsureTypeMapPopulated(); + if (!_szfObjectTypeMap.TryGetValue(typeIdentifier, 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)!; } /// - /// Parse SZF content into a generic structure before type-specific processing + /// Creates a generic SzfObject instance from its string identifier. /// - private SzfParseResult ParseSzfStructure(List lines) + /// The string identifier of the SzfObject type. + /// A new instance of SzfObject. + /// Thrown if the type is unknown or cannot be instantiated. + 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)!; + } + + /// + /// Parses the raw SZF content lines into a structured SzfParseResult. + /// + public SzfParseResult ParseSzfStructure(List lines) { var result = new SzfParseResult(); - var currentSection = new List(); - var currentSectionName = string.Empty; + var currentSectionLines = new List(); + string? currentSectionName = null; foreach (var line in lines) { - // Skip comments - if (line.StartsWith("//") || line.StartsWith("#")) - continue; + var trimmedLine = line.Trim(); - // Parse headers (!type:, !schema:) - var headerMatch = HeaderRegex.Match(line); - if (headerMatch.Success) + // Handle headers (!type:, !schema:) + if (trimmedLine.StartsWith("!type:", StringComparison.OrdinalIgnoreCase)) { - var headerName = headerMatch.Groups[1].Value.ToLower(); - var headerValue = headerMatch.Groups[2].Value.Trim(); + result.ObjectTypeIdentifier = trimmedLine.Substring("!type:".Length).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.ObjectType = ParseObjectType(headerValue); - break; - case "schema": - result.SchemaVersion = headerValue; - break; - default: - result.Headers[headerName] = headerValue; - break; + result.Headers[parts[0].TrimStart('!').Trim()] = parts[1].Trim(); } continue; } - // Parse section headers [Section Name] - var sectionMatch = SectionRegex.Match(line); - if (sectionMatch.Success) + // Handle section headers ([Section Name]) + if (trimmedLine.StartsWith("[") && trimmedLine.EndsWith("]")) { - // Process previous section if exists - if (!string.IsNullOrEmpty(currentSectionName)) + // If there's an active section, process it before starting a new one + if (currentSectionName != null) { - ProcessSection(result, currentSectionName, currentSection); + ProcessSection(result, currentSectionName, currentSectionLines); + currentSectionLines.Clear(); } - // Start new section - currentSectionName = sectionMatch.Groups[1].Value.Trim(); - currentSection.Clear(); + currentSectionName = trimmedLine.Substring(1, trimmedLine.Length - 2).Trim(); continue; } - // Add line to current section - if (!string.IsNullOrEmpty(currentSectionName)) + // Handle empty lines (ignored within sections for parsing structure) + 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 - if (!string.IsNullOrEmpty(currentSectionName)) + // Process the last section if any + if (currentSectionName != null) { - ProcessSection(result, currentSectionName, currentSection); + ProcessSection(result, currentSectionName, currentSectionLines); } return result; } /// - /// Process an individual section and its fields + /// Processes lines belonging to a single section, parsing fields and adding to the result. /// private void ProcessSection(SzfParseResult result, string sectionName, List sectionLines) { - var section = new SzfSection { Name = sectionName }; + var szfSection = new SzfSection { Name = sectionName }; foreach (var line in sectionLines) { - var fieldMatch = FieldRegex.Match(line); - if (fieldMatch.Success) + // Expected format: Name (type) = Value + var parts = line.Split(new[] { '=' }, 2); + if (parts.Length == 2) { - var field = new SzfField - { - Name = fieldMatch.Groups[1].Value.Trim(), - Type = fieldMatch.Groups[2].Value.Trim(), - Value = ParseFieldValue(fieldMatch.Groups[3].Value.Trim(), fieldMatch.Groups[2].Value.Trim()) - }; + var nameAndTypePart = parts[0].Trim(); + var valueString = parts[1].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); } /// - /// Parse field value based on its declared type + /// Parses a field value string into an appropriate object type based on the field type. /// private object? ParseFieldValue(string valueString, string fieldType) { - if (string.IsNullOrEmpty(valueString)) - return null; - - return fieldType.ToLower() switch + return fieldType.ToLowerInvariant() switch { - "text" => valueString, - "text-field" => valueString, "number" => ParseNumber(valueString), "bool" => ParseBoolean(valueString), - "calculated" => valueString, // Store formula as string - "system" => valueString, - "dataset-reference" => valueString, - "dataset-type" => valueString, - "dataset-reference-multiple" => valueString, - "dataset-type-multiple" => valueString, - "group" => ParseBoolean(valueString), // Groups are typically true/false - _ => valueString // Default to string + "text-field" => valueString, // Text-fields are multi-line, keep as string + _ => valueString // Default to string for "text" and unknown types }; } - /// - /// Parse number values (int or double) - /// private object ParseNumber(string value) { - if (int.TryParse(value, out var intResult)) - return intResult; - - if (double.TryParse(value, out var doubleResult)) - return doubleResult; - - throw new SzfParseException($"Invalid number format: {value}"); + if (int.TryParse(value, out var i)) return i; + if (double.TryParse(value, out var d)) return d; + return 0; // Default or throw error } - /// - /// Parse boolean values - /// private bool ParseBoolean(string value) { - return value.ToLower() switch - { - "true" => true, - "false" => false, - "1" => true, - "0" => false, - _ => throw new SzfParseException($"Invalid boolean format: {value}") - }; + return bool.TryParse(value, out var b) && b; } /// - /// Parse object type from string using the dynamically built map + /// Validates that the parsed object type identifier matches the expected type T. /// - private SzfObjectType ParseObjectType(string typeString) + /// The expected SzfObject type. + /// The string identifier parsed from the SZF content. + /// Thrown if the types do not match. + /// Thrown if T is not properly decorated with SzfObjectAttribute. + private void ValidateExpectedType(string parsedTypeIdentifier) where T : SzfObject { - if (s_stringToObjectTypeMap.TryGetValue(typeString.ToLower(), out var objectType)) + var targetAttribute = typeof(T).GetCustomAttribute(); + 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}"); - } - - /// - /// Validate that the parsed type matches the requested generic type using the attribute - /// - private void ValidateObjectType(SzfObjectType parsedType) where T : SzfObject - { - var targetType = typeof(T); - var attribute = targetType.GetCustomAttribute(); - - if (attribute == null) + if (!string.Equals(targetAttribute.TypeIdentifier, parsedTypeIdentifier, StringComparison.OrdinalIgnoreCase)) { - throw new SzfParseException($"Type {targetType.Name} does not have SzfObjectAttribute."); - } - - if (parsedType != attribute.ObjectType) - { - throw new SzfParseException($"Type mismatch: Expected {attribute.ObjectType}, found {parsedType}"); + throw new SzfParseException( + $"Mismatched SzfObject type. Expected '{targetAttribute.TypeIdentifier}' but found '{parsedTypeIdentifier}'."); } } /// - /// Populate the SzfObject with parsed data + /// Populates an SzfObject instance with the data from a parsed SzfParseResult. /// - private void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult) + public void PopulateSzfObject(SzfObject obj, SzfParseResult parseResult) { - // Set schema version - obj.SchemaVersion = parseResult.SchemaVersion; + // Extract metadata fields into a dictionary for PopulateFromMetadata + var metadataFields = parseResult.Sections + .FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))? + .Fields.ToDictionary(f => f.Name, f => f, StringComparer.OrdinalIgnoreCase) + ?? new Dictionary(); - // Find and process Metadata section - var metadataSection = - parseResult.Sections.FirstOrDefault(s => s.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase)); - if (metadataSection != null) - { - var metadata = metadataSection.Fields.ToDictionary(f => f.Name, f => f); - obj.PopulateFromMetadata(metadata); - } + obj.PopulateFromMetadata(metadataFields); - // 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); } } /// -/// Internal class to hold parsed SZF structure +/// Represents the parsed structure of an SZF file. /// -public class SzfParseResult // Changed from internal to public for broader accessibility if needed +public class SzfParseResult { - public SzfObjectType ObjectType { get; set; } - public string SchemaVersion { get; set; } = "1.0.0"; - public Dictionary Headers { get; set; } = new(); - public List Sections { get; set; } = new(); + // Changed from SzfObjectType to string + public string ObjectTypeIdentifier { get; set; } = string.Empty; + public string SchemaVersion { get; set; } = string.Empty; + public Dictionary Headers { get; set; } = new Dictionary(); + public List Sections { get; set; } = new List(); } /// -/// Internal class representing a parsed section +/// Represents a section within an SZF file. /// -public class SzfSection // Changed from internal to public for broader accessibility if needed +public class SzfSection { public string Name { get; set; } = string.Empty; - public List Fields { get; set; } = new(); + public List Fields { get; set; } = new List(); } /// -/// Exception thrown during SZF parsing +/// Exception thrown during SZF parsing. /// public class SzfParseException : Exception { diff --git a/SessionZero/Pages/SzfParseTest.razor b/SessionZero/Pages/SzfParseTest.razor index ce3ecd9..6c0b787 100644 --- a/SessionZero/Pages/SzfParseTest.razor +++ b/SessionZero/Pages/SzfParseTest.razor @@ -25,41 +25,44 @@ { - - - - - - - + + @if (!string.IsNullOrEmpty(parsedSchemaVersion)) + { + + } + + + + +
Object Type@parsedObject.ObjectType
Schema Version@parsedObject.SchemaVersion
Name@parsedObject.Name
Version@parsedObject.Version
ID@parsedObject.Id
Author@parsedObject.Author
Description@parsedObject.Description
Object Type@parsedObject.TypeIdentifier
Schema Version@parsedSchemaVersion
Name@parsedObject.Metadata.Name
Version@parsedObject.Metadata.Version
ID@parsedObject.Metadata.Guid
Author@parsedObject.Metadata.Author
Description@parsedObject.Metadata.Description
- @if (parsedObject.ExtendedMetadata.Any()) - { -

Extended Metadata

- - - - - - - - - @foreach (var meta in parsedObject.ExtendedMetadata) - { - - - - - } - -
KeyValue
@meta.Key@meta.Value
- } - @* Display Dataset specific data *@ @if (parsedObject is Dataset dataset) { + @if (dataset.Metadata.Any()) // Display Dataset's specific metadata + { +

Dataset Specific Metadata

+ + + + + + + + + @foreach (var meta in dataset.Metadata) + { + + + + + } + +
KeyValue
@meta.Key@meta.Value
+ } + @if (dataset.Entries.Any()) {

Dataset Entries

@@ -128,6 +131,19 @@ } } } + + @* Display Character specific data *@ + @if (parsedObject is Character character) + { + @if (character.CharacterSections.Any()) + { +

Character Sections

+ @foreach (var section in character.CharacterSections) + { + @RenderCharacterSection(section) + } + } + } } @if (errorMessages.Any()) { @@ -210,6 +226,7 @@ @code { private string szfCode = string.Empty; private SzfObject? parsedObject; + private string? parsedSchemaVersion; // To store the schema version from parsing private string generatedSzfOutput = string.Empty; // New field for generated output private List logMessages = new List(); private List errorMessages = new List(); @@ -229,10 +246,17 @@ 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($"Object Type: {parsedObject.ObjectType}"); - Log($"Schema Version: {parsedObject.SchemaVersion}"); + Log($"Object Type: {parsedObject.TypeIdentifier}"); + Log($"Schema Version: {parsedSchemaVersion}"); } catch (Exception ex) { @@ -254,8 +278,8 @@ Log("Starting SZF generation..."); try { - // Use the ToSzfString() method on the parsed object, which uses SzfGenerator internally - generatedSzfOutput = parsedObject.ToSzfString(); + // Use the SzfGenerator to generate the string + generatedSzfOutput = SzfGenerator.Generate(parsedObject); Log("SZF content generated successfully."); } catch (Exception ex) @@ -271,6 +295,7 @@ logMessages.Clear(); errorMessages.Clear(); parsedObject = null; + parsedSchemaVersion = null; // Clear parsed schema version generatedSzfOutput = string.Empty; } @@ -309,7 +334,7 @@ } ; - // New helper for rendering CharacterTemplate sections + // Helper for rendering CharacterTemplate sections private RenderFragment RenderTemplateSection(TemplateSection templateSection) =>@
@templateSection.Name @if (templateSection.Fields.Any()) @@ -331,4 +356,27 @@ @RenderTemplateSection(subSection) }
; + + // New helper for rendering Character sections + private RenderFragment RenderCharacterSection(CharacterSection characterSection) =>@
+

@characterSection.Name

+ @if (characterSection.Fields.Any()) + { + + + @foreach (var field in characterSection.Fields) + { + + + + + } + +
@field.Name@field.Value
+ } + @foreach (var subSection in characterSection.Subsections) + { + @RenderCharacterSection(subSection) + } +
; } \ No newline at end of file diff --git a/test_character.szf b/test_character.szf new file mode 100644 index 0000000..f586ce7 --- /dev/null +++ b/test_character.szf @@ -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. \ No newline at end of file diff --git a/test_dataset.szf b/test_dataset.szf index 4d05f69..e69de29 100644 --- a/test_dataset.szf +++ b/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 \ No newline at end of file diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..dfdbba6 --- /dev/null +++ b/todo.md @@ -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 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`