updating the szf format and parsers to be more consistent

This commit is contained in:
Chris Bell 2025-07-03 12:02:52 -05:00
parent d89774e8fc
commit 7f15c4c8ba
5 changed files with 196 additions and 166 deletions

View File

@ -46,13 +46,13 @@
<select>
<option value="" disabled selected>Field Type</option>
<option value="text">Text</option>
<option value="text-field">Multi-Line Text</option>
<option value="number">Number</option>
<option value="calculated">Formula</option>
<option value="boolean">Boolean</option>
<option value="entry-reference">Entry Reference</option>
<option value="entry-reference-multi">Multi Entry Reference</option>
<option value="system">System</option>
<option value="calculated">Formula</option>
<option value="text-field">Multi-Line Text</option>
@* <option value="entry-reference">Entry Reference</option> *@
@* <option value="entry-reference-multi">Multi Entry Reference</option> *@
@* <option value="system">System</option> *@
</select>
<input type="text" placeholder="Field Value"/>

View File

@ -22,6 +22,7 @@ public class CharacterSection
[SzfObject("character")]
public class Character : SzfObject
{
public string TemplateName { get; private set; } = "Unknown";
public Guid TemplateGuid { get; private set; }
public Version TemplateVersion { get; private set; } = new Version(1, 0, 0);
@ -59,125 +60,114 @@ public class Character : SzfObject
base.PopulateFromMetadata(metadata);
// Populate Character-specific metadata from the "Metadata" section
if (metadata.TryGetValue("SourceTemplateGuid", out var guidField))
if (metadata.TryGetValue("TemplateRef", out var refField) && refField.Value is string refValue)
{
if (Guid.TryParse(guidField.Value?.ToString(), out var guid))
var parts = refValue.Split('|');
if (parts.Length == 3)
{
TemplateGuid = guid;
TemplateName = parts[0];
if (Guid.TryParse(parts[1], out var guid))
{
TemplateGuid = guid;
}
if (System.Version.TryParse(parts[2], out var ver))
{
TemplateVersion = ver;
}
}
}
if (metadata.TryGetValue("SourceTemplateVersion", out var versionField))
// Fallback for legacy format
else
{
// Ensure the value is converted to a string and explicitly call System.Version.TryParse
if (System.Version.TryParse(Convert.ToString(versionField.Value), out var ver))
if (metadata.TryGetValue("SourceTemplateGuid", out var guidField))
{
TemplateVersion = ver;
if (Guid.TryParse(guidField.Value?.ToString(), out var guid))
{
TemplateGuid = guid;
}
}
if (metadata.TryGetValue("SourceTemplateVersion", out var versionField))
{
// Ensure the value is converted to a string and explicitly call System.Version.TryParse
if (System.Version.TryParse(Convert.ToString(versionField.Value), out var ver))
{
TemplateVersion = ver;
}
}
}
}
public override void ParseSections(List<SzfSection> sections)
{
CharacterSections.Clear(); // Clear existing sections to ensure a fresh parse
// A temporary list to hold top-level CharacterSections as we parse
var tempTopLevelCharacterSections = new List<CharacterSection>();
// A map to quickly find parent sections by their name (simplified for lookup)
// e.g., "Character Information" -> CharacterSection object
var sectionNameMap = new Dictionary<string, CharacterSection>(StringComparer.OrdinalIgnoreCase);
CharacterSections.Clear();
var sectionMap = new Dictionary<string, CharacterSection>(StringComparer.OrdinalIgnoreCase);
// This implementation assumes that parent sections are defined in the file
// before any of their subsections.
foreach (var szfSection in sections)
{
if (szfSection.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))
var name = szfSection.Name;
// Metadata is handled by PopulateFromMetadata, so skip here
if (name.Equals("Metadata", StringComparison.OrdinalIgnoreCase))
{
// Metadata is handled by PopulateFromMetadata, so skip here
continue;
}
if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
{
// This is a top-level section (e.g., [Section: Character Information])
var sectionName = szfSection.Name.Replace("Section:", "").Trim();
var characterSection = new CharacterSection { Name = sectionName };
// --- Handle Subsections ---
var nameParts = name.Split('.');
string? parentName = null;
string? childName = null;
// New format: [ParentName.ChildName]
if (nameParts.Length > 1 && !nameParts[0].Equals("Section", StringComparison.OrdinalIgnoreCase))
{
parentName = nameParts[0];
childName = string.Join(".", nameParts.Skip(1));
}
// Legacy format: [Section.ParentName.ChildName]
else if (nameParts.Length > 2 && nameParts[0].Equals("Section", StringComparison.OrdinalIgnoreCase))
{
parentName = nameParts[1];
childName = string.Join(".", nameParts.Skip(2));
}
if (parentName != null && childName != null && sectionMap.TryGetValue(parentName, out var parentSection))
{
var subSection = new CharacterSection { Name = childName };
foreach (var field in szfSection.Fields)
{
characterSection.Fields.Add(new CharacterField
{
Name = field.Name,
Type = field.Type, // Assign directly as string
Value = field.Value
});
subSection.Fields.Add(new CharacterField
{ Name = field.Name, Type = field.Type, Value = field.Value });
}
tempTopLevelCharacterSections.Add(characterSection);
sectionNameMap[sectionName] = characterSection; // Store by its simplified name for easy lookup
parentSection.Subsections.Add(subSection);
}
else if (szfSection.Name.StartsWith("Section.", StringComparison.OrdinalIgnoreCase))
else // --- Handle Top-Level Sections ---
{
// This is a subsection (e.g., [Section.Ability Scores.Modifiers])
var parts = szfSection.Name.Split('.');
if (parts.Length < 2) continue; // Malformed section name
// The path to the immediate parent (e.g., "Ability Scores" for "Ability Scores.Modifiers")
var parentPathSegments = new List<string>();
// Start from the first segment after "Section." and go up to the segment before the current section's name
for (int i = 1; i < parts.Length - 1; i++)
var sectionName = name;
// Legacy format: [Section:SectionName]
if (sectionName.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
{
parentPathSegments.Add(parts[i]);
sectionName = sectionName.Substring("Section:".Length).Trim();
}
var parentLookupKey = string.Join(".", parentPathSegments);
// Find the direct parent section within the already processed hierarchy
CharacterSection? parentSection =
FindCharacterSectionByPath(tempTopLevelCharacterSections, parentLookupKey);
if (parentSection != null)
if (!sectionMap.ContainsKey(sectionName))
{
var subSection = new CharacterSection
{
Name = parts.Last().Trim() // The last part of the name is the subsection's own name
};
var characterSection = new CharacterSection { Name = sectionName };
foreach (var field in szfSection.Fields)
{
subSection.Fields.Add(new CharacterField
{
Name = field.Name,
Type = field.Type, // Assign directly as string
Value = field.Value
});
characterSection.Fields.Add(new CharacterField
{ Name = field.Name, Type = field.Type, Value = field.Value });
}
parentSection.Subsections.Add(subSection);
CharacterSections.Add(characterSection);
sectionMap[sectionName] = characterSection;
}
// If parent not found, it implies an issue with SZF structure or order.
// For now, we silently skip it. In a robust system, you might log an error.
}
else
{
// Handle any other root-level sections that are not "Metadata"
// and don't conform to the "Section:..." or "Section...." patterns.
// These are treated as flat character sections.
var characterSection = new CharacterSection { Name = szfSection.Name.Trim() };
foreach (var field in szfSection.Fields)
{
characterSection.Fields.Add(new CharacterField
{
Name = field.Name,
Type = field.Type, // Assign directly as string
Value = field.Value
});
}
tempTopLevelCharacterSections.Add(characterSection);
}
}
CharacterSections.AddRange(tempTopLevelCharacterSections);
}
// Helper method to find a section by its hierarchical path (e.g., "ParentName.SubName")
@ -208,8 +198,7 @@ public class Character : SzfObject
base.GenerateMetadata(writer);
// Write Character-specific metadata fields
writer.WriteField("SourceTemplateGuid", "text", TemplateGuid.ToString());
writer.WriteField("SourceTemplateVersion", "text", TemplateVersion.ToString());
writer.WriteField("TemplateRef", "text", $"{TemplateName}|{TemplateGuid}|{TemplateVersion}");
}
public override void GenerateContent(SzfFieldWriter writer)

View File

@ -20,66 +20,82 @@ public class CharacterTemplate : SzfObject
public override void ParseSections(List<SzfSection> sections)
{
// 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 reference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty);
if (reference != null)
{
RequiredDatasets.Add(new RequiredDatasetReference
{
Alias = field.Name,
Reference = reference
});
}
}
}
this.Sections.Clear();
this.RequiredDatasets.Clear();
var sectionMap = new Dictionary<string, TemplateSection>(StringComparer.OrdinalIgnoreCase);
// Parse [Section: ...] and [Section....] sections
// This implementation assumes that parent sections are defined in the file
// before any of their subsections.
foreach (var szfSection in sections)
{
if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
{
var templateSection = new TemplateSection
{
Name = szfSection.Name.Replace("Section:", "").Trim()
};
var name = szfSection.Name;
// --- Handle Special "Required Datasets" Section ---
if (name.Equals("Required Datasets", StringComparison.OrdinalIgnoreCase))
{
foreach (var field in szfSection.Fields)
{
templateSection.Fields.Add(ConvertToTemplateField(field));
var reference = DatasetReference.Parse(field.Value?.ToString() ?? string.Empty);
if (reference != null)
{
RequiredDatasets.Add(new RequiredDatasetReference
{
Alias = field.Name,
Reference = reference
});
}
}
Sections.Add(templateSection);
continue; // Processed, move to next section
}
else if (szfSection.Name.StartsWith("Section.", StringComparison.OrdinalIgnoreCase))
// --- Handle Subsections ---
var nameParts = name.Split('.');
string? parentName = null;
string? childName = null;
// New format: [ParentName.ChildName]
if (nameParts.Length > 1 && !nameParts[0].Equals("Section", StringComparison.OrdinalIgnoreCase))
{
// Handle subsections by finding their parent
var parts = szfSection.Name.Split('.');
if (parts.Length >= 2)
parentName = nameParts[0];
childName = string.Join(".", nameParts.Skip(1));
}
// Legacy format: [Section.ParentName.ChildName]
else if (nameParts.Length > 2 && nameParts[0].Equals("Section", StringComparison.OrdinalIgnoreCase))
{
parentName = nameParts[1];
childName = string.Join(".", nameParts.Skip(2));
}
if (parentName != null && childName != null && sectionMap.TryGetValue(parentName, out var parentSection))
{
var subSection = new TemplateSection { Name = childName };
foreach (var field in szfSection.Fields)
{
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)
subSection.Fields.Add(ConvertToTemplateField(field));
}
parentSection.Subsections.Add(subSection);
}
else // --- Handle Top-Level Sections ---
{
var sectionName = name;
// Legacy format: [Section:SectionName]
if (sectionName.StartsWith("Section:", StringComparison.OrdinalIgnoreCase))
{
sectionName = sectionName.Substring("Section:".Length).Trim();
}
if (!sectionMap.ContainsKey(sectionName))
{
var templateSection = new TemplateSection { Name = sectionName };
foreach (var field in szfSection.Fields)
{
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);
templateSection.Fields.Add(ConvertToTemplateField(field));
}
this.Sections.Add(templateSection);
sectionMap[sectionName] = templateSection;
}
}
}

View File

@ -36,42 +36,67 @@ public class Dataset : SzfObject
public override void ParseSections(List<SzfSection> sections)
{
var currentEntry = (DatasetEntry?)null;
// This implementation assumes that parent entries are defined in the file
// before any of their subsections.
Entries.Clear();
var entryMap = new Dictionary<string, DatasetEntry>(StringComparer.OrdinalIgnoreCase);
foreach (var szfSection in sections)
{
if (szfSection.Name.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase))
{
currentEntry = new DatasetEntry
{
Name = szfSection.Name.Replace("Entry:", "").Trim()
};
var name = szfSection.Name;
// --- Check for Subsections ---
// Handles new format `[EntryName.SubSection]` and legacy `[Entry.EntryName.SubSection]`
var nameParts = name.Split('.');
string? parentName = null;
string? subSectionName = null;
if (nameParts.Length > 1)
{
if (nameParts[0].Equals("Entry", StringComparison.OrdinalIgnoreCase) && nameParts.Length > 2)
{
// Legacy: [Entry.EntryName.SubSection]
parentName = nameParts[1];
subSectionName = string.Join(".", nameParts.Skip(2));
}
else if (!nameParts[0].Equals("Entry", StringComparison.OrdinalIgnoreCase))
{
// New: [EntryName.SubSection]
parentName = nameParts[0];
subSectionName = string.Join(".", nameParts.Skip(1));
}
}
if (parentName != null && subSectionName != null && entryMap.TryGetValue(parentName, out var parentEntry))
{
var subSection = new DatasetSection { Name = subSectionName };
foreach (var field in szfSection.Fields)
{
currentEntry.Fields.Add(ConvertToDatasetField(field));
subSection.Fields.Add(ConvertToDatasetField(field));
}
Entries.Add(currentEntry);
parentEntry.Sections.Add(subSection);
}
else if (szfSection.Name.StartsWith("Entry.", StringComparison.OrdinalIgnoreCase) && currentEntry != null)
else // --- Handle as Top-Level Entry ---
{
// 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 entryName = name;
// Legacy: [Entry:EntryName]
if (entryName.StartsWith("Entry:", StringComparison.OrdinalIgnoreCase))
{
var subSection = new DatasetSection
{
Name = string.Join(".", parts.Skip(2)).Trim() // Name for the subsection
};
entryName = entryName.Substring("Entry:".Length).Trim();
}
foreach (var field in szfSection.Fields)
{
subSection.Fields.Add(ConvertToDatasetField(field));
}
if (!entryMap.TryGetValue(entryName, out var entry))
{
entry = new DatasetEntry { Name = entryName };
Entries.Add(entry);
entryMap[entryName] = entry;
}
currentEntry.Sections.Add(subSection);
// Add fields to the entry
foreach (var field in szfSection.Fields)
{
entry.Fields.Add(ConvertToDatasetField(field));
}
}
}

View File

@ -101,7 +101,7 @@ A dataset file is a collection of structured entries.
* Type (text): The category of the dataset (e.g., items, spells, classes).
* Guid (text): A globally unique identifier. **Optional**: If left blank, the system can generate one.
* Version (text): The semantic version of the dataset.
* **[Entry:EntryName] Section**: Defines a single item in the dataset.
* **[EntryName] Section**: Defines a single item in the dataset.
* EntryName is the unique key for the entry within the dataset.
**Example: CoreItems.szf**
@ -121,12 +121,12 @@ Author (text) = Fantasy Creator
Description (text-field) = A collection of basic items for any fantasy campaign.
# Definition for a Longsword
[Entry:Longsword]
[Longsword]
Name (text) = Longsword
Description (text-field) = A standard sword with a long blade and crossguard.
Category (text) = Weapon
[Entry.Longsword.Stats]
[Longsword.Stats]
Damage (text) = 1d8 slashing
Weight (number) = 3
Cost (number) = 15
@ -205,25 +205,25 @@ Guid = a1b2c3d4-e5f6-7890-abcd-ef1234567890
Version = 1.0.0
TemplateRef = Standard Fantasy Character|f9e8d7c6-b5a4-3210-9876-543210fedcba|2.1.0
[Section: Info]
[Info]
CharacterName = Elara
# The value 'Ranger' is an entry from the 'ClassData' dataset,
# as specified by the 'Class' field in the template.
Class = Ranger
[Section: AbilityScores]
[AbilityScores]
Strength = 16
Dexterity = 14
# Calculated values are not stored in the character file.
[Section: Equipment]
[Equipment]
# 'PrimaryWeapon' is a user-defined field name for the item.
# The value 'ItemData.Longsword' refers to the 'Longsword' entry in the 'ItemData' dataset.
PrimaryWeapon = ItemData.Longsword
PrimaryWeapon.Quantity = 1
[Section: Inventory]
[Inventory]
HealingPotion = ItemData.HealingPotion
HealingPotion.Quantity = 5
HempRope = ItemData.HempRope