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.Collections.Generic;
using System.Linq; using System.Linq;
@ -9,7 +8,7 @@ public class CharacterTemplate : SzfObject
{ {
public string GameSystem { get; set; } = string.Empty; public string GameSystem { get; set; } = string.Empty;
public List<RequiredDatasetReference> RequiredDatasets { get; set; } = new(); public List<RequiredDatasetReference> RequiredDatasets { get; set; } = new();
public List<TemplateSection> Sections { get; set; } = new(); // Top-level sections public List<TemplateSection> Sections { get; set; } = new();
public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate; public override SzfObjectType ObjectType => SzfObjectType.CharacterTemplate;
@ -23,105 +22,81 @@ public class CharacterTemplate : SzfObject
public override void ParseSections(List<SzfSection> sections) public override void ParseSections(List<SzfSection> sections)
{ {
// Parse "Required Datasets" section // Parse [Required Datasets] section
var requiredDatasetsSection = var requiredDatasetsSection =
sections.FirstOrDefault(s => s.Name.Equals("Required Datasets", StringComparison.OrdinalIgnoreCase)); sections.FirstOrDefault(s => s.Name.Equals("Required Datasets", StringComparison.OrdinalIgnoreCase));
if (requiredDatasetsSection != null) if (requiredDatasetsSection != null)
{ {
foreach (var field in requiredDatasetsSection.Fields) foreach (var field in requiredDatasetsSection.Fields)
{ {
var datasetReference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty); var reference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty);
if (datasetReference != null) if (reference != null)
{ {
RequiredDatasets.Add(new RequiredDatasetReference RequiredDatasets.Add(new RequiredDatasetReference
{ {
Alias = field.Name, Alias = field.Name,
Reference = datasetReference Reference = reference
}); });
} }
} }
} }
// Parse and build the hierarchical template sections // Parse [Section: ...] and [Section....] sections
// Use a map to quickly find existing sections by their full path for nesting
var sectionPathMap = new Dictionary<string, TemplateSection>(StringComparer.OrdinalIgnoreCase);
foreach (var szfSection in sections) foreach (var szfSection in sections)
{ {
// Only process sections that start with "Section:" if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
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++)
{ {
var sectionNamePart = nameParts[i]; var templateSection = new TemplateSection
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))
{ {
targetSection = existingSection; Name = szfSection.Name.Replace("Section:", "").Trim()
} };
else
{
// Create a new section if it doesn't exist
var newSection = new TemplateSection { Name = sectionNamePart };
sectionPathMap[accumulatedPath] = newSection;
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) 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) private TemplateField ConvertToTemplateField(SzfField szfField)
{ {
return new TemplateField return new TemplateField
{ {
Name = szfField.Name, Name = szfField.Name,
Type = ParseDatasetFieldType(szfField.Type), // Reusing DatasetFieldType for consistency Type = ParseDatasetFieldType(szfField.Type),
Value = szfField.Value, 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) private DatasetFieldType ParseDatasetFieldType(string typeString)
{ {
return typeString.ToLower() switch return typeString.ToLower() switch
@ -130,26 +105,86 @@ public class CharacterTemplate : SzfObject
"text-field" => DatasetFieldType.TextField, "text-field" => DatasetFieldType.TextField,
"number" => DatasetFieldType.Number, "number" => DatasetFieldType.Number,
"bool" => DatasetFieldType.Boolean, "bool" => DatasetFieldType.Boolean,
"group" => DatasetFieldType.Group,
"calculated" => DatasetFieldType.Calculated, "calculated" => DatasetFieldType.Calculated,
"system" => DatasetFieldType.System, "system" => DatasetFieldType.System,
"dataset-reference" => DatasetFieldType.DatasetReference, "dataset-reference" => DatasetFieldType.DatasetReference,
"dataset-type" => DatasetFieldType.DatasetType, "dataset-type" => DatasetFieldType.DatasetType,
"dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple, "dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple,
"dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple, "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() public override SzfValidationResult Validate()
{ {
var result = base.Validate(); var result = base.Validate(); // Run base validation first
// if (string.IsNullOrWhiteSpace(GameSystem)) if (string.IsNullOrWhiteSpace(GameSystem))
// result.AddError("GameSystem is required for CharacterTemplate"); 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.
return result; return result;
} }
@ -165,7 +200,7 @@ public class TemplateSection
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public List<TemplateField> Fields { get; set; } = new(); 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 public class TemplateField

View File

@ -1,118 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace SessionZero.Data; namespace SessionZero.Data;
[SzfObject(SzfObjectType.Dataset, "dataset")] [SzfObject(SzfObjectType.Dataset, "dataset")]
public class Dataset : SzfObject public class Dataset : SzfObject
{ {
public string DatasetType { get; set; } = string.Empty; public string DatasetType { get; set; } = string.Empty;
public string ImageUrl { get; set; } = string.Empty; public string ImageUrl { get; set; } = string.Empty; // New field for dataset image
public List<DatasetEntry> Entries { get; set; } = new();
// Collection of entries within this dataset public Dictionary<string, object> Metadata { get; set; } = new(); // To store other metadata fields
public List<DatasetEntry> Entries { get; set; } = [];
// Additional metadata fields for .szf compatibility
public Dictionary<string, object> Metadata { get; set; } = new();
public override SzfObjectType ObjectType => SzfObjectType.Dataset; public override SzfObjectType ObjectType => SzfObjectType.Dataset;
public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata) public override void PopulateFromMetadata(Dictionary<string, SzfField> metadata)
{ {
// Call base method to populate common fields
base.PopulateFromMetadata(metadata); base.PopulateFromMetadata(metadata);
// Populate dataset-specific fields
if (metadata.TryGetValue("Type", out var typeField)) if (metadata.TryGetValue("Type", out var typeField))
DatasetType = typeField.Value?.ToString() ?? string.Empty; DatasetType = typeField.Value?.ToString() ?? string.Empty;
if (metadata.TryGetValue("ImageUrl", out var imageField)) if (metadata.TryGetValue("ImageUrl", out var imageUrlField))
ImageUrl = imageField.Value?.ToString() ?? string.Empty; 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) public override void ParseSections(List<SzfSection> sections)
{ {
// Dictionary to hold DatasetEntry objects by their name for easy lookup var currentEntry = (DatasetEntry?)null;
var entriesByName = new Dictionary<string, DatasetEntry>(StringComparer.OrdinalIgnoreCase);
// First pass: Identify and populate top-level DatasetEntry objects
foreach (var szfSection in sections) foreach (var szfSection in sections)
{ {
if (szfSection.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase)) if (szfSection.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase))
{ {
var entryName = szfSection.Name.Substring("Entry:".Length).Trim(); currentEntry = new DatasetEntry
if (string.IsNullOrEmpty(entryName)) continue; {
Name = szfSection.Name.Replace("Entry:", "").Trim()
};
var newEntry = new DatasetEntry { Name = entryName };
foreach (var field in szfSection.Fields) foreach (var field in szfSection.Fields)
{ {
newEntry.Fields.Add(ConvertToDatasetField(field)); currentEntry.Fields.Add(ConvertToDatasetField(field));
} }
Entries.Add(newEntry); Entries.Add(currentEntry);
entriesByName[entryName] = newEntry;
} }
} else if (szfSection.Name.StartsWith("Entry.", StringComparison.OrdinalIgnoreCase) && currentEntry != null)
// 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" // Handle subsections by finding their parent entry
if (!szfSection.Name.StartsWith("Entry.", StringComparison.OrdinalIgnoreCase)) continue; var parts = szfSection.Name.Split('.');
if (parts.Length >= 2 && parts[0].Equals("Entry", StringComparison.OrdinalIgnoreCase) &&
// Split the section name by '.' to identify parts parts[1].Equals(currentEntry.Name, StringComparison.OrdinalIgnoreCase))
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"
{ {
var sectionNamePart = nameParts[i]; var subSection = new DatasetSection
var existingSection = currentSectionList.FirstOrDefault(s => {
s.Name.Equals(sectionNamePart, StringComparison.OrdinalIgnoreCase)); 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) 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 currentEntry.Sections.Add(subSection);
currentSectionList = currentParentSection.Subsections; }
} }
} }
} }
/// <summary>
/// Convert SzfField to DatasetField
/// </summary>
private DatasetField ConvertToDatasetField(SzfField szfField) 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 return new DatasetField
{ {
Name = szfField.Name, Name = szfField.Name,
@ -121,9 +90,6 @@ public class Dataset : SzfObject
}; };
} }
/// <summary>
/// Parse DatasetFieldType from string
/// </summary>
private DatasetFieldType ParseDatasetFieldType(string typeString) private DatasetFieldType ParseDatasetFieldType(string typeString)
{ {
return typeString.ToLower() switch return typeString.ToLower() switch
@ -132,38 +98,78 @@ public class Dataset : SzfObject
"text-field" => DatasetFieldType.TextField, "text-field" => DatasetFieldType.TextField,
"number" => DatasetFieldType.Number, "number" => DatasetFieldType.Number,
"bool" => DatasetFieldType.Boolean, "bool" => DatasetFieldType.Boolean,
"group" => DatasetFieldType.Group,
"calculated" => DatasetFieldType.Calculated, "calculated" => DatasetFieldType.Calculated,
"system" => DatasetFieldType.System, "system" => DatasetFieldType.System,
"dataset-reference" => DatasetFieldType.DatasetReference, "dataset-reference" => DatasetFieldType.DatasetReference,
"dataset-type" => DatasetFieldType.DatasetType, "dataset-type" => DatasetFieldType.DatasetType,
"dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple, "dataset-reference-multiple" => DatasetFieldType.DatasetReferenceMultiple,
"dataset-type-multiple" => DatasetFieldType.DatasetTypeMultiple, "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() public override SzfValidationResult Validate()
{ {
var result = base.Validate(); var result = base.Validate();
if (string.IsNullOrWhiteSpace(DatasetType)) if (string.IsNullOrWhiteSpace(DatasetType))
result.AddError("DatasetType is required"); result.AddError("DatasetType is required for Dataset");
// Validate entries if (!Entries.Any())
var entryNames = new HashSet<string>(); result.AddWarning("Dataset contains no entries.");
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}");
}
return result; return result;
} }
@ -172,36 +178,30 @@ public class Dataset : SzfObject
public class DatasetEntry public class DatasetEntry
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public List<DatasetField> Fields { get; set; } = []; public List<DatasetField> Fields { get; set; } = new();
public List<DatasetSection> Sections { get; set; } = []; public List<DatasetSection> Sections { get; set; } = new(); // For nested sections within an entry
} }
public class DatasetSection public class DatasetSection
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public List<DatasetField> Fields { get; set; } = []; public List<DatasetField> Fields { get; set; } = new();
public List<DatasetSection> Subsections { get; set; } = []; public List<DatasetSection> Subsections { get; set; } = new();
} }
public class DatasetField public class DatasetField
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public DatasetFieldType Type { get; set; } public DatasetFieldType Type { get; set; }
public object? Value { get; set; } = null; public object? Value { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public bool IsRequired { get; set; } = false; public bool IsRequired { get; set; }
public object? DefaultValue { get; set; } public object? DefaultValue { get; set; }
// For calculated fields
public string? Formula { get; set; } public string? Formula { get; set; }
public string? ReferencedDatasetId { get; set; } // For dataset-reference types, stores the ID
// For dataset reference fields public string? ReferencedDatasetType { get; set; } // For dataset-type types, stores the type
public string? ReferencedDatasetId { get; set; } public bool AllowMultiple { get; set; }
public string? ReferencedDatasetType { get; set; } public List<DatasetField> GroupFields { get; set; } = new(); // For group fields
public bool AllowMultiple { get; set; } = false;
// For group fields
public List<DatasetField> GroupFields { get; set; } = [];
} }
public enum DatasetFieldType public enum DatasetFieldType
@ -210,42 +210,50 @@ public enum DatasetFieldType
TextField, TextField,
Number, Number,
Boolean, Boolean,
Group,
Calculated, Calculated,
System, System,
DatasetReference, DatasetReference,
DatasetType, DatasetType,
DatasetReferenceMultiple, DatasetReferenceMultiple,
DatasetTypeMultiple DatasetTypeMultiple,
Group
} }
// Supporting classes for dataset management
public class DatasetReference public class DatasetReference
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public Guid Guid { get; set; } 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) public static DatasetReference? Parse(string reference)
{ {
var parts = reference.Split('|'); if (string.IsNullOrWhiteSpace(reference))
if (parts.Length != 3 || !Guid.TryParse(parts[1], out var id))
return null; return null;
var parts = reference.Split('|');
if (parts.Length != 3)
return null; // Invalid format
if (Guid.TryParse(parts[1], out var guid))
{
return new DatasetReference return new DatasetReference
{ {
Name = parts[0], Name = parts[0],
Guid = id, Guid = guid,
Version = parts[2] Version = parts[2]
}; };
} }
return null; // Invalid GUID format
}
} }
public class DatasetValidationResult public class DatasetValidationResult : SzfValidationResult
{ {
public bool IsValid { get; set; } // Specific validation results for datasets can go here if needed
public List<string> Errors { get; set; } = [];
public List<string> Warnings { get; set; } = [];
} }

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; using System.Text;
namespace SessionZero.Data; namespace SessionZero.Data;
@ -17,15 +18,16 @@ public class SzfGenerator
public string Generate(SzfObject obj) public string Generate(SzfObject obj)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
var writer = new SzfFieldWriter(builder, _options);
// Generate header // Generate header
GenerateHeader(builder, obj); GenerateHeader(writer, obj);
// Generate metadata section // Generate metadata section
GenerateMetadataSection(builder, obj); GenerateMetadataSection(writer, obj);
// Generate type-specific content // Generate type-specific content
GenerateTypeSpecificContent(builder, obj); GenerateTypeSpecificContent(writer, obj);
return builder.ToString(); return builder.ToString();
} }
@ -33,179 +35,48 @@ public class SzfGenerator
/// <summary> /// <summary>
/// Generate the SZF header (!type: and !schema:) /// Generate the SZF header (!type: and !schema:)
/// </summary> /// </summary>
private void GenerateHeader(StringBuilder builder, SzfObject obj) private void GenerateHeader(SzfFieldWriter writer, SzfObject obj)
{ {
var objectTypeString = obj.ObjectType switch var type = obj.GetType();
{ var attribute = type.GetCustomAttribute<SzfObjectAttribute>();
SzfObjectType.Dataset => "dataset",
SzfObjectType.CharacterTemplate => "character_template",
SzfObjectType.Character => "character",
SzfObjectType.Session => "session",
_ => throw new SzfGenerateException($"Unsupported object type: {obj.ObjectType}")
};
builder.AppendLine($"!type: {objectTypeString}"); if (attribute == null)
builder.AppendLine($"!schema: {obj.SchemaVersion}"); {
builder.AppendLine(); 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> /// <summary>
/// Generate the common metadata section /// Generate the common metadata section
/// </summary> /// </summary>
private void GenerateMetadataSection(StringBuilder builder, SzfObject obj) private void GenerateMetadataSection(SzfFieldWriter writer, SzfObject obj)
{ {
builder.AppendLine("[Metadata]"); writer.AppendSectionHeader("Metadata");
// Standard metadata fields // Standard metadata fields
WriteField(builder, "Name", "text", obj.Name); writer.WriteField("Name", "text", obj.Name);
WriteField(builder, "Version", "text", obj.Version); writer.WriteField("Version", "text", obj.Version);
WriteField(builder, "GUID", "text", obj.Id.ToString()); writer.WriteField("GUID", "text", obj.Id.ToString());
WriteField(builder, "Description", "text-field", obj.Description); writer.WriteField("Description", "text-field", obj.Description);
WriteField(builder, "Author", "text", obj.Author); writer.WriteField("Author", "text", obj.Author);
// Type-specific metadata // Type-specific metadata delegated to the object itself
GenerateTypeSpecificMetadata(builder, obj); obj.GenerateMetadata(writer);
builder.AppendLine(); writer.AppendLine();
} }
/// <summary> /// <summary>
/// Generate type-specific metadata fields /// Generate type-specific content sections delegated to the object itself
/// </summary> /// </summary>
private void GenerateTypeSpecificMetadata(StringBuilder builder, SzfObject obj) private void GenerateTypeSpecificContent(SzfFieldWriter writer, SzfObject obj)
{ {
switch (obj) obj.GenerateContent(writer);
{
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
};
} }
} }

View File

@ -1,3 +1,8 @@
// SzfObject.cs
using System;
using System.Collections.Generic;
namespace SessionZero.Data; namespace SessionZero.Data;
public abstract class SzfObject 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> /// <param name="sections">A list of all parsed sections from the SZF file.</param>
public abstract void ParseSections(List<SzfSection> sections); 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() public virtual SzfValidationResult Validate()
{ {
var result = new SzfValidationResult { IsValid = true }; var result = new SzfValidationResult { IsValid = true };

View File

@ -3,20 +3,26 @@
@using Microsoft.Extensions.Logging @using Microsoft.Extensions.Logging
@inject SzfParser SzfParser @inject SzfParser SzfParser
@inject ILogger<SzfParser> Logger @inject ILogger<SzfParser> Logger
@inject SzfGenerator SzfGenerator
<h1>SZF Parser Test Page</h1> <h1>SZF Parser and Generator Test Page</h1>
<div> <div class="test-container">
<div> <div class="input-section">
<h2>SZF Input</h2>
<div> <div>
<textarea id="szf-input" @bind="szfCode" rows="20"></textarea> <textarea id="szf-input" @bind="szfCode" rows="20"></textarea>
</div> </div>
<button @onclick="ParseSzf">Parse SZF</button> <button @onclick="ParseSzf">Parse SZF</button>
</div>
<div>
@if (parsedObject != null) @if (parsedObject != null)
{ {
<button @onclick="GenerateSzf">Generate SZF from Parsed Object</button>
}
</div>
<div class="output-section">
<h2>Parsed Object Data</h2> <h2>Parsed Object Data</h2>
@if (parsedObject != null)
{
<table> <table>
<tbody> <tbody>
<tr><th>Object Type</th><td>@parsedObject.ObjectType</td></tr> <tr><th>Object Type</th><td>@parsedObject.ObjectType</td></tr>
@ -105,7 +111,7 @@
<tr> <tr>
<td>@reqDataset.Alias</td> <td>@reqDataset.Alias</td>
<td>@reqDataset.Reference?.Name</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> <td>@reqDataset.Reference?.Version</td>
</tr> </tr>
} }
@ -136,25 +142,81 @@
</div> </div>
} }
</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> <div class="logs-section">
<h2>Logs</h2> <h2>Logs</h2>
<pre>@string.Join("\n", logMessages)</pre> <pre>@string.Join("\n", logMessages)</pre>
</div> </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 { @code {
private string szfCode = string.Empty; private string szfCode = string.Empty;
private SzfObject? parsedObject; private SzfObject? parsedObject;
private string generatedSzfOutput = string.Empty; // New field for generated output
private List<string> logMessages = new List<string>(); private List<string> logMessages = new List<string>();
private List<string> errorMessages = new List<string>(); private List<string> errorMessages = new List<string>();
private void ParseSzf() private void ParseSzf()
{ {
logMessages.Clear(); ClearState(); // Clear previous states
errorMessages.Clear();
parsedObject = null;
Log("Starting SZF parsing..."); Log("Starting SZF parsing...");
if (string.IsNullOrWhiteSpace(szfCode)) 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) private void Log(string message)
{ {
Logger.LogInformation(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 // Register our services
builder.Services.AddScoped<SzfParser>(); builder.Services.AddScoped<SzfParser>();
builder.Services.AddScoped<SzfGenerator>();
await builder.Build().RunAsync(); await builder.Build().RunAsync();