diff --git a/.gitignore b/.gitignore
index e82f6272d..50e57f2ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,12 @@ ExportedObj/
# Per-user editor state
/[Uu]ser[Ss]ettings/
+# .NET build output outside Unity
+/Aetheria.Shared/bin/
+/Aetheria.Shared/obj/
+/tools/**/bin/
+/tools/**/obj/
+
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
diff --git a/Aetheria.Shared/Aetheria.Shared.csproj b/Aetheria.Shared/Aetheria.Shared.csproj
new file mode 100644
index 000000000..b546290bd
--- /dev/null
+++ b/Aetheria.Shared/Aetheria.Shared.csproj
@@ -0,0 +1,35 @@
+
+
+
+
+ netstandard2.1
+ 9.0
+ disable
+ disable
+ true
+ false
+
+ $(DefineConstants);UniRxLibrary
+
+ $(NoWarn);CS0108;CS0114;CS0162;CS0168;CS0169;CS0219;CS0414;CS0618;CS0649;CS8321;CS8981;SYSLIB0021
+ $(MSBuildThisFileDirectory)..\Assets\
+
+ $(MSBuildThisFileDirectory)..\Library\PackageCache\com.unity.mathematics@*\Unity.Mathematics\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/three-gates-scope.md b/docs/three-gates-scope.md
index e9e5452ed..a99c86073 100644
--- a/docs/three-gates-scope.md
+++ b/docs/three-gates-scope.md
@@ -59,9 +59,12 @@ AI share it, so AI accuracy is no longer superhuman by construction.
pickup (`ShieldManager`), `TractorBeam`, `Mine`. Not blockers.
**Invariant: game simulation runs independently of Unity.** `ServerShared`
-enforces this at compile time (`Aetheria.Shared.Unity.asmdef` sets
-`noEngineReferences`), but Unity-owned hit resolution breaks it behaviorally:
-the simulation compiles without Unity yet cannot resolve combat without it.
+cannot reference UnityEngine (`Aetheria.Shared.Unity.asmdef` sets
+`noEngineReferences`), and `Aetheria.Shared/Aetheria.Shared.csproj` builds it
+from source with the plain .NET SDK, so a clean headless build is the check.
+Tools (`tools/AetherDb`) and future test harnesses reference that project.
+Unity-owned hit resolution still breaks the invariant behaviorally: the
+simulation builds without Unity yet cannot resolve combat without it.
That was a velocity compromise. Moving hit authority into fire control
restores the invariant, and lets the planner and a balance harness simulate
combat.
@@ -95,3 +98,9 @@ Economy simulation, hauling, mining yield, crafting/blueprints, reputation
changes, story placement, multiplayer and `Economy.Server`, a headless
server deployment, and the CultMesh/daemon rebuild (parked in
`F:\Projects\AetheriaEve`).
+
+Post-ship direction for items: generic item classes (docking bays, cargo
+bays) become faction-neutral blueprints that branded manufacturing runs
+reference, so no faction's absence from a galaxy can remove a required item
+class. Until then, database variety has to guarantee it; `LoadoutGenerator`
+logs whenever a required item falls back to an unavailable manufacturer.
diff --git a/tools/AetherDb/AetherDb.cs b/tools/AetherDb/AetherDb.cs
new file mode 100644
index 000000000..c31fcf6ee
--- /dev/null
+++ b/tools/AetherDb/AetherDb.cs
@@ -0,0 +1,37 @@
+using System;
+using System.IO;
+
+// Opens GameData/AetherDB.msgpack through the game's own CultCache and data types, for inspection and editing scripts.
+// Close the editor's Database Tools before saving from a script, or its next Save will overwrite the script's changes.
+public sealed class AetherDb
+{
+ public string Root { get; }
+ public CultCache Cache { get; }
+ private readonly SingleFileMessagePackBackingStore _store;
+
+ private AetherDb(string root)
+ {
+ Root = root;
+ RegisterResolver.Register();
+ var gameData = Path.Combine(root, "GameData");
+ _store = new SingleFileMessagePackBackingStore(Path.Combine(gameData, "AetherDB.msgpack"));
+ Cache = new CultCache();
+ Cache.AddBackingStore(_store);
+ Cache.AddBackingStore(new MultiFileMessagePackBackingStore(gameData), typeof(NameFile));
+ Cache.PullAllBackingStores();
+ }
+
+ // With no root given, walks up from the working directory to the repository
+ public static AetherDb Open(string root = null) => new AetherDb(root ?? FindRoot());
+
+ // Writes every entry back to AetherDB.msgpack, as the editor's Save button does
+ public void Save() => _store.PushAll();
+
+ private static string FindRoot()
+ {
+ for (var dir = new DirectoryInfo(Environment.CurrentDirectory); dir != null; dir = dir.Parent)
+ if (File.Exists(Path.Combine(dir.FullName, "GameData", "AetherDB.msgpack")))
+ return dir.FullName;
+ throw new DirectoryNotFoundException("Run from inside the Aetheria repository.");
+ }
+}
diff --git a/tools/AetherDb/Directory.Build.props b/tools/AetherDb/Directory.Build.props
new file mode 100644
index 000000000..1a7b942e9
--- /dev/null
+++ b/tools/AetherDb/Directory.Build.props
@@ -0,0 +1,14 @@
+
+
+
+ disable
+ disable
+
+ false
+
+
+
+
+
+
diff --git a/tools/AetherDb/scripts/station-fit.cs b/tools/AetherDb/scripts/station-fit.cs
new file mode 100644
index 000000000..fd1323daf
--- /dev/null
+++ b/tools/AetherDb/scripts/station-fit.cs
@@ -0,0 +1,22 @@
+// Checks that some docking bay fits every station hull, which LoadoutGenerator.GenerateStationLoadout requires.
+// Exits with the number of station hulls that cannot take any docking bay.
+using System;
+using System.Linq;
+
+var db = AetherDb.Open();
+var bays = db.Cache.GetAll().ToArray();
+var failures = 0;
+foreach (var hull in db.Cache.GetAll().Where(h => h.HullType == HullType.Station))
+{
+ // Hardpoint gear is equipped before the docking bay, so the worst case leaves only the non-hardpoint interior free
+ var worstCase = new Shape(hull.Shape.Width, hull.Shape.Height);
+ foreach (var v in hull.InteriorCells.Coordinates) worstCase[v] = true;
+ foreach (var hardpoint in hull.Hardpoints)
+ foreach (var v in hardpoint.Shape.Coordinates)
+ worstCase[hardpoint.Position + v] = false;
+
+ var fitting = bays.Where(b => b.Shape.FitsWithin(worstCase, out _, out _)).Select(b => b.Name).ToArray();
+ if (fitting.Length == 0) failures++;
+ Console.WriteLine($"{hull.Name}: {(fitting.Length == 0 ? "NO docking bay fits" : string.Join(", ", fitting))}");
+}
+return failures;