SessionZeroWasm/SessionZero/Data/Character.cs

254 lines
10 KiB
C#

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<CharacterField> Fields { get; set; } = new();
public List<CharacterSection> 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<CharacterSection> CharacterSections { get; private set; } = new List<CharacterSection>();
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<string, SzfField> 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<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);
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<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++)
{
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<CharacterSection> sections, string path)
{
var segments = path.Split('.');
CharacterSection? currentSection = null;
List<CharacterSection> 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);
}
}
}