Give packs an identity so create can refuse missing deps and load in a stable order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 00:20:36 +03:00
co-authored by Cursor
parent e5a0ae5b9d
commit 263c94c55d
28 changed files with 851 additions and 56 deletions
+24 -1
View File
@@ -303,7 +303,30 @@ internal sealed class GameLoopService(
}
}
var packIds = mods.NormalizePackIds(extras);
IReadOnlyList<string> packIds;
try
{
packIds = mods.ResolveSelectedPacks(extras);
}
catch (PackDependencyException ex) when (ex.Code == PackDependencyException.MissingCode)
{
command.Result.TrySetResult(new SchoolCreationOutcome(
null,
SchoolCreationError.MissingMod,
ex.MissingPackId));
return;
}
catch (PackDependencyException)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.ModCycle));
return;
}
catch (ContentLoadException)
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.InvalidCatalog));
return;
}
if (!mods.PackExists(CatalogLoader.CorePackId))
{
command.Result.TrySetResult(new SchoolCreationOutcome(null, SchoolCreationError.InvalidCatalog));
+71 -4
View File
@@ -51,9 +51,9 @@ internal sealed class ModContent
return true;
}
public IReadOnlyList<ModPackInfo> ListPacks()
public IReadOnlyList<ModPackInfo> ListPacks(string locale)
{
var packs = new List<ModPackInfo> { new(CatalogLoader.CorePackId, Required: true) };
var packs = new List<ModPackInfo> { Describe(CatalogLoader.CorePackId, required: true, locale) };
if (!Directory.Exists(Root))
{
return packs;
@@ -67,7 +67,7 @@ internal sealed class ModContent
continue;
}
packs.Add(new ModPackInfo(id, Required: false));
packs.Add(Describe(id, required: false, locale));
}
return packs;
@@ -76,6 +76,30 @@ internal sealed class ModContent
public IReadOnlyList<string> NormalizePackIds(IReadOnlyList<string>? extraModIds) =>
CatalogLoader.NormalizePackOrder(extraModIds ?? []);
/// <summary>
/// Player extras plus <c>core</c>, reordered so each pack's <c>requires</c> load first.
/// Missing dependencies and cycles throw <see cref="PackDependencyException"/>.
/// </summary>
public IReadOnlyList<string> ResolveSelectedPacks(IReadOnlyList<string>? extraModIds)
{
var selected = NormalizePackIds(extraModIds);
var manifests = new Dictionary<string, PackManifest>(StringComparer.OrdinalIgnoreCase);
foreach (var packId in selected)
{
manifests[packId] = ReadManifest(packId);
}
return PackLoadOrder.Resolve(selected, manifests);
}
public PackManifest ReadManifest(string packId)
{
var jsonc = Path.Combine(PackPath(packId), "pack.jsonc");
var json = Path.Combine(PackPath(packId), "pack.json");
var path = File.Exists(jsonc) ? jsonc : File.Exists(json) ? json : null;
return path is null ? PackManifest.Empty : PackManifest.Parse(packId, File.ReadAllText(path));
}
public IReadOnlyList<ContentDocument> ReadDocuments(IReadOnlyList<string> packIds)
{
var documents = new List<ContentDocument>();
@@ -136,7 +160,50 @@ internal sealed class ModContent
public DefCatalog LoadCatalog(IReadOnlyList<string> packIds) => LoadCatalog(packIds, _logger);
private ModPackInfo Describe(string packId, bool required, string locale)
{
PackManifest manifest;
try
{
manifest = PackExists(packId) ? ReadManifest(packId) : PackManifest.Empty;
}
catch (ContentLoadException ex)
{
_logger.LogWarning(ex, "Could not read pack.jsonc for {PackId}.", packId);
manifest = PackManifest.Empty;
}
return new ModPackInfo(packId, required, ReadPackLabel(packId, locale), manifest.Version, manifest.Requires);
}
private string ReadPackLabel(string packId, string locale)
{
var jsonc = Path.Combine(PackPath(packId), "localizations", $"{locale}.jsonc");
var json = Path.Combine(PackPath(packId), "localizations", $"{locale}.json");
var path = File.Exists(jsonc) ? jsonc : File.Exists(json) ? json : null;
if (path is null)
{
return packId;
}
try
{
var table = PackManifest.ReadLocaleTable(File.ReadAllText(path), $"{packId}:localizations/{locale}");
return table.TryGetValue(packId, out var label) ? label : packId;
}
catch (ContentLoadException ex)
{
_logger.LogWarning(ex, "Could not read pack label for {PackId}.", packId);
return packId;
}
}
private string PackPath(string packId) => Path.Combine(Root, packId);
}
internal sealed record ModPackInfo(string Id, bool Required);
internal sealed record ModPackInfo(
string Id,
bool Required,
string Label,
string Version,
IReadOnlyList<string> Requires);
@@ -3,7 +3,10 @@ using HSchool.Simulation;
namespace HSchool.Server.Game;
/// <summary>What the supervisor reports back after trying to create a school.</summary>
internal readonly record struct SchoolCreationOutcome(SchoolState? School, SchoolCreationError Error)
internal readonly record struct SchoolCreationOutcome(
SchoolState? School,
SchoolCreationError Error,
string? MissingPackId = null)
{
public bool Succeeded => Error == SchoolCreationError.None && School is not null;
}
+7 -1
View File
@@ -4,7 +4,13 @@ namespace HSchool.Server.Game;
/// Immutable copy of a school, safe to hand to request threads. The live <c>School</c> object
/// never leaves its worker thread.
/// </summary>
internal sealed record SchoolState(int Id, string Name, DateTime GameTime, bool Running, byte SpeedIndex);
internal sealed record SchoolState(
int Id,
string Name,
DateTime GameTime,
bool Running,
byte SpeedIndex,
IReadOnlyList<string> ModIds);
/// <summary>Everything the main menu needs in one read.</summary>
internal sealed record SchoolsState(int MaxSchools, IReadOnlyList<SchoolState> Schools);
+4 -2
View File
@@ -94,7 +94,7 @@ internal sealed class SchoolWorker
_mods = mods;
_onFailed = onFailed;
_logger = logger;
_snapshot = new SchoolState(id, name, time, running, (byte)speedIndex);
_snapshot = new SchoolState(id, name, time, running, (byte)speedIndex, modIds ?? []);
}
public int Id => _id;
@@ -203,6 +203,7 @@ internal sealed class SchoolWorker
private void RunLoop(CancellationToken cancellationToken)
{
var packIds = _mods.NormalizePackIds(_modIds);
_logger.LogInformation("School {SchoolId} loading packs [{Packs}].", _id, string.Join(", ", packIds));
foreach (var packId in packIds)
{
if (!_mods.PackExists(packId))
@@ -577,7 +578,8 @@ internal sealed class SchoolWorker
school.Name,
school.Clock.Time,
school.Clock.IsRunning,
(byte)school.Clock.SpeedIndex));
(byte)school.Clock.SpeedIndex,
school.Catalog?.PackIds ?? _modIds ?? []));
Volatile.Write(ref _rosterSnapshot, school.Roster);
Volatile.Write(ref _applicantSnapshot, school.Applicants);
Volatile.Write(ref _timetableSnapshot, school.Timetable);