Seperation of szf generator. Updated test page

This commit is contained in:
Chris Bell 2025-07-01 00:37:38 -05:00
parent 22cce4257d
commit 678c10a872
11 changed files with 636 additions and 369 deletions

132
5e_template.szf Normal file
View File

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

View File

@ -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<RequiredDatasetReference> RequiredDatasets { get; set; } = new();
public List<TemplateSection> Sections { get; set; } = new(); // Top-level sections
public List<TemplateSection> Sections { get; set; } = new();
public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate;
@ -23,105 +22,81 @@ public class CharacterTemplate : SzfObject
public override void ParseSections(List<SzfSection> 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<string, TemplateSection>(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<TemplateSection> 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<TemplateField> Fields { get; set; } = new();
public List<TemplateSection> Subsections { get; set; } = new();
public List<TemplateSection> Subsections { get; set; } = new(); // For nested sections
}
public class TemplateField

View File

@ -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<DatasetEntry> Entries { get; set; } = [];
// Additional metadata fields for .szf compatibility
public Dictionary<string, object> Metadata { get; set; } = new();
public string ImageUrl { get; set; } = string.Empty; // New field for dataset image
public List<DatasetEntry> Entries { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new(); // To store other metadata fields
public override SzfObjectType ObjectType => SzfObjectType.Dataset;
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
{
// 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!;
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="sections">A list of all parsed sections from the SZF file.</param>
public override void ParseSections(List<SzfSection> sections)
{
// Dictionary to hold DatasetEntry objects by their name for easy lookup
var entriesByName = new Dictionary<string, DatasetEntry>(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<DatasetSection> 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);
}
}
}
}
/// <summary>
/// Convert SzfField to DatasetField
/// </summary>
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
};
}
/// <summary>
/// Parse DatasetFieldType from string
/// </summary>
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<string>();
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<DatasetField> Fields { get; set; } = [];
public List<DatasetSection> Sections { get; set; } = [];
public List<DatasetField> Fields { get; set; } = new();
public List<DatasetSection> Sections { get; set; } = new(); // For nested sections within an entry
}
public class DatasetSection
{
public string Name { get; set; } = string.Empty;
public List<DatasetField> Fields { get; set; } = [];
public List<DatasetSection> Subsections { get; set; } = [];
public List<DatasetField> Fields { get; set; } = new();
public List<DatasetSection> 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<DatasetField> 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<DatasetField> 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<string> Errors { get; set; } = [];
public List<string> Warnings { get; set; } = [];
// Specific validation results for datasets can go here if needed
}

View File

@ -0,0 +1,98 @@
// SzfFieldWriter.cs
using System.Text;
namespace SessionZero.Data;
/// <summary>
/// Helper class to write fields to a StringBuilder following SZF format,
/// respecting generator options like indentation.
/// </summary>
public class SzfFieldWriter
{
public readonly StringBuilder Builder;
public readonly SzfGeneratorOptions Options;
public SzfFieldWriter(StringBuilder builder, SzfGeneratorOptions options)
{
Builder = builder;
Options = options;
}
/// <summary>
/// Writes a generic field with a string type.
/// </summary>
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}");
}
}
/// <summary>
/// Writes a field with a DatasetFieldType.
/// </summary>
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);
}
/// <summary>
/// Formats field values based on their declared type for SZF output.
/// </summary>
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
};
}
/// <summary>
/// Appends a new line to the string builder.
/// </summary>
public void AppendLine()
{
Builder.AppendLine();
}
/// <summary>
/// Appends a section header to the string builder.
/// </summary>
public void AppendSectionHeader(string header)
{
Builder.AppendLine($"[{header}]");
}
}

View File

@ -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
/// <summary>
/// Generate the SZF header (!type: and !schema:)
/// </summary>
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<SzfObjectAttribute>();
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();
}
/// <summary>
/// Generate the common metadata section
/// </summary>
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();
}
/// <summary>
/// Generate type-specific metadata fields
/// Generate type-specific content sections delegated to the object itself
/// </summary>
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
}
}
/// <summary>
/// Generate type-specific content sections
/// </summary>
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}");
}
}
/// <summary>
/// Generate dataset-specific content (entries, sections, fields)
/// </summary>
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();
}
}
/// <summary>
/// Generate dataset sections recursively
/// </summary>
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();
}
/// <summary>
/// Write a dataset field in SZF format
/// </summary>
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);
}
/// <summary>
/// Write a field in SZF format: Name (type) = value
/// </summary>
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}");
}
}
/// <summary>
/// Format field values based on their type
/// </summary>
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);
}
}

View File

@ -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
/// <param name="sections">A list of all parsed sections from the SZF file.</param>
public abstract void ParseSections(List<SzfSection> sections);
/// <summary>
/// Virtual method for SzfObjects to generate their specific metadata fields.
/// Override this in derived classes to add custom metadata to the [Metadata] section.
/// </summary>
/// <param name="writer">The SzfFieldWriter instance to write fields.</param>
public virtual void GenerateMetadata(SzfFieldWriter writer)
{
// Default implementation does nothing, derived classes can override.
}
/// <summary>
/// Virtual method for SzfObjects to generate their specific content sections.
/// Override this in derived classes to add custom sections and fields.
/// </summary>
/// <param name="writer">The SzfFieldWriter instance to write fields and manage sections.</param>
public virtual void GenerateContent(SzfFieldWriter writer)
{
// Default implementation does nothing, derived classes can override.
}
public virtual SzfValidationResult Validate()
{
var result = new SzfValidationResult { IsValid = true };

View File

@ -3,20 +3,26 @@
@using Microsoft.Extensions.Logging
@inject SzfParser SzfParser
@inject ILogger<SzfParser> Logger
@inject SzfGenerator SzfGenerator
<h1>SZF Parser Test Page</h1>
<h1>SZF Parser and Generator Test Page</h1>
<div>
<div>
<div class="test-container">
<div class="input-section">
<h2>SZF Input</h2>
<div>
<textarea id="szf-input" @bind="szfCode" rows="20"></textarea>
</div>
<button @onclick="ParseSzf">Parse SZF</button>
</div>
<div>
@if (parsedObject != null)
{
<h2>Parsed Object Data</h2>
<button @onclick="GenerateSzf">Generate SZF from Parsed Object</button>
}
</div>
<div class="output-section">
<h2>Parsed Object Data</h2>
@if (parsedObject != null)
{
<table>
<tbody>
<tr><th>Object Type</th><td>@parsedObject.ObjectType</td></tr>
@ -105,7 +111,7 @@
<tr>
<td>@reqDataset.Alias</td>
<td>@reqDataset.Reference?.Name</td>
<td>@reqDataset.Reference?.Guid</td> @* Changed from DatasetId to Guid *@
<td>@reqDataset.Reference?.Guid</td>
<td>@reqDataset.Reference?.Version</td>
</tr>
}
@ -136,25 +142,81 @@
</div>
}
</div>
<div class="generated-output-section">
<h2>Generated SZF Output</h2>
@if (!string.IsNullOrEmpty(generatedSzfOutput))
{
<textarea id="generated-szf-output" @bind="generatedSzfOutput" rows="20" readonly></textarea>
}
else
{
<p>No SZF has been generated yet.</p>
}
</div>
</div>
<div>
<div class="logs-section">
<h2>Logs</h2>
<pre>@string.Join("\n", logMessages)</pre>
</div>
<style>
.test-container {
display: grid;
grid-template-columns: 1fr 1fr 1fr; /* Three columns for input, parsed data, generated output */
gap: 20px;
}
.input-section, .output-section, .generated-output-section {
border: 1px solid #ccc;
padding: 15px;
border-radius: 5px;
}
textarea {
width: 100%;
box-sizing: border-box; /* Include padding and border in the element's total width and height */
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f2f2f2;
}
.logs-section {
margin-top: 20px;
border: 1px solid #ccc;
padding: 15px;
border-radius: 5px;
}
pre {
white-space: pre-wrap; /* Ensures logs wrap */
word-wrap: break-word;
}
</style>
@code {
private string szfCode = string.Empty;
private SzfObject? parsedObject;
private string generatedSzfOutput = string.Empty; // New field for generated output
private List<string> logMessages = new List<string>();
private List<string> errorMessages = new List<string>();
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);

View File

@ -0,0 +1,3 @@
.main-content {
text-align: left; !important;
}

View File

@ -15,6 +15,7 @@ builder.Services.AddBlazoredLocalStorage();
// Register our services
builder.Services.AddScoped<SzfParser>();
builder.Services.AddScoped<SzfGenerator>();
await builder.Build().RunAsync();