diff --git a/5e_template.szf b/5e_template.szf new file mode 100644 index 0000000..1ea3846 --- /dev/null +++ b/5e_template.szf @@ -0,0 +1,132 @@ +!type: character_template +!schema: 1.0.0 + +[Metadata] +Name (text) = D&D 5e Character Sheet +Version (text) = 1.0.0 +GUID (text) = 5e0c7a12-d4b9-4f3e-9c87-a1b2c3d4e5f6 +Description (text-field) = A comprehensive character sheet template for Dungeons & Dragons 5th Edition, covering core stats, skills, combat, and general information. +Author (text) = AI Assistant +GameSystem (text) = Dungeons & Dragons 5th Edition + +[Required Datasets] +DnD5eRaces (dataset-reference) = D&D 5e Races|e1a2b3c4-d5e6-7f8a-9b0c-1d2e3f4a5b6c|1.0.0 +DnD5eClasses (dataset-reference) = D&D 5e Classes|f1e2d3c4-b5a6-7980-1234-56789abcdef0|1.0.0 +DnD5eSpells (dataset-reference) = D&D 5e Spells|a1b2c3d4-e5f6-7890-1234-567890abcdef|1.0.0 +DnD5eEquipment (dataset-reference) = D&D 5e Equipment|b1c2d3e4-f5a6-7890-1234-567890fedcba|1.0.0 + +[Section: Character Information] +CharacterName (text) = +PlayerName (text) = +Race (dataset-reference) = DnD5eRaces +Class (dataset-reference) = DnD5eClasses +Level (number) = 1 +Background (text) = +Alignment (text) = +ExperiencePoints (number) = 0 +ProficiencyBonus (calculated) = 2 + floor((Level - 1) / 4) + +[Section: Ability Scores] +Strength (number) = 10 +Dexterity (number) = 10 +Constitution (number) = 10 +Intelligence (number) = 10 +Wisdom (number) = 10 +Charisma (number) = 10 + +[Section.Ability Scores.Modifiers] +StrengthMod (calculated) = floor((Strength - 10) / 2) +DexterityMod (calculated) = floor((Dexterity - 10) / 2) +ConstitutionMod (calculated) = floor((Constitution - 10) / 2) +IntelligenceMod (calculated) = floor((Intelligence - 10) / 2) +WisdomMod (calculated) = floor((Wisdom - 10) / 2) +CharismaMod (calculated) = floor((Charisma - 10) / 2) + +[Section: Saving Throws] +StrengthSave (number) = 0 +DexteritySave (number) = 0 +ConstitutionSave (number) = 0 +IntelligenceSave (number) = 0 +WisdomSave (number) = 0 +CharismaSave (number) = 0 + +[Section: Skills] +Acrobatics (number) = 0 +AnimalHandling (number) = 0 +Arcana (number) = 0 +Athletics (number) = 0 +Deception (number) = 0 +History (number) = 0 +Insight (number) = 0 +Intimidation (number) = 0 +Investigation (number) = 0 +Medicine (number) = 0 +Nature (number) = 0 +Perception (number) = 0 +Performance (number) = 0 +Persuasion (number) = 0 +Religion (number) = 0 +SleightOfHand (number) = 0 +Stealth (number) = 0 +Survival (number) = 0 + +[Section: Combat] +ArmorClass (number) = 0 +Initiative (calculated) = DexterityMod +Speed (number) = 30 +HitPointsMax (number) = 0 +HitPointsCurrent (number) = 0 +TemporaryHitPoints (number) = 0 +HitDice (text) = +DeathSavesSuccess (number) = 0 +DeathSavesFailure (number) = 0 + +[Section: Senses] +PassivePerception (calculated) = 10 + Perception +Darkvision (text) = + +[Section: Feats & Traits] +Feats (text-field) = +RacialTraits (text-field) = +ClassFeatures (text-field) = +OtherProficienciesAndLanguages (text-field) = + +[Section: Equipment] +CopperPieces (number) = 0 +SilverPieces (number) = 0 +ElectrumPieces (number) = 0 +GoldPieces (number) = 0 +PlatinumPieces (number) = 0 +Inventory (text-field) = + +[Section: Spellcasting] +SpellcastingAbility (text) = +SpellSaveDC (number) = 0 +SpellAttackBonus (number) = 0 + +[Section.Spellcasting.Cantrips] +Cantrips (dataset-type-multiple) = DnD5eSpells + +[Section.Spellcasting.Level1Spells] +Level1SlotsTotal (number) = 0 +Level1SlotsUsed (number) = 0 +Level1Spells (dataset-type-multiple) = DnD5eSpells + +[Section.Spellcasting.Level2Spells] +Level2SlotsTotal (number) = 0 +Level2SlotsUsed (number) = 0 +Level2Spells (dataset-type-multiple) = DnD5eSpells + +[Section.Spellcasting.Level3Spells] +Level3SlotsTotal (number) = 0 +Level3SlotsUsed (number) = 0 +Level3Spells (dataset-type-multiple) = DnD5eSpells + +[Section: Character Details] +PersonalityTraits (text-field) = +Ideals (text-field) = +Bonds (text-field) = +Flaws (text-field) = +Backstory (text-field) = +AlliesAndOrganizations (text-field) = +AdditionalNotes (text-field) = \ No newline at end of file diff --git a/SessionZero/Data/CharacterTemplate.cs b/SessionZero/Data/CharacterTemplate.cs index 7c5e8aa..ba0a59c 100644 --- a/SessionZero/Data/CharacterTemplate.cs +++ b/SessionZero/Data/CharacterTemplate.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; @@ -9,7 +8,7 @@ public class CharacterTemplate : SzfObject { public string GameSystem { get; set; } = string.Empty; public List RequiredDatasets { get; set; } = new(); - public List Sections { get; set; } = new(); // Top-level sections + public List Sections { get; set; } = new(); public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate; @@ -23,105 +22,81 @@ public class CharacterTemplate : SzfObject public override void ParseSections(List sections) { - // Parse "Required Datasets" section + // Parse [Required Datasets] section var requiredDatasetsSection = sections.FirstOrDefault(s => s.Name.Equals("Required Datasets", StringComparison.OrdinalIgnoreCase)); if (requiredDatasetsSection != null) { foreach (var field in requiredDatasetsSection.Fields) { - var datasetReference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty); - if (datasetReference != null) + var reference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty); + if (reference != null) { RequiredDatasets.Add(new RequiredDatasetReference { Alias = field.Name, - Reference = datasetReference + Reference = reference }); } } } - // Parse and build the hierarchical template sections - // Use a map to quickly find existing sections by their full path for nesting - var sectionPathMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - + // Parse [Section: ...] and [Section....] sections foreach (var szfSection in sections) { - // Only process sections that start with "Section:" - if (!szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase)) continue; - - // Normalize the section name (e.g., "Section:Ability Scores.Modifiers" to "Ability Scores.Modifiers") - var rawSectionPath = szfSection.Name.Substring("Section:".Length).Trim(); - var nameParts = rawSectionPath.Split('.'); - - TemplateSection? currentParentSection = null; - List currentSectionList = Sections; // Start from the top-level Sections list - - string accumulatedPath = "Section"; // Used for dictionary key to ensure uniqueness and hierarchy - - for (int i = 0; i < nameParts.Length; i++) + if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase)) { - var sectionNamePart = nameParts[i]; - accumulatedPath += "." + sectionNamePart; // Build the full path for the map key - - TemplateSection? targetSection = null; - - // Check if this specific section (at this level of hierarchy) already exists - if (sectionPathMap.TryGetValue(accumulatedPath, out var existingSection)) + var templateSection = new TemplateSection { - targetSection = existingSection; - } - else - { - // Create a new section if it doesn't exist - var newSection = new TemplateSection { Name = sectionNamePart }; - sectionPathMap[accumulatedPath] = newSection; + Name = szfSection.Name.Replace("Section:", "").Trim() + }; - if (currentParentSection == null) - { - // This is a top-level section - currentSectionList.Add(newSection); - } - else - { - // This is a subsection of the current parent - currentParentSection.Subsections.Add(newSection); - } - - targetSection = newSection; - } - - currentParentSection = targetSection; // Move down the hierarchy - currentSectionList = - targetSection.Subsections; // Next iteration will look in subsections of this section - } - - // Once we've traversed/created the full path, currentParentSection points to the innermost section. - // Now populate its fields from the SzfSection. - if (currentParentSection != null) - { foreach (var field in szfSection.Fields) { - currentParentSection.Fields.Add(ConvertToTemplateField(field)); + templateSection.Fields.Add(ConvertToTemplateField(field)); + } + + Sections.Add(templateSection); + } + else if (szfSection.Name.StartsWith("Section.", StringComparison.OrdinalIgnoreCase)) + { + // Handle subsections by finding their parent + var parts = szfSection.Name.Split('.'); + if (parts.Length >= 2) + { + var parentSectionName = parts[0] + ":" + parts[1].Trim(); // Reconstruct parent name + var parentSection = Sections.FirstOrDefault(s => + s.Name.Equals(parentSectionName.Replace("Section:", "").Trim(), + StringComparison.OrdinalIgnoreCase)); + if (parentSection != null) + { + var subSection = new TemplateSection + { + Name = string.Join(".", parts.Skip(2)).Trim() // Name for the subsection + }; + + foreach (var field in szfSection.Fields) + { + subSection.Fields.Add(ConvertToTemplateField(field)); + } + + parentSection.Subsections.Add(subSection); + } } } } } - private TemplateField ConvertToTemplateField(SzfField szfField) { return new TemplateField { Name = szfField.Name, - Type = ParseDatasetFieldType(szfField.Type), // Reusing DatasetFieldType for consistency - Value = szfField.Value, + Type = ParseDatasetFieldType(szfField.Type), + Value = szfField.Value }; } - // Helper method to parse DatasetFieldType, can be shared or defined here. - // Copying from Dataset.cs for now, consider a common utility. private DatasetFieldType ParseDatasetFieldType(string typeString) { return typeString.ToLower() switch @@ -130,26 +105,86 @@ public class CharacterTemplate : SzfObject "text-field" => DatasetFieldType.TextField, "number" => DatasetFieldType.Number, "bool" => DatasetFieldType.Boolean, - "group" => DatasetFieldType.Group, "calculated" => DatasetFieldType.Calculated, "system" => DatasetFieldType.System, "dataset-reference" => DatasetFieldType.DatasetReference, "dataset-type" => DatasetFieldType.DatasetType, "dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple, "dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple, - _ => DatasetFieldType.Text + "group" => DatasetFieldType.Group, + _ => DatasetFieldType.Text // Default to text if unknown }; } + public override void GenerateMetadata(SzfFieldWriter writer) + { + // Call base to ensure common metadata is generated first if base had any specific generation logic + base.GenerateMetadata(writer); + + writer.WriteField("GameSystem", "text", GameSystem); + } + + public override void GenerateContent(SzfFieldWriter writer) + { + // Generate [Required Datasets] section + if (RequiredDatasets.Any() || writer.Options.IncludeEmptySections) + { + writer.AppendSectionHeader("Required Datasets"); + foreach (var reqDataset in RequiredDatasets) + { + writer.WriteField(reqDataset.Alias, "dataset-reference", reqDataset.Reference?.ToString()); + } + + writer.AppendLine(); + } + + // Generate Template Sections + foreach (var section in Sections) + { + GenerateTemplateSection(writer, section, "Section"); + writer.AppendLine(); + } + } + + private void GenerateTemplateSection(SzfFieldWriter writer, TemplateSection section, string parentPath) + { + var currentPath = $"{parentPath}: {section.Name}"; + writer.AppendSectionHeader(currentPath); + + foreach (var field in section.Fields) + { + writer.WriteField(field.Name, field.Type, field.Value); + } + + foreach (var subSection in section.Subsections) + { + GenerateTemplateSubSection(writer, subSection, $"Section.{section.Name}"); + } + } + + private void GenerateTemplateSubSection(SzfFieldWriter writer, TemplateSection subSection, string parentPath) + { + var currentPath = $"{parentPath}.{subSection.Name}"; + writer.AppendSectionHeader(currentPath); + + foreach (var field in subSection.Fields) + { + writer.WriteField(field.Name, field.Type, field.Value); + } + + foreach (var nestedSubSection in subSection.Subsections) + { + // Handle deeper nesting if needed, recursively calling this method + GenerateTemplateSubSection(writer, nestedSubSection, currentPath); + } + } + public override SzfValidationResult Validate() { - var result = base.Validate(); + var result = base.Validate(); // Run base validation first - // if (string.IsNullOrWhiteSpace(GameSystem)) - // result.AddError("GameSystem is required for CharacterTemplate"); - - // Further validation for required datasets and template sections can be added here - // e.g., checking for duplicate section names or missing required fields. + if (string.IsNullOrWhiteSpace(GameSystem)) + result.AddError("GameSystem is required for CharacterTemplate"); return result; } @@ -165,7 +200,7 @@ public class TemplateSection { public string Name { get; set; } = string.Empty; public List Fields { get; set; } = new(); - public List Subsections { get; set; } = new(); + public List Subsections { get; set; } = new(); // For nested sections } public class TemplateField diff --git a/SessionZero/Data/Dataset.cs b/SessionZero/Data/Dataset.cs index 7b97002..dc0eaf7 100644 --- a/SessionZero/Data/Dataset.cs +++ b/SessionZero/Data/Dataset.cs @@ -1,118 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + namespace SessionZero.Data; [SzfObject(SzfObjectType.Dataset, "dataset")] public class Dataset : SzfObject { public string DatasetType { get; set; } = string.Empty; - public string ImageUrl { get; set; } = string.Empty; - - // Collection of entries within this dataset - public List Entries { get; set; } = []; - - // Additional metadata fields for .szf compatibility - public Dictionary Metadata { get; set; } = new(); + 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) { - // Call base method to populate common fields base.PopulateFromMetadata(metadata); - // Populate dataset-specific fields if (metadata.TryGetValue("Type", out var typeField)) DatasetType = typeField.Value?.ToString() ?? string.Empty; - if (metadata.TryGetValue("ImageUrl", out var imageField)) - ImageUrl = imageField.Value?.ToString() ?? string.Empty; + if (metadata.TryGetValue("ImageUrl", out var imageUrlField)) + ImageUrl = imageUrlField.Value?.ToString() ?? string.Empty; + + // Store any other metadata fields not explicitly mapped + foreach (var kvp in metadata) + { + if (!new[] { "Name", "Version", "GUID", "Description", "Author", "Type", "ImageUrl" } + .Contains(kvp.Key, StringComparer.OrdinalIgnoreCase)) + { + Metadata[kvp.Key] = kvp.Value.Value!; + } + } } - /// - /// Parses sections specific to the Dataset object, such as entries and their subsections. - /// This method is called by the SzfParser to populate Dataset-specific data. - /// - /// A list of all parsed sections from the SZF file. public override void ParseSections(List sections) { - // Dictionary to hold DatasetEntry objects by their name for easy lookup - var entriesByName = new Dictionary(StringComparer.OrdinalIgnoreCase); + var currentEntry = (DatasetEntry?)null; - // First pass: Identify and populate top-level DatasetEntry objects foreach (var szfSection in sections) { if (szfSection.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase)) { - var entryName = szfSection.Name.Substring("Entry:".Length).Trim(); - if (string.IsNullOrEmpty(entryName)) continue; + currentEntry = new DatasetEntry + { + Name = szfSection.Name.Replace("Entry:", "").Trim() + }; - var newEntry = new DatasetEntry { Name = entryName }; foreach (var field in szfSection.Fields) { - newEntry.Fields.Add(ConvertToDatasetField(field)); + currentEntry.Fields.Add(ConvertToDatasetField(field)); } - Entries.Add(newEntry); - entriesByName[entryName] = newEntry; + Entries.Add(currentEntry); } - } - - // Second pass: Populate sections and subsections for each entry - foreach (var szfSection in sections) - { - // We are looking for sections like "Entry.EntryName.SectionName" or "Entry.EntryName.SectionName.SubSectionName" - if (!szfSection.Name.StartsWith("Entry.", StringComparison.OrdinalIgnoreCase)) continue; - - // Split the section name by '.' to identify parts - var nameParts = szfSection.Name.Split('.'); - - // Ensure it's a section of an entry (e.g., "Entry.EntryName.Section") - if (nameParts.Length < 3 || !nameParts[0].Equals("Entry", StringComparison.OrdinalIgnoreCase)) continue; - - var entryName = nameParts[1]; - if (!entriesByName.TryGetValue(entryName, out var targetEntry)) continue; // Entry not found, skip - - // We need to build the hierarchy of DatasetSection objects - DatasetSection? currentParentSection = null; - List currentSectionList = targetEntry.Sections; - - for (int i = 2; i < nameParts.Length; i++) // Start from the first section name after "Entry.EntryName" + else if (szfSection.Name.StartsWith("Entry.", StringComparison.OrdinalIgnoreCase) && currentEntry != null) { - var sectionNamePart = nameParts[i]; - var existingSection = currentSectionList.FirstOrDefault(s => - s.Name.Equals(sectionNamePart, StringComparison.OrdinalIgnoreCase)); + // Handle subsections by finding their parent entry + var parts = szfSection.Name.Split('.'); + if (parts.Length >= 2 && parts[0].Equals("Entry", StringComparison.OrdinalIgnoreCase) && + parts[1].Equals(currentEntry.Name, StringComparison.OrdinalIgnoreCase)) + { + var subSection = new DatasetSection + { + Name = string.Join(".", parts.Skip(2)).Trim() // Name for the subsection + }; - if (existingSection == null) - { - // Create new section - var newSection = new DatasetSection { Name = sectionNamePart }; - currentSectionList.Add(newSection); - currentParentSection = newSection; - } - else - { - currentParentSection = existingSection; - } - - // If this is the last part of the section name, populate its fields - if (i == nameParts.Length - 1) - { foreach (var field in szfSection.Fields) { - currentParentSection.Fields.Add(ConvertToDatasetField(field)); + subSection.Fields.Add(ConvertToDatasetField(field)); } - } - // Move to the next level down in the hierarchy - currentSectionList = currentParentSection.Subsections; + currentEntry.Sections.Add(subSection); + } } } } - /// - /// Convert SzfField to DatasetField - /// private DatasetField ConvertToDatasetField(SzfField szfField) { + // You would typically parse more properties here like Description, IsRequired etc. + // For simplicity, only Name, Type and Value are mapped for now. return new DatasetField { Name = szfField.Name, @@ -121,9 +90,6 @@ public class Dataset : SzfObject }; } - /// - /// Parse DatasetFieldType from string - /// private DatasetFieldType ParseDatasetFieldType(string typeString) { return typeString.ToLower() switch @@ -132,38 +98,78 @@ public class Dataset : SzfObject "text-field" => DatasetFieldType.TextField, "number" => DatasetFieldType.Number, "bool" => DatasetFieldType.Boolean, - "group" => DatasetFieldType.Group, "calculated" => DatasetFieldType.Calculated, "system" => DatasetFieldType.System, "dataset-reference" => DatasetFieldType.DatasetReference, "dataset-type" => DatasetFieldType.DatasetType, "dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple, "dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple, - _ => DatasetFieldType.Text + "group" => DatasetFieldType.Group, + _ => DatasetFieldType.Text // Default to text if unknown }; } + public override void GenerateMetadata(SzfFieldWriter writer) + { + // Call base to ensure common metadata is generated first if base had any specific generation logic + base.GenerateMetadata(writer); + + writer.WriteField("Type", "text", DatasetType); + if (!string.IsNullOrEmpty(ImageUrl)) + writer.WriteField("ImageUrl", "text", ImageUrl); + + // Write any other dynamic metadata + foreach (var kvp in Metadata) + { + writer.WriteField(kvp.Key, "text", kvp.Value); // Assuming text for generic metadata for now + } + } + + public override void GenerateContent(SzfFieldWriter writer) + { + foreach (var entry in Entries) + { + writer.AppendSectionHeader($"Entry: {entry.Name}"); + + foreach (var field in entry.Fields) + { + writer.WriteField(field.Name, field.Type, field.Value); + } + + foreach (var section in entry.Sections) + { + GenerateDatasetSection(writer, section, $"Entry.{entry.Name}"); + } + + writer.AppendLine(); + } + } + + private void GenerateDatasetSection(SzfFieldWriter writer, DatasetSection section, string parentPath) + { + var sectionPath = $"{parentPath}.{section.Name}"; + writer.AppendSectionHeader(sectionPath); + + foreach (var field in section.Fields) + { + writer.WriteField(field.Name, field.Type, field.Value); + } + + foreach (var subsection in section.Subsections) + { + GenerateDatasetSection(writer, subsection, sectionPath); + } + } public override SzfValidationResult Validate() { var result = base.Validate(); if (string.IsNullOrWhiteSpace(DatasetType)) - result.AddError("DatasetType is required"); + result.AddError("DatasetType is required for Dataset"); - // Validate entries - var entryNames = new HashSet(); - foreach (var entry in Entries) - { - if (string.IsNullOrWhiteSpace(entry.Name)) - { - result.AddError("All entries must have a name"); - continue; - } - - if (!entryNames.Add(entry.Name)) - result.AddError($"Duplicate entry name: {entry.Name}"); - } + if (!Entries.Any()) + result.AddWarning("Dataset contains no entries."); return result; } @@ -172,36 +178,30 @@ public class Dataset : SzfObject public class DatasetEntry { public string Name { get; set; } = string.Empty; - public List Fields { get; set; } = []; - public List Sections { get; set; } = []; + public List Fields { get; set; } = new(); + public List Sections { get; set; } = new(); // For nested sections within an entry } public class DatasetSection { public string Name { get; set; } = string.Empty; - public List Fields { get; set; } = []; - public List Subsections { get; set; } = []; + public List Fields { get; set; } = new(); + public List Subsections { get; set; } = new(); } public class DatasetField { public string Name { get; set; } = string.Empty; public DatasetFieldType Type { get; set; } - public object? Value { get; set; } = null; + public object? Value { get; set; } public string? Description { get; set; } - public bool IsRequired { get; set; } = false; + public bool IsRequired { get; set; } public object? DefaultValue { get; set; } - - // For calculated fields public string? Formula { get; set; } - - // For dataset reference fields - public string? ReferencedDatasetId { get; set; } - public string? ReferencedDatasetType { get; set; } - public bool AllowMultiple { get; set; } = false; - - // For group fields - public List GroupFields { get; set; } = []; + public string? ReferencedDatasetId { get; set; } // For dataset-reference types, stores the ID + public string? ReferencedDatasetType { get; set; } // For dataset-type types, stores the type + public bool AllowMultiple { get; set; } + public List GroupFields { get; set; } = new(); // For group fields } public enum DatasetFieldType @@ -210,42 +210,50 @@ public enum DatasetFieldType TextField, Number, Boolean, - Group, Calculated, System, DatasetReference, DatasetType, DatasetReferenceMultiple, - DatasetTypeMultiple + DatasetTypeMultiple, + Group } -// Supporting classes for dataset management public class DatasetReference { public string Name { get; set; } = string.Empty; public Guid Guid { get; set; } - public string Version { get; set; } = string.Empty; + public string Version { get; set; } = "1.0.0"; - public override string ToString() => $"{Name}|{Guid}|{Version}"; + public override string ToString() + { + return $"{Name}|{Guid}|{Version}"; + } public static DatasetReference? Parse(string reference) { - var parts = reference.Split('|'); - if (parts.Length != 3 || !Guid.TryParse(parts[1], out var id)) + if (string.IsNullOrWhiteSpace(reference)) return null; - return new DatasetReference + var parts = reference.Split('|'); + if (parts.Length != 3) + return null; // Invalid format + + if (Guid.TryParse(parts[1], out var guid)) { - Name = parts[0], - Guid = id, - Version = parts[2] - }; + return new DatasetReference + { + Name = parts[0], + Guid = guid, + Version = parts[2] + }; + } + + return null; // Invalid GUID format } } -public class DatasetValidationResult +public class DatasetValidationResult : SzfValidationResult { - public bool IsValid { get; set; } - public List Errors { get; set; } = []; - public List Warnings { get; set; } = []; + // Specific validation results for datasets can go here if needed } \ No newline at end of file diff --git a/SessionZero/Data/SzfFieldWriter.cs b/SessionZero/Data/SzfFieldWriter.cs new file mode 100644 index 0000000..49b79b5 --- /dev/null +++ b/SessionZero/Data/SzfFieldWriter.cs @@ -0,0 +1,98 @@ +// SzfFieldWriter.cs + +using System.Text; + +namespace SessionZero.Data; + +/// +/// Helper class to write fields to a StringBuilder following SZF format, +/// respecting generator options like indentation. +/// +public class SzfFieldWriter +{ + public readonly StringBuilder Builder; + public readonly SzfGeneratorOptions Options; + + public SzfFieldWriter(StringBuilder builder, SzfGeneratorOptions options) + { + Builder = builder; + Options = options; + } + + /// + /// Writes a generic field with a string type. + /// + public void WriteField(string name, string type, object? value) + { + if (!Options.IncludeEmptyFields && (value == null || string.IsNullOrEmpty(value.ToString()))) + { + return; // Skip empty fields if option is set + } + + var formattedValue = FormatFieldValue(value, type); + + if (Options.IndentFields) + { + Builder.AppendLine($"{Options.Indent}{name} ({type}) = {formattedValue}"); + } + else + { + Builder.AppendLine($"{name} ({type}) = {formattedValue}"); + } + } + + /// + /// Writes a field with a DatasetFieldType. + /// + public void WriteField(string name, DatasetFieldType type, object? value) + { + var typeString = type switch + { + DatasetFieldType.Text => "text", + DatasetFieldType.TextField => "text-field", + DatasetFieldType.Number => "number", + DatasetFieldType.Boolean => "bool", + DatasetFieldType.Group => "group", + DatasetFieldType.Calculated => "calculated", + DatasetFieldType.System => "system", + DatasetFieldType.DatasetReference => "dataset-reference", + DatasetFieldType.DatasetType => "dataset-type", + DatasetFieldType.DatasetReferenceMultiple => "dataset-reference-multiple", + DatasetFieldType.DatasetTypeMultiple => "dataset-type-multiple", + _ => "text" // Default or unknown types + }; + WriteField(name, typeString, value); + } + + /// + /// Formats field values based on their declared type for SZF output. + /// + private string FormatFieldValue(object? value, string fieldType) + { + if (value == null) + return string.Empty; + + return fieldType.ToLower() switch + { + "bool" => (bool)value ? "true" : "false", + "number" => value.ToString() ?? "0", + _ => value.ToString() ?? string.Empty + }; + } + + /// + /// Appends a new line to the string builder. + /// + public void AppendLine() + { + Builder.AppendLine(); + } + + /// + /// Appends a section header to the string builder. + /// + public void AppendSectionHeader(string header) + { + Builder.AppendLine($"[{header}]"); + } +} \ No newline at end of file diff --git a/SessionZero/Data/SzfGenerator.cs b/SessionZero/Data/SzfGenerator.cs index b7ec4ee..fe35089 100644 --- a/SessionZero/Data/SzfGenerator.cs +++ b/SessionZero/Data/SzfGenerator.cs @@ -1,3 +1,4 @@ +using System.Reflection; using System.Text; namespace SessionZero.Data; @@ -17,15 +18,16 @@ public class SzfGenerator public string Generate(SzfObject obj) { var builder = new StringBuilder(); + var writer = new SzfFieldWriter(builder, _options); // Generate header - GenerateHeader(builder, obj); + GenerateHeader(writer, obj); // Generate metadata section - GenerateMetadataSection(builder, obj); + GenerateMetadataSection(writer, obj); // Generate type-specific content - GenerateTypeSpecificContent(builder, obj); + GenerateTypeSpecificContent(writer, obj); return builder.ToString(); } @@ -33,179 +35,48 @@ public class SzfGenerator /// /// Generate the SZF header (!type: and !schema:) /// - private void GenerateHeader(StringBuilder builder, SzfObject obj) + private void GenerateHeader(SzfFieldWriter writer, SzfObject obj) { - var objectTypeString = obj.ObjectType switch - { - SzfObjectType.Dataset => "dataset", - SzfObjectType.CharacterTemplate => "character_template", - SzfObjectType.Character => "character", - SzfObjectType.Session => "session", - _ => throw new SzfGenerateException($"Unsupported object type: {obj.ObjectType}") - }; + var type = obj.GetType(); + var attribute = type.GetCustomAttribute(); - builder.AppendLine($"!type: {objectTypeString}"); - builder.AppendLine($"!schema: {obj.SchemaVersion}"); - builder.AppendLine(); + 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}"); + writer.Builder.AppendLine(); } /// /// Generate the common metadata section /// - private void GenerateMetadataSection(StringBuilder builder, SzfObject obj) + private void GenerateMetadataSection(SzfFieldWriter writer, SzfObject obj) { - builder.AppendLine("[Metadata]"); + writer.AppendSectionHeader("Metadata"); // Standard metadata fields - WriteField(builder, "Name", "text", obj.Name); - WriteField(builder, "Version", "text", obj.Version); - WriteField(builder, "GUID", "text", obj.Id.ToString()); - WriteField(builder, "Description", "text-field", obj.Description); - WriteField(builder, "Author", "text", obj.Author); + 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 - GenerateTypeSpecificMetadata(builder, obj); + // Type-specific metadata delegated to the object itself + obj.GenerateMetadata(writer); - builder.AppendLine(); + writer.AppendLine(); } /// - /// Generate type-specific metadata fields + /// Generate type-specific content sections delegated to the object itself /// - private void GenerateTypeSpecificMetadata(StringBuilder builder, SzfObject obj) + private void GenerateTypeSpecificContent(SzfFieldWriter writer, SzfObject obj) { - switch (obj) - { - case Dataset dataset: - WriteField(builder, "Type", "text", dataset.DatasetType); - if (!string.IsNullOrEmpty(dataset.ImageUrl)) - WriteField(builder, "ImageUrl", "text", dataset.ImageUrl); - break; - // Add other types as they're implemented - } - } - - /// - /// Generate type-specific content sections - /// - private void GenerateTypeSpecificContent(StringBuilder builder, SzfObject obj) - { - switch (obj) - { - case Dataset dataset: - GenerateDatasetContent(builder, dataset); - break; - // Add other types as they're implemented - default: - throw new SzfGenerateException($"Content generation not implemented for type: {obj.GetType().Name}"); - } - } - - /// - /// Generate dataset-specific content (entries, sections, fields) - /// - private void GenerateDatasetContent(StringBuilder builder, Dataset dataset) - { - foreach (var entry in dataset.Entries) - { - // Entry header - builder.AppendLine($"[Entry: {entry.Name}]"); - - // Entry fields - foreach (var field in entry.Fields) - { - WriteDatasetField(builder, field); - } - - // Entry sections - foreach (var section in entry.Sections) - { - GenerateDatasetSection(builder, section, $"Entry.{entry.Name}"); - } - - builder.AppendLine(); - } - } - - /// - /// Generate dataset sections recursively - /// - private void GenerateDatasetSection(StringBuilder builder, DatasetSection section, string parentPath) - { - var sectionPath = $"{parentPath}.{section.Name}"; - builder.AppendLine($"[{sectionPath}]"); - - // Section fields - foreach (var field in section.Fields) - { - WriteDatasetField(builder, field); - } - - // Subsections - foreach (var subsection in section.Subsections) - { - GenerateDatasetSection(builder, subsection, sectionPath); - } - - builder.AppendLine(); - } - - /// - /// Write a dataset field in SZF format - /// - private void WriteDatasetField(StringBuilder builder, DatasetField field) - { - var typeString = field.Type switch - { - DatasetFieldType.Text => "text", - DatasetFieldType.TextField => "text-field", - DatasetFieldType.Number => "number", - DatasetFieldType.Boolean => "bool", - DatasetFieldType.Group => "group", - DatasetFieldType.Calculated => "calculated", - DatasetFieldType.System => "system", - DatasetFieldType.DatasetReference => "dataset-reference", - DatasetFieldType.DatasetType => "dataset-type", - DatasetFieldType.DatasetReferenceMultiple => "dataset-reference-multiple", - DatasetFieldType.DatasetTypeMultiple => "dataset-type-multiple", - _ => "text" - }; - - var value = FormatFieldValue(field.Value, field.Type); - WriteField(builder, field.Name, typeString, value); - } - - /// - /// Write a field in SZF format: Name (type) = value - /// - private void WriteField(StringBuilder builder, string name, string type, object? value) - { - var formattedValue = value?.ToString() ?? string.Empty; - - if (_options.IndentFields) - { - builder.AppendLine($"{_options.Indent}{name} ({type}) = {formattedValue}"); - } - else - { - builder.AppendLine($"{name} ({type}) = {formattedValue}"); - } - } - - /// - /// Format field values based on their type - /// - private string FormatFieldValue(object? value, DatasetFieldType fieldType) - { - if (value == null) - return string.Empty; - - return fieldType switch - { - DatasetFieldType.Boolean => (bool)value ? "true" : "false", - DatasetFieldType.Number => value.ToString() ?? "0", - _ => value.ToString() ?? string.Empty - }; + obj.GenerateContent(writer); } } diff --git a/SessionZero/Data/SzfObject.cs b/SessionZero/Data/SzfObject.cs index 1fcee9b..b7140be 100644 --- a/SessionZero/Data/SzfObject.cs +++ b/SessionZero/Data/SzfObject.cs @@ -1,3 +1,8 @@ +// SzfObject.cs + +using System; +using System.Collections.Generic; + namespace SessionZero.Data; public abstract class SzfObject @@ -22,6 +27,26 @@ public abstract class SzfObject /// A list of all parsed sections from the SZF file. public abstract void ParseSections(List sections); + /// + /// Virtual method for SzfObjects to generate their specific metadata fields. + /// Override this in derived classes to add custom metadata to the [Metadata] section. + /// + /// The SzfFieldWriter instance to write fields. + public virtual void GenerateMetadata(SzfFieldWriter writer) + { + // Default implementation does nothing, derived classes can override. + } + + /// + /// Virtual method for SzfObjects to generate their specific content sections. + /// Override this in derived classes to add custom sections and fields. + /// + /// 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 }; diff --git a/SessionZero/Pages/Test.razor b/SessionZero/Pages/SzfParseTest.razor similarity index 73% rename from SessionZero/Pages/Test.razor rename to SessionZero/Pages/SzfParseTest.razor index 308eaf7..ce3ecd9 100644 --- a/SessionZero/Pages/Test.razor +++ b/SessionZero/Pages/SzfParseTest.razor @@ -3,20 +3,26 @@ @using Microsoft.Extensions.Logging @inject SzfParser SzfParser @inject ILogger Logger +@inject SzfGenerator SzfGenerator -

SZF Parser Test Page

+

SZF Parser and Generator Test Page

-
-
+
+
+

SZF Input

-
-
@if (parsedObject != null) { -

Parsed Object Data

+ + } +
+
+

Parsed Object Data

+ @if (parsedObject != null) + { @@ -105,7 +111,7 @@ - @* Changed from DatasetId to Guid *@ + } @@ -136,25 +142,81 @@ } +
+

Generated SZF Output

+ @if (!string.IsNullOrEmpty(generatedSzfOutput)) + { + + } + else + { +

No SZF has been generated yet.

+ } +
-
+

Logs

@string.Join("\n", logMessages)
+ + @code { private string szfCode = string.Empty; private SzfObject? parsedObject; + private string generatedSzfOutput = string.Empty; // New field for generated output private List logMessages = new List(); private List errorMessages = new List(); private void ParseSzf() { - logMessages.Clear(); - errorMessages.Clear(); - parsedObject = null; - + ClearState(); // Clear previous states Log("Starting SZF parsing..."); if (string.IsNullOrWhiteSpace(szfCode)) @@ -180,6 +242,38 @@ } } + private void GenerateSzf() + { + generatedSzfOutput = string.Empty; // Clear previous generated output + if (parsedObject == null) + { + Log("No object to generate from. Please parse an SZF first."); + return; + } + + Log("Starting SZF generation..."); + try + { + // Use the ToSzfString() method on the parsed object, which uses SzfGenerator internally + generatedSzfOutput = parsedObject.ToSzfString(); + Log("SZF content generated successfully."); + } + catch (Exception ex) + { + const string message = "An error occurred during generation."; + LogError(message, ex); + errorMessages.Add($"{message} See logs for details."); + } + } + + private void ClearState() + { + logMessages.Clear(); + errorMessages.Clear(); + parsedObject = null; + generatedSzfOutput = string.Empty; + } + private void Log(string message) { Logger.LogInformation(message); diff --git a/SessionZero/Pages/Test.razor.cs b/SessionZero/Pages/SzfParseTest.razor.cs similarity index 100% rename from SessionZero/Pages/Test.razor.cs rename to SessionZero/Pages/SzfParseTest.razor.cs diff --git a/SessionZero/Pages/SzfParseTest.razor.css b/SessionZero/Pages/SzfParseTest.razor.css new file mode 100644 index 0000000..3afd00d --- /dev/null +++ b/SessionZero/Pages/SzfParseTest.razor.css @@ -0,0 +1,3 @@ +.main-content { + text-align: left; !important; +} \ No newline at end of file diff --git a/SessionZero/Pages/Test.razor.css b/SessionZero/Pages/Test.razor.css deleted file mode 100644 index e69de29..0000000 diff --git a/SessionZero/Program.cs b/SessionZero/Program.cs index 84e4f6f..1dc8198 100644 --- a/SessionZero/Program.cs +++ b/SessionZero/Program.cs @@ -15,6 +15,7 @@ builder.Services.AddBlazoredLocalStorage(); // Register our services builder.Services.AddScoped(); +builder.Services.AddScoped(); await builder.Build().RunAsync(); \ No newline at end of file
Object Type@parsedObject.ObjectType
@reqDataset.Alias @reqDataset.Reference?.Name@reqDataset.Reference?.Guid@reqDataset.Reference?.Guid @reqDataset.Reference?.Version