Added advanced section and fixed linter
This commit is contained in:
parent
f6e04df6ac
commit
e0891f0c7f
@ -3,6 +3,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using SessionZero.Pages;
|
||||||
|
|
||||||
namespace SessionZero.Data;
|
namespace SessionZero.Data;
|
||||||
|
|
||||||
@ -138,6 +139,29 @@ public abstract class SzfObject
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if required metadata fields are present and adds errors if they are missing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">Dictionary containing metadata fields</param>
|
||||||
|
/// <param name="requiredFields">Array of required field names</param>
|
||||||
|
/// <param name="errors">List to add errors to</param>
|
||||||
|
protected void CheckRequiredMetadata(Dictionary<string, object> metadata, string[] requiredFields, List<SzfEditor.SzfError> errors)
|
||||||
|
{
|
||||||
|
if (metadata == null)
|
||||||
|
{
|
||||||
|
errors.Add(new SzfEditor.SzfError(0, "Missing Metadata section"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var field in requiredFields)
|
||||||
|
{
|
||||||
|
if (!metadata.ContainsKey(field) || metadata[field] == null || string.IsNullOrEmpty(metadata[field].ToString()))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfEditor.SzfError(0, $"Missing required metadata: {field}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SzfValidationResult
|
public class SzfValidationResult
|
||||||
|
@ -13,6 +13,10 @@
|
|||||||
<img src="res/icons/outline/user/users-group.svg" class="nav-icon" />
|
<img src="res/icons/outline/user/users-group.svg" class="nav-icon" />
|
||||||
Characters
|
Characters
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink class="nav-link" href="advanced">
|
||||||
|
<img src="res/icons/outline/general/bug.svg" class="nav-icon" />
|
||||||
|
Advanced
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
17
SessionZero/Pages/Advanced.razor
Normal file
17
SessionZero/Pages/Advanced.razor
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
@page "/Advanced"
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
|
<h1 class="page-title">Advanced Features</h1>
|
||||||
|
|
||||||
|
<div class="library-card-container">
|
||||||
|
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/szf-editor"); })">
|
||||||
|
<img src="res/icons/outline/general/edit.svg" class="icon" />
|
||||||
|
<h1>SZF Editor</h1>
|
||||||
|
<p>Manually create and edit SZF files with syntax highlighting and basic linting</p>
|
||||||
|
</NavLink>
|
||||||
|
<NavLink class="card" onclick="@(() => { NavigationManager.NavigateTo("/szf-parser-test"); })">
|
||||||
|
<img src="res/icons/outline/general/bug.svg" class="icon"/>
|
||||||
|
<h1>SZF Testing Grounds</h1>
|
||||||
|
<p>Internal development tool for testing SZF parsing, saving, and retrieving</p>
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
@ -1,5 +1,9 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
|
@inject NavigationManager NavigationManager
|
||||||
|
|
||||||
<PageTitle>Home</PageTitle>
|
<PageTitle>Home</PageTitle>
|
||||||
|
|
||||||
<h1 class="page-title">Welcome to SessionZero</h1>
|
<h1 class="page-title">Welcome to SessionZero</h1>
|
||||||
|
|
||||||
|
<p class="page-description">SessionZero is a tool designed to help you manage your tabletop RPG sessions, characters, and campaigns.</p>
|
||||||
|
|
||||||
|
@ -85,47 +85,13 @@
|
|||||||
|
|
||||||
private static readonly HashSet<string> ValidTypes = new()
|
private static readonly HashSet<string> ValidTypes = new()
|
||||||
{
|
{
|
||||||
"text", "text-field", "number", "bool", "group", "calculated",
|
"text", "text-field", "number", "bool", "calculated",
|
||||||
"system", "dataset-reference", "dataset-type",
|
"system", "entry-reference", "entry-reference-list"
|
||||||
"dataset-reference-multiple", "dataset-type-multiple"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
var exampleSzf = @"!type: character_template
|
var exampleSzf = "";
|
||||||
!schema: 1.0.0
|
|
||||||
bad line here
|
|
||||||
|
|
||||||
# This is a comment. It will be ignored by the parser.
|
|
||||||
# It's useful for adding notes to your templates.
|
|
||||||
|
|
||||||
[Metadata]
|
|
||||||
Name (text) = Robust Fantasy Character
|
|
||||||
Author (text) = Gemini
|
|
||||||
GameSystem (text) = Fantasy RPG Universal
|
|
||||||
Description (text-field) = A character sheet with calculated values and dependencies.
|
|
||||||
InvalidType (faketype) = some value
|
|
||||||
|
|
||||||
[Required_Datasets]
|
|
||||||
Classes (dataset-reference) = Core Classes|c3d4e5f6-g7h8-9012-cdef-123456789012|1.5.0
|
|
||||||
|
|
||||||
[Section: Ability Scores]
|
|
||||||
Strength (number) = 14
|
|
||||||
Dexterity (number) = 12
|
|
||||||
Constitution (number) = 15
|
|
||||||
IsCool (bool) = true
|
|
||||||
|
|
||||||
[Section.Ability_Scores.Modifiers]
|
|
||||||
# Modifiers are calculated automatically.
|
|
||||||
StrengthMod (calculated) = (Strength - 10) / 2
|
|
||||||
DexterityMod (calculated) = (Dexterity - 10) / 2
|
|
||||||
ConstitutionMod (calculated) = (Constitution - 10) / 2
|
|
||||||
|
|
||||||
[Section: Combat]
|
|
||||||
ProficiencyBonus (number) = 2
|
|
||||||
HitPoints (calculated) = 10 + ConstitutionMod
|
|
||||||
ArmorClass (calculated) = 10 + DexterityMod
|
|
||||||
";
|
|
||||||
|
|
||||||
SetContent(exampleSzf);
|
SetContent(exampleSzf);
|
||||||
}
|
}
|
||||||
@ -265,7 +231,34 @@ ArmorClass (calculated) = 10 + DexterityMod
|
|||||||
var current = structure;
|
var current = structure;
|
||||||
var path = new List<string>();
|
var path = new List<string>();
|
||||||
var errors = new List<SzfError>();
|
var errors = new List<SzfError>();
|
||||||
|
var fileType = "";
|
||||||
|
var schemaVersion = "";
|
||||||
|
var hasRequiredHeader = false;
|
||||||
|
|
||||||
|
// First pass - verify header requirements
|
||||||
|
if (lines.Length >= 2)
|
||||||
|
{
|
||||||
|
var typeMatch = Regex.Match(lines[0].Trim(), @"^!type:\s*([\w_-]+)$");
|
||||||
|
var schemaMatch = Regex.Match(lines[1].Trim(), @"^!schema:\s*([\d.]+)$");
|
||||||
|
|
||||||
|
if (typeMatch.Success && schemaMatch.Success)
|
||||||
|
{
|
||||||
|
fileType = typeMatch.Groups[1].Value;
|
||||||
|
schemaVersion = schemaMatch.Groups[1].Value;
|
||||||
|
hasRequiredHeader = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasRequiredHeader)
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(0, "Missing required header. First two lines must be !type: and !schema:"));
|
||||||
|
}
|
||||||
|
else if (!new[] { "dataset", "character_template", "character", "session" }.Contains(fileType))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(0, $"Invalid file type: {fileType}. Must be one of: dataset, character_template, character, session"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass - parse content
|
||||||
for (int idx = 0; idx < lines.Length; idx++)
|
for (int idx = 0; idx < lines.Length; idx++)
|
||||||
{
|
{
|
||||||
var line = lines[idx];
|
var line = lines[idx];
|
||||||
@ -288,7 +281,32 @@ ArmorClass (calculated) = 10 + DexterityMod
|
|||||||
}
|
}
|
||||||
else if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
else if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
|
||||||
{
|
{
|
||||||
|
// Extract section name (remove brackets)
|
||||||
var sectionPath = trimmed.Substring(1, trimmed.Length - 2).Replace(": ", ".");
|
var sectionPath = trimmed.Substring(1, trimmed.Length - 2).Replace(": ", ".");
|
||||||
|
|
||||||
|
// Parse the section path and validate each segment
|
||||||
|
var sections = sectionPath.Split('.');
|
||||||
|
for (int i = 0; i < sections.Length; i++)
|
||||||
|
{
|
||||||
|
var section = sections[i].Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(section))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, "Empty section name is not allowed"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Entry: prefix for datasets
|
||||||
|
if (fileType == "dataset" && i == 0 && section.StartsWith("Entry:"))
|
||||||
|
{
|
||||||
|
// This is valid for datasets
|
||||||
|
}
|
||||||
|
// Otherwise, check if section follows PascalCase
|
||||||
|
else if (!Regex.IsMatch(section, @"^([A-Z][a-z0-9]*)+$") && section != "Required_Datasets")
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Section name '{section}' must use PascalCase (e.g., AbilityScores, CharacterInfo)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
path = sectionPath.Split('.').Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
path = sectionPath.Split('.').Where(p => !string.IsNullOrWhiteSpace(p)).ToList();
|
||||||
|
|
||||||
current = structure;
|
current = structure;
|
||||||
@ -305,6 +323,27 @@ ArmorClass (calculated) = 10 + DexterityMod
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
// For character files, the field syntax is different (no type)
|
||||||
|
if (fileType == "character")
|
||||||
|
{
|
||||||
|
var characterFieldMatch = Regex.Match(trimmed, @"^([\w\d_]+)\s*=\s*(.*)$");
|
||||||
|
if (characterFieldMatch.Success)
|
||||||
|
{
|
||||||
|
var name = characterFieldMatch.Groups[1].Value;
|
||||||
|
var value = characterFieldMatch.Groups[2].Value.Trim();
|
||||||
|
|
||||||
|
// Validate PascalCase for field names in character files
|
||||||
|
if (!Regex.IsMatch(name, @"^([A-Z][a-z0-9]*)+$") && !name.Contains("."))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Field name '{name}' must use PascalCase (e.g., CharacterName, MaxHealth)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
current[name] = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard field definition with type
|
||||||
var match = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$");
|
var match = Regex.Match(trimmed, @"^([\w\d_]+)\s+\(([\w\-]+)\)\s*=\s*(.*)$");
|
||||||
if (!match.Success)
|
if (!match.Success)
|
||||||
{
|
{
|
||||||
@ -313,22 +352,89 @@ ArmorClass (calculated) = 10 + DexterityMod
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = match.Groups[1].Value;
|
var fieldName = match.Groups[1].Value;
|
||||||
var type = match.Groups[2].Value;
|
var fieldType = match.Groups[2].Value;
|
||||||
var value = match.Groups[3].Value.Trim();
|
var fieldValue = match.Groups[3].Value.Trim();
|
||||||
|
|
||||||
if (!ValidTypes.Contains(type))
|
// Validate PascalCase for field names
|
||||||
|
if (!Regex.IsMatch(fieldName, @"^([A-Z][a-z0-9]*)+$"))
|
||||||
{
|
{
|
||||||
errors.Add(new SzfError(idx, $"Unknown field type: \"{type}\""));
|
errors.Add(new SzfError(idx, $"Field name '{fieldName}' must use PascalCase (e.g., CharacterName, MaxHealth)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
current[name] = new Dictionary<string, object> { ["type"] = type, ["value"] = value };
|
// Validate field type
|
||||||
|
if (!ValidTypes.Contains(fieldType))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Unknown field type: \"{fieldType}\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate specific field value formats
|
||||||
|
if (fieldType == "number" && !string.IsNullOrEmpty(fieldValue) &&
|
||||||
|
!Regex.IsMatch(fieldValue, @"^-?\d+(\.\d+)?$"))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Invalid number format: {fieldValue}"));
|
||||||
|
}
|
||||||
|
else if (fieldType == "bool" && !string.IsNullOrEmpty(fieldValue) &&
|
||||||
|
!Regex.IsMatch(fieldValue.ToLower(), @"^(true|false)$"))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(idx, $"Boolean values must be 'true' or 'false', got: {fieldValue}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
current[fieldName] = new Dictionary<string, object> { ["type"] = fieldType, ["value"] = fieldValue };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify required metadata sections based on file type
|
||||||
|
if (hasRequiredHeader)
|
||||||
|
{
|
||||||
|
if (!structure.ContainsKey("Metadata") && !(structure.ContainsKey("Metadata") && structure["Metadata"] is Dictionary<string, object>))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(0, "Missing [Metadata] section which is required for all file types"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var metadata = structure["Metadata"] as Dictionary<string, object>;
|
||||||
|
|
||||||
|
// Check file type specific requirements
|
||||||
|
if (fileType == "dataset")
|
||||||
|
{
|
||||||
|
CheckRequiredMetadata(metadata, new[] {"Name", "Type", "Version"}, errors);
|
||||||
|
}
|
||||||
|
else if (fileType == "character_template")
|
||||||
|
{
|
||||||
|
CheckRequiredMetadata(metadata, new[] {"Name", "Version"}, errors);
|
||||||
|
|
||||||
|
// Check for RequiredDatasets section if referenced
|
||||||
|
if (structure.ContainsValue("dataset-reference") || structure.ContainsValue("entry-reference"))
|
||||||
|
{
|
||||||
|
if (!structure.ContainsKey("RequiredDatasets") && !structure.ContainsKey("Required_Datasets"))
|
||||||
|
{
|
||||||
|
errors.Add(new SzfError(0, "Template uses dataset references but is missing [RequiredDatasets] section"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (fileType == "character")
|
||||||
|
{
|
||||||
|
CheckRequiredMetadata(metadata, new[] {"TemplateRef"}, errors);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (structure, errors);
|
return (structure, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CheckRequiredMetadata(Dictionary<string, object> metadata, string[] requiredFields, List<SzfError> errors)
|
||||||
|
{
|
||||||
|
foreach (var field in requiredFields)
|
||||||
|
{
|
||||||
|
if (!metadata.ContainsKey(field))
|
||||||
|
{
|
||||||
|
// Reporting the error on a general line since this check runs after initial parsing.
|
||||||
|
errors.Add(new SzfError(0, $"Missing required metadata field: '{field}'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private bool HasErrorOnLine(int lineIndex)
|
private bool HasErrorOnLine(int lineIndex)
|
||||||
{
|
{
|
||||||
return _errors.Any(e => e.Line == lineIndex);
|
return _errors.Any(e => e.Line == lineIndex);
|
||||||
|
Loading…
Reference in New Issue
Block a user