diff --git a/server/NeonSprawl.Server/CatalogSchemaRegistry.cs b/server/NeonSprawl.Server/CatalogSchemaRegistry.cs
new file mode 100644
index 0000000..aa1d287
--- /dev/null
+++ b/server/NeonSprawl.Server/CatalogSchemaRegistry.cs
@@ -0,0 +1,34 @@
+using System.Collections.Concurrent;
+using Json.Schema;
+
+namespace NeonSprawl.Server;
+
+///
+/// Thread-safe registration for (JsonSchema.Net registry is not concurrent).
+/// Used by content catalog loaders when parallel integration tests spin up multiple hosts.
+///
+internal static class CatalogSchemaRegistry
+{
+ private static readonly Lock RegisterLock = new();
+ private static readonly ConcurrentDictionary SchemasByPath =
+ new(StringComparer.Ordinal);
+
+ /// Parses once per process and registers it under a global lock.
+ public static JsonSchema GetOrRegisterFromFile(string schemaFilePath)
+ {
+ var fullPath = Path.GetFullPath(schemaFilePath);
+ if (SchemasByPath.TryGetValue(fullPath, out var cached))
+ return cached;
+
+ lock (RegisterLock)
+ {
+ if (SchemasByPath.TryGetValue(fullPath, out cached))
+ return cached;
+
+ var schema = JsonSchema.FromText(File.ReadAllText(fullPath));
+ SchemaRegistry.Global.Register(schema);
+ SchemasByPath[fullPath] = schema;
+ return schema;
+ }
+ }
+}
diff --git a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalogLoader.cs b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalogLoader.cs
index b3a5c78..f43a8f4 100644
--- a/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalogLoader.cs
+++ b/server/NeonSprawl.Server/Game/Crafting/RecipeDefinitionCatalogLoader.cs
@@ -45,12 +45,8 @@ public static class RecipeDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
- var ioSchemaText = File.ReadAllText(recipeIoRowSchemaPath);
- var defSchemaText = File.ReadAllText(recipeDefSchemaPath);
- var ioSchema = JsonSchema.FromText(ioSchemaText);
- SchemaRegistry.Global.Register(ioSchema);
- var defSchema = JsonSchema.FromText(defSchemaText);
- SchemaRegistry.Global.Register(defSchema);
+ var ioSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(recipeIoRowSchemaPath);
+ var defSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(recipeDefSchemaPath);
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
diff --git a/server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs b/server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs
index 7a3b464..b510ede 100644
--- a/server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs
+++ b/server/NeonSprawl.Server/Game/Encounters/RewardTableDefinitionCatalogLoader.cs
@@ -44,10 +44,8 @@ public static class RewardTableDefinitionCatalogLoader
if (errors.Count > 0)
ThrowIfAny(errors);
- var grantRowSchema = JsonSchema.FromText(File.ReadAllText(rewardGrantRowSchemaPath));
- SchemaRegistry.Global.Register(grantRowSchema);
- var tableSchema = JsonSchema.FromText(File.ReadAllText(rewardTableSchemaPath));
- SchemaRegistry.Global.Register(tableSchema);
+ var grantRowSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(rewardGrantRowSchemaPath);
+ var tableSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(rewardTableSchemaPath);
var evalOptions = new EvaluationOptions { OutputFormat = OutputFormat.List };
diff --git a/server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs b/server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs
index 7d099f0..f570220 100644
--- a/server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs
+++ b/server/NeonSprawl.Server/Game/Quests/QuestDefinitionCatalogLoader.cs
@@ -9,7 +9,6 @@ namespace NeonSprawl.Server.Game.Quests;
/// Loads and validates content/quests/*_quests.json using the same rules as scripts/validate_content.py (NEO-113).
public static class QuestDefinitionCatalogLoader
{
- private static readonly Lock SchemaLoadLock = new();
private static JsonSchema? _questObjectiveDefSchema;
private static JsonSchema? _questStepDefSchema;
private static JsonSchema? _questDefSchema;
@@ -352,23 +351,12 @@ public static class QuestDefinitionCatalogLoader
string questStepDefSchemaPath,
string questDefSchemaPath)
{
- lock (SchemaLoadLock)
- {
- if (_questDefSchema is not null)
- return;
+ if (_questDefSchema is not null)
+ return;
- var objectiveSchema = JsonSchema.FromText(File.ReadAllText(questObjectiveDefSchemaPath));
- SchemaRegistry.Global.Register(objectiveSchema);
- _questObjectiveDefSchema = objectiveSchema;
-
- var stepSchema = JsonSchema.FromText(File.ReadAllText(questStepDefSchemaPath));
- SchemaRegistry.Global.Register(stepSchema);
- _questStepDefSchema = stepSchema;
-
- var defSchema = JsonSchema.FromText(File.ReadAllText(questDefSchemaPath));
- SchemaRegistry.Global.Register(defSchema);
- _questDefSchema = defSchema;
- }
+ _questObjectiveDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questObjectiveDefSchemaPath);
+ _questStepDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questStepDefSchemaPath);
+ _questDefSchema = CatalogSchemaRegistry.GetOrRegisterFromFile(questDefSchemaPath);
}
private static void ThrowIfAny(List errors)