using System; using System.Collections.Generic; using System.Linq; namespace SessionZero.Data; // Helper classes to represent the structured character data public class CharacterField { public string Name { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; // Store as string for writing (e.g., "text", "number") public object? Value { get; set; } } public class CharacterSection { public string Name { get; set; } = string.Empty; public List Fields { get; set; } = new(); public List Subsections { get; set; } = new(); } [SzfObject("character")] public class Character : SzfObject { public Guid TemplateGuid { get; private set; } public Version TemplateVersion { get; private set; } = new Version(1, 0, 0); // This will hold the actual character data organized in sections, // using our custom hierarchical CharacterSection objects. public List CharacterSections { get; private set; } = new List(); public Character() { // Default constructor for deserialization/initialization. // Base SzfObject constructor handles its own Metadata initialization. } public Character( Guid templateGuid, Version templateVersion, string name, string version, string? description = null, string? author = null ) { TemplateGuid = templateGuid; TemplateVersion = templateVersion; // Do NOT directly set Metadata here. The SzfObject base class manages its own Metadata. // If constructing a new object, you would typically set its Name, Version, etc. // via the SzfObject's own properties or methods after construction, or through a // dedicated constructor in SzfObject that passes to its Metadata. // For existing SzfObjects, PopulateFromMetadata handles reading. } public override void PopulateFromMetadata(Dictionary metadata) { // Call the base method to populate common SzfObject metadata (Name, Version, Description, Author) base.PopulateFromMetadata(metadata); // Populate Character-specific metadata from the "Metadata" section if (metadata.TryGetValue("SourceTemplateGuid", out var guidField)) { if (Guid.TryParse(guidField.Value?.ToString(), out var guid)) { TemplateGuid = guid; } } if (metadata.TryGetValue("SourceTemplateVersion", out var versionField)) { // Ensure the value is converted to a string and explicitly call System.Version.TryParse if (System.Version.TryParse(Convert.ToString(versionField.Value), out var ver)) { TemplateVersion = ver; } } } public override void ParseSections(List sections) { CharacterSections.Clear(); // Clear existing sections to ensure a fresh parse // A temporary list to hold top-level CharacterSections as we parse var tempTopLevelCharacterSections = new List(); // A map to quickly find parent sections by their name (simplified for lookup) // e.g., "Character Information" -> CharacterSection object var sectionNameMap = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var szfSection in sections) { if (szfSection.Name.Equals("Metadata", StringComparison.OrdinalIgnoreCase)) { // Metadata is handled by PopulateFromMetadata, so skip here continue; } if (szfSection.Name.StartsWith("Section:", StringComparison.OrdinalIgnoreCase)) { // This is a top-level section (e.g., [Section: Character Information]) var sectionName = szfSection.Name.Replace("Section:", "").Trim(); var characterSection = new CharacterSection { Name = sectionName }; foreach (var field in szfSection.Fields) { characterSection.Fields.Add(new CharacterField { Name = field.Name, Type = field.Type, // SzfField.Type is expected to be a string (e.g., "text", "number") Value = field.Value }); } tempTopLevelCharacterSections.Add(characterSection); sectionNameMap[sectionName] = characterSection; // Store by its simplified name for easy lookup } else if (szfSection.Name.StartsWith("Section.", StringComparison.OrdinalIgnoreCase)) { // This is a subsection (e.g., [Section.Ability Scores.Modifiers]) var parts = szfSection.Name.Split('.'); if (parts.Length < 2) continue; // Malformed section name // The path to the immediate parent (e.g., "Ability Scores" for "Ability Scores.Modifiers") var parentPathSegments = new List(); // Start from the first segment after "Section." and go up to the segment before the current section's name for (int i = 1; i < parts.Length - 1; i++) { parentPathSegments.Add(parts[i]); } var parentLookupKey = string.Join(".", parentPathSegments); // Find the direct parent section within the already processed hierarchy CharacterSection? parentSection = FindCharacterSectionByPath(tempTopLevelCharacterSections, parentLookupKey); if (parentSection != null) { var subSection = new CharacterSection { Name = parts.Last().Trim() // The last part of the name is the subsection's own name }; foreach (var field in szfSection.Fields) { subSection.Fields.Add(new CharacterField { Name = field.Name, Type = field.Type, Value = field.Value }); } parentSection.Subsections.Add(subSection); } // If parent not found, it implies an issue with SZF structure or order. // For now, we silently skip it. In a robust system, you might log an error. } else { // Handle any other root-level sections that are not "Metadata" // and don't conform to the "Section:..." or "Section...." patterns. // These are treated as flat character sections. var characterSection = new CharacterSection { Name = szfSection.Name.Trim() }; foreach (var field in szfSection.Fields) { characterSection.Fields.Add(new CharacterField { Name = field.Name, Type = field.Type, Value = field.Value }); } tempTopLevelCharacterSections.Add(characterSection); } } CharacterSections.AddRange(tempTopLevelCharacterSections); } // Helper method to find a section by its hierarchical path (e.g., "ParentName.SubName") private CharacterSection? FindCharacterSectionByPath(List sections, string path) { var segments = path.Split('.'); CharacterSection? currentSection = null; List currentSearchList = sections; foreach (var segment in segments) { currentSection = currentSearchList.FirstOrDefault(s => s.Name.Equals(segment, StringComparison.OrdinalIgnoreCase)); if (currentSection == null) return null; // Segment not found in the current level currentSearchList = currentSection.Subsections; // Move to subsections for the next segment } return currentSection; } public override void GenerateMetadata(SzfFieldWriter writer) { // Ensure common SzfObject metadata (Name, Version, Description, Author) is written first base.GenerateMetadata(writer); // Write Character-specific metadata fields writer.WriteField("SourceTemplateGuid", "text", TemplateGuid.ToString()); writer.WriteField("SourceTemplateVersion", "text", TemplateVersion.ToString()); } public override void GenerateContent(SzfFieldWriter writer) { // Generate all the character data sections foreach (var section in CharacterSections) { // For top-level sections, the prefix is "Section". This will result in [Section: Name] GenerateCharacterSection(writer, section, "Section"); writer.AppendLine(); // Add a blank line after each top-level section for readability } } private void GenerateCharacterSection(SzfFieldWriter writer, CharacterSection section, string parentSectionHeaderPrefix) { // Construct the full section header string based on the current nesting level string currentSectionHeader; if (parentSectionHeaderPrefix == "Section") { // Top-level section, e.g., [Section: Character Information] currentSectionHeader = $"Section: {section.Name}"; } else { // Nested section, e.g., [Section.Ability Scores.Modifiers] currentSectionHeader = $"{parentSectionHeaderPrefix}.{section.Name}"; } writer.AppendSectionHeader(currentSectionHeader); // Write all fields within the current section foreach (var field in section.Fields) { // Use the string representation of the type as required by SzfFieldWriter writer.WriteField(field.Name, field.Type, field.Value); } // Recursively generate all subsections foreach (var subSection in section.Subsections) { GenerateCharacterSection(writer, subSection, currentSectionHeader); } } }