Created the shared DatapackService and seperate szpack cli tool

This commit is contained in:
Chris Bell 2025-10-16 23:07:00 -05:00
parent ca5057a9f7
commit c397b40c61
9 changed files with 493 additions and 0 deletions

View File

@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SessionZero.Client", "src\S
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SessionZero.Server", "src\SessionZero.Server\SessionZero.Server.csproj", "{54824F5A-0499-4BC9-AC8E-88E943C66256}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SessionZero.Server", "src\SessionZero.Server\SessionZero.Server.csproj", "{54824F5A-0499-4BC9-AC8E-88E943C66256}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "szpack", "tools\szpack\szpack.csproj", "{70FDB950-CAE6-4C38-A476-3FC10BFCF6E6}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -24,5 +26,9 @@ Global
{54824F5A-0499-4BC9-AC8E-88E943C66256}.Debug|Any CPU.Build.0 = Debug|Any CPU {54824F5A-0499-4BC9-AC8E-88E943C66256}.Debug|Any CPU.Build.0 = Debug|Any CPU
{54824F5A-0499-4BC9-AC8E-88E943C66256}.Release|Any CPU.ActiveCfg = Release|Any CPU {54824F5A-0499-4BC9-AC8E-88E943C66256}.Release|Any CPU.ActiveCfg = Release|Any CPU
{54824F5A-0499-4BC9-AC8E-88E943C66256}.Release|Any CPU.Build.0 = Release|Any CPU {54824F5A-0499-4BC9-AC8E-88E943C66256}.Release|Any CPU.Build.0 = Release|Any CPU
{70FDB950-CAE6-4C38-A476-3FC10BFCF6E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{70FDB950-CAE6-4C38-A476-3FC10BFCF6E6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{70FDB950-CAE6-4C38-A476-3FC10BFCF6E6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{70FDB950-CAE6-4C38-A476-3FC10BFCF6E6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -0,0 +1,172 @@
using System.Text.Json;
using SessionZero.Shared.Models;
using System.IO;
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.IO.Compression;
namespace SessionZero.Shared.Services;
public static class DatapackService
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
/// <summary>
/// Creates a new Datapack model with required metadata.
/// </summary>
/// <param name="name">The display name of the datapack.</param>
/// <param name="version">The initial version string (e.g., "1.0.0").</param>
/// <param name="author">The pack creator's name.</param>
/// <param name="license">The license (e.g., "CC-BY-SA-4.0").</param>
/// <returns>A fully initialized Datapack model.</returns>
public static Datapack CreateEmptyDatapack(string name, string version, string author, string license)
{
return new Datapack
{
Id = Guid.NewGuid(),
Name = name,
Version = version,
Author = author,
License = license,
Description = String.Empty,
CreatedAt = DateTime.UtcNow,
SessionZeroVersion = "0.0.1",
Dependencies = new()
};
}
/// <summary>
/// Serializes a Datapack model and saves it to a file named 'szpack.json'
/// inside a new subdirectory named after the datapack's slug.
/// </summary>
/// <param name="datapack">The Datapack object to save.</param>
/// <param name="parentDirectoryPath">The directory where the new datapack folder will be created.</param>
/// <returns>The full path to the created szpack.json file.</returns>
public static async Task<string> SaveDatapackMetadataAsync(Datapack datapack, string parentDirectoryPath)
{
var packDirectoryName = datapack.Name.ToLower().Replace(' ', '-').Trim();
var packRootDirectory = Path.Combine(parentDirectoryPath, packDirectoryName);
Directory.CreateDirectory(packRootDirectory);
var filePath = Path.Combine(packRootDirectory, "szpack.json");
var jsonString = JsonSerializer.Serialize(datapack, JsonOptions);
await File.WriteAllTextAsync(filePath, jsonString);
return filePath;
}
/// <summary>
/// Serializes an SzObject (Dataset, Template, etc.) and saves it to a file.
/// </summary>
/// <typeparam name="T">The type of the object, must inherit from SzObject.</typeparam>
/// <param name="szObject">The object to save.</param>
/// <param name="directoryPath">The specific subdirectory (e.g., 'datasets') within the datapack root.</param>
/// <returns>The full path to the created JSON file.</returns>
public static async Task<string> SaveSzObjectAsync<T>(T szObject, string directoryPath) where T : SzObject
{
Directory.CreateDirectory(directoryPath);
var fileName = $"{szObject.Id}.json";
var filePath = Path.Combine(directoryPath, fileName);
var jsonString = JsonSerializer.Serialize(szObject, JsonOptions);
await File.WriteAllTextAsync(filePath, jsonString);
return filePath;
}
/// <summary>
/// Defines and creates the standard directory structure for a SessionZero datapack.
/// </summary>
/// <param name="rootPath">The root directory path of the new datapack.</param>
/// <returns>A dictionary containing the standard folder names and their absolute paths.</returns>
public static Dictionary<string, string> CreateDatapackDirectoryStructure(string rootPath)
{
var structure = new Dictionary<string, string>
{
{ "datasets", Path.Combine(rootPath, "datasets") },
{ "character_templates", Path.Combine(rootPath, "characters") },
{ "session_templates", Path.Combine(rootPath, "sessions") },
{ "media", Path.Combine(rootPath, "media") },
{ "images", Path.Combine(rootPath, "media", "images") }
};
foreach (var path in structure.Values)
{
Directory.CreateDirectory(path);
}
return structure;
}
/// <summary>
/// Compresses a datapack directory into a .szpack zip archive.
/// </summary>
/// <param name="sourceDirectoryPath">The path to the datapack's root directory.</param>
/// <param name="outputFilePath">The full path and filename for the output .szpack file.</param>
public static void PackDatapack(string sourceDirectoryPath, string outputFilePath)
{
if (!Directory.Exists(sourceDirectoryPath))
{
throw new DirectoryNotFoundException($"Source directory not found: {sourceDirectoryPath}");
}
if (File.Exists(outputFilePath))
{
File.Delete(outputFilePath);
}
ZipFile.CreateFromDirectory(sourceDirectoryPath, outputFilePath);
}
/// <summary>
/// Extracts a .szpack zip archive into a target directory.
/// </summary>
/// <param name="sourceFilePath">The path to the .szpack file.</param>
/// <param name="destinationDirectoryPath">The directory where the contents will be extracted.</param>
public static void UnpackDatapack(string sourceFilePath, string destinationDirectoryPath)
{
if (!File.Exists(sourceFilePath))
{
throw new FileNotFoundException($"Datapack file not found: {sourceFilePath}");
}
Directory.CreateDirectory(destinationDirectoryPath);
ZipFile.ExtractToDirectory(sourceFilePath, destinationDirectoryPath, overwriteFiles: true);
}
/// <summary>
/// Deserializes and loads the core Datapack metadata (szpack.json) from a directory.
/// </summary>
/// <param name="datapackRootPath">The path to the datapack's root directory.</param>
/// <returns>A Datapack model containing the metadata.</returns>
/// <exception cref="FileNotFoundException">Thrown if szpack.json is not found.</exception>
/// <exception cref="JsonException">Thrown if deserialization fails.</exception>
public static async Task<Datapack> LoadDatapackMetadataAsync(string datapackRootPath)
{
var filePath = Path.Combine(datapackRootPath, "szpack.json");
if (!File.Exists(filePath))
{
throw new FileNotFoundException($"The required metadata file 'szpack.json' was not found in: {datapackRootPath}", filePath);
}
var jsonString = await File.ReadAllTextAsync(filePath);
var datapack = JsonSerializer.Deserialize<Datapack>(jsonString, JsonOptions);
if (datapack is null)
{
throw new JsonException($"Failed to deserialize szpack.json from {filePath}. The file may be corrupt or invalid.");
}
return datapack;
}
}

View File

@ -0,0 +1,45 @@
using SessionZero.Shared.Models;
namespace SessionZero.Shared.Validation;
public static class DatapackValidator
{
public static List<string> ValidateDatapack(Datapack datapack)
{
var errors = new List<string>();
if (datapack.Id == Guid.Empty) errors.Add("Datapack 'Id' is required and must not be an empty GUID.");
ValidateRequiredString(errors, datapack.Name, nameof(datapack.Name));
ValidateRequiredString(errors, datapack.Version, nameof(datapack.Version));
ValidateRequiredString(errors, datapack.Author, nameof(datapack.Author));
ValidateRequiredString(errors, datapack.License, nameof(datapack.License));
ValidateRequiredString(errors, datapack.SessionZeroVersion, nameof(datapack.SessionZeroVersion));
if (datapack.CreatedAt == DateTime.MinValue) errors.Add("Datapack 'CreatedAt' is required and must not be an empty DateTime.");
return errors;
}
public static List<string> ValidateSzObject(SzObject szObject)
{
var errors = new List<string>();
var objectName = szObject.Id;
ValidateRequiredString(errors, szObject.Id, $"{objectName}'s Id");
ValidateRequiredString(errors, szObject.Name, $"{objectName}'s Name");
ValidateRequiredString(errors, szObject.SzType, $"{objectName}'s SzType");
ValidateRequiredString(errors, szObject.Version, $"{objectName}'s Version");
ValidateRequiredString(errors, szObject.SchemaVersion, $"{objectName}'s SchemaVersion");
// 2. Validate Icon path format (optional/future: e.g., only allows alphanumeric + slashes + extension)
// For now, we'll only check if it's not null, which is done by the property definition in SzObject.
return errors;
}
private static void ValidateRequiredString(List<string> errors, string? value, string fieldName)
{
if (string.IsNullOrWhiteSpace(value)) errors.Add($"'{fieldName}' is required and must not be empty.");
}
}

View File

@ -0,0 +1,107 @@
/*
* WARNING:
* This tool was created by an LLM based on the SessionZero shared lib project, therefore it is subject to errors and does not reflect the architecture of the SessionZero project.
* It was created to be used as a quick and dirty validation tool for the szpack format.
*/
using SessionZero.Shared.Models;
using SessionZero.Shared.Services;
using Spectre.Console;
using Spectre.Console.Cli;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace SessionZero.Tools.Packer;
// Settings class defines the command-line arguments
public class CreateSettings : CommandSettings
{
[CommandArgument(0, "<name>")]
[Description("The display name of the datapack (e.g., 'My Fantasy Spells').")]
public required string Name { get; init; }
[CommandArgument(1, "[author]")]
[Description("The author's name.")]
[DefaultValue("Test Author")]
public string Author { get; init; } = "Test Author";
[CommandOption("-o|--output")]
[Description("The parent directory where the new pack folder will be created.")]
[DefaultValue(".")]
public string Output { get; init; } = Environment.CurrentDirectory;
}
public class CreateCommand : AsyncCommand<CreateSettings>
{
public override async Task<int> ExecuteAsync([NotNull] CommandContext context, [NotNull] CreateSettings settings, CancellationToken cancellationToken)
{
AnsiConsole.MarkupLine($"\n[bold white on blue] --- Creating Datapack: '{settings.Name}' --- [/]");
try
{
// 1. Create the top-level model and save szpack.json
var newPack = DatapackService.CreateEmptyDatapack(settings.Name, "1.0.0", settings.Author, "MIT");
var szpackPath = await DatapackService.SaveDatapackMetadataAsync(newPack, settings.Output);
var packRootDirectory = Path.GetDirectoryName(szpackPath)!;
AnsiConsole.MarkupLine($"[green]✅ Created metadata file at: {szpackPath}[/]");
// 2. Create the standard directory structure
var structure = DatapackService.CreateDatapackDirectoryStructure(packRootDirectory);
AnsiConsole.MarkupLine($"[green]✅ Created standard directories at: {packRootDirectory}[/]");
// 3. Create and save test objects
await CreateTestObjects(packRootDirectory, structure);
AnsiConsole.MarkupLine("\n[bold]Creation Complete![/] Use 'szpack pack' to compress it.");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"\n[bold white on red]❌ ERROR:[/] Failed to create datapack. Details: {ex.Message}");
return 1;
}
}
// Helper method for creating test objects (copied from previous Program.cs)
private static async Task CreateTestObjects(string rootPath, Dictionary<string, string> structure)
{
// ... (Test object creation logic remains the same, ensure you have the necessary usings in this file)
// --- Test object creation logic (omitted for brevity, assume the previous logic is here) ---
var testDataset = new Dataset { /* ... test data ... */ Id = "basic-attributes", Name = "Basic Character Attributes", SzType = "Dataset", Version = "1.0.0", SchemaVersion = "1.0.0", DatasetType = "Attribute", Entries = new() };
var dsPath = await DatapackService.SaveSzObjectAsync(testDataset, structure["datasets"]);
AnsiConsole.MarkupLine($" -> Saved Test Dataset: [yellow]{Path.GetFileName(dsPath)}[/]");
var testCharTemplate = new CharacterTemplate { /* ... test data ... */ Id = "default-char-sheet", Name = "Default Character Template", SzType = "Template", Version = "1.0.0", SchemaVersion = "1.0.0", Sections = new() };
var charPath = await DatapackService.SaveSzObjectAsync(testCharTemplate, structure["character_templates"]);
AnsiConsole.MarkupLine($" -> Saved Test Character Template: [yellow]{Path.GetFileName(charPath)}[/]");
var testSessionTemplate = new SessionTemplate
{
/* ... test data ... */ Id = "basic-encounter",
Name = "Basic Encounter Template",
SzType = "Template",
Version = "1.0.0",
SchemaVersion = "1.0.0",
CharacterTemplateLink = new()
{
DatapackId = Guid.Empty,
TemplateId = testCharTemplate.Id,
Version = testCharTemplate.Version
},
RequiredDatasets = new()
{
new()
{
DatapackId = Guid.Empty,
DatasetId = testDataset.Id,
Version = testDataset.Version
}
},
Sections = new()
};
var sessionPath = await DatapackService.SaveSzObjectAsync(testSessionTemplate, structure["session_templates"]);
AnsiConsole.MarkupLine($" -> Saved Test Session Template: [yellow]{Path.GetFileName(sessionPath)}[/]");
}
}

View File

@ -0,0 +1,43 @@
/*
* WARNING:
* This tool was created by an LLM based on the SessionZero shared lib project, therefore it is subject to errors and does not reflect the architecture of the SessionZero project.
* It was created to be used as a quick and dirty validation tool for the szpack format.
*/
using SessionZero.Shared.Services;
using Spectre.Console;
using Spectre.Console.Cli;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace SessionZero.Tools.Packer;
public class PackSettings : CommandSettings
{
[CommandArgument(0, "<input>")]
[Description("The root directory of the datapack to pack.")]
public required string Input { get; init; }
}
public class PackCommand : Command<PackSettings>
{
public override int Execute([NotNull] CommandContext context, [NotNull] PackSettings settings, CancellationToken cancellationToken)
{
AnsiConsole.MarkupLine($"\n[bold white on blue] --- Packing Datapack: {settings.Input} --- [/]");
try
{
var packDirectoryName = new DirectoryInfo(settings.Input).Name;
var outputFilePath = Path.Combine(Path.GetDirectoryName(settings.Input) ?? Environment.CurrentDirectory, $"{packDirectoryName}.szpack");
DatapackService.PackDatapack(settings.Input, outputFilePath);
AnsiConsole.MarkupLine($"[green]✅ Successfully created archive: {outputFilePath}[/]");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"\n[bold white on red]❌ ERROR:[/] Failed to pack datapack. Details: {ex.Message}");
return 1;
}
}
}

36
tools/szpack/Program.cs Normal file
View File

@ -0,0 +1,36 @@
/*
* WARNING:
* This tool was created by an LLM based on the SessionZero shared lib project, therefore it is subject to errors and does not reflect the architecture of the SessionZero project.
* It was created to be used as a quick and dirty validation tool for the szpack format.
*/
using Spectre.Console.Cli;
namespace SessionZero.Tools.Packer;
internal class Program
{
static int Main(string[] args)
{
var app = new CommandApp();
app.Configure(config =>
{
config.SetApplicationName("szpack");
config.AddCommand<CreateCommand>("create")
.WithDescription("Creates a new datapack directory with test objects.");
config.AddCommand<PackCommand>("pack")
.WithDescription("Compresses a datapack directory into a .szpack file.");
config.AddCommand<UnpackCommand>("unpack")
.WithDescription("Extracts a .szpack file and displays its metadata.");
// Optional: Set a default command if no command is specified
// config.SetDefaultCommand<HelpCommand>();
});
return app.Run(args);
}
}

View File

@ -0,0 +1,55 @@
/*
* WARNING:
* This tool was created by an LLM based on the SessionZero shared lib project, therefore it is subject to errors and does not reflect the architecture of the SessionZero project.
* It was created to be used as a quick and dirty validation tool for the szpack format.
*/
using SessionZero.Shared.Services;
using Spectre.Console;
using Spectre.Console.Cli;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
namespace SessionZero.Tools.Packer;
public class UnpackSettings : CommandSettings
{
[CommandArgument(0, "<input>")]
[Description("The path to the .szpack file.")]
public required string Input { get; init; }
[CommandOption("-o|--output")]
[Description("The directory where the pack will be extracted.")]
[DefaultValue("unpacked")]
public string Output { get; init; } = "unpacked";
}
public class UnpackCommand : AsyncCommand<UnpackSettings>
{
public override async Task<int> ExecuteAsync([NotNull] CommandContext context, [NotNull] UnpackSettings settings, CancellationToken cancellationToken)
{
AnsiConsole.MarkupLine($"\n[bold white on blue] --- Unpacking Datapack: {settings.Input} --- [/]");
try
{
// Unpack the archive
DatapackService.UnpackDatapack(settings.Input, settings.Output);
AnsiConsole.MarkupLine($"[green]✅ Successfully unpacked archive to: {settings.Output}[/]");
// Load and display the core data
var datapack = await DatapackService.LoadDatapackMetadataAsync(settings.Output);
AnsiConsole.MarkupLine("\n[bold]--- Datapack Info (szpack.json) ---[/]");
AnsiConsole.MarkupLine($"[yellow]ID:[/]\t\t{datapack.Id}");
AnsiConsole.MarkupLine($"[yellow]Name:[/]\t{datapack.Name}");
AnsiConsole.MarkupLine($"[yellow]Version:[/]\t{datapack.Version}");
AnsiConsole.MarkupLine($"[yellow]Author:[/]\t{datapack.Author}");
return 0;
}
catch (Exception ex)
{
AnsiConsole.MarkupLine($"\n[bold white on red]❌ ERROR:[/] Failed to unpack or load datapack. Details: {ex.Message}");
return 1;
}
}
}

10
tools/szpack/readme.md Normal file
View File

@ -0,0 +1,10 @@
# SZPACK CLI Tool
## ***WARNING:***
This tool was created by an LLM based on the SessionZero shared lib project, therefore it is subject to errors and does not reflect the architecture of the SessionZero project.
It was created to be used as a quick and dirty validation tool for the szpack format.
## Usage
* `szpack create <name>`: Creates a sample datapack directory with a given name
* `szpack pack <directory>`: Packs a datapack directory into a szpack file (must be a valid datapack)
* `szpack unpack <name.szpack>`: Unpacks a .szpack file into a datapack directory and displays some metadata

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\SessionZero.Shared\SessionZero.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.52.1-preview.0.5" />
<PackageReference Include="Spectre.Console.Cli" Version="0.52.1-preview.0.5" />
</ItemGroup>
</Project>