Add HSchool.Content project for JSONC definitions, catalog, and map validation. Update solution structure to include new content and tests projects. Enhance school management to support mod packs and map instances, ensuring proper loading and validation. Revise documentation to reflect these changes and update tests for new functionality.
ci / server (push) Failing after 3m36s
ci / client (push) Successful in 13s

This commit is contained in:
Leonid Pershin
2026-08-18 14:35:09 +03:00
parent 37c39a3beb
commit 1bc75244e8
55 changed files with 2289 additions and 59 deletions
+94
View File
@@ -0,0 +1,94 @@
using HSchool.Content;
using HSchool.Simulation;
using Microsoft.Extensions.Options;
namespace HSchool.Server.Game;
/// <summary>
/// Reads <c>mods/&lt;id&gt;/</c> from disk and hands the files to <see cref="CatalogLoader"/>.
/// Content itself never sees these paths.
/// </summary>
internal sealed class ModContent
{
private readonly CatalogLoader _loader = new();
private readonly ILogger<ModContent> _logger;
public ModContent(IOptions<SimulationOptions> options, IHostEnvironment environment, ILogger<ModContent> logger)
{
_logger = logger;
var configured = options.Value.ModsDirectory;
Root = Path.IsPathRooted(configured)
? configured
: Path.GetFullPath(Path.Combine(environment.ContentRootPath, configured));
logger.LogInformation("Mod packs directory is {Directory}.", Root);
}
public string Root { get; }
public bool PackExists(string packId) => Directory.Exists(PackPath(packId));
public IReadOnlyList<string> NormalizePackIds(IReadOnlyList<string>? extraModIds) =>
CatalogLoader.NormalizePackOrder(extraModIds ?? []);
public IReadOnlyList<ContentDocument> ReadDocuments(IReadOnlyList<string> packIds)
{
var documents = new List<ContentDocument>();
foreach (var packId in packIds)
{
var packRoot = PackPath(packId);
if (!Directory.Exists(packRoot))
{
throw new SchoolContentUnavailableException($"Mod folder '{packId}' is missing under {Root}.");
}
foreach (var path in Directory.EnumerateFiles(packRoot, "*.*", SearchOption.AllDirectories))
{
if (!path.EndsWith(".jsonc", StringComparison.OrdinalIgnoreCase)
&& !path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relative = Path.GetRelativePath(packRoot, path).Replace('\\', '/');
documents.Add(new ContentDocument(packId, relative, File.ReadAllText(path)));
}
}
return documents;
}
public DefCatalog LoadCatalog(IReadOnlyList<string> packIds, ILogger workerLog)
{
var documents = ReadDocuments(packIds);
try
{
return _loader.Load(packIds, documents, new LoggerContentLog(workerLog));
}
catch (ContentLoadException ex)
{
throw new SchoolContentUnavailableException(ex.Message, ex);
}
}
public MapLayout LoadMap(IReadOnlyList<string> packIds, MapLayout? saved)
{
if (saved is not null)
{
return saved;
}
var documents = ReadDocuments(packIds);
var map = CatalogLoader.LastDefaultMap(packIds, documents);
if (map is null)
{
throw new SchoolContentUnavailableException(
$"No maps/default.jsonc found for packs [{string.Join(", ", packIds)}].");
}
return map;
}
private string PackPath(string packId) => Path.Combine(Root, packId);
}