Build ServerShared without Unity and add AetherDb scripts

Aetheria.Shared.csproj compiles ServerShared with MessagePack, UniRx
core, Ink and Unity.Mathematics from source against .NET Standard 2.1
(Unity's API level) with no Unity defines; a clean build checks that the
simulation stays engine-independent. tools/AetherDb opens AetherDB.msgpack
through the game's own CultCache for file-based C# scripts; station-fit
checks every station hull can take a docking bay.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-11 23:23:20 +02:00
commit 02058a3cdf
6 changed files with 126 additions and 3 deletions

6
.gitignore vendored
View file

@ -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

View file

@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- The game simulation built without Unity: the headless counterpart of the Aetheria.Shared.Unity assembly.
Compiles ServerShared and its dependencies from source with no Unity defines, so a successful build is the
check that the simulation stays independent of the engine. Tools and test harnesses reference this project. -->
<PropertyGroup>
<!-- Unity compiles ServerShared against .NET Standard 2.1; newer targets also impose C# 11 ref-safety rules the vendored MessagePack predates -->
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<!-- UniRx's own switch for building outside Unity -->
<DefineConstants>$(DefineConstants);UniRxLibrary</DefineConstants>
<!-- Existing code warnings; the build is a Unity-independence check, not a lint pass -->
<NoWarn>$(NoWarn);CS0108;CS0114;CS0162;CS0168;CS0169;CS0219;CS0414;CS0618;CS0649;CS8321;CS8981;SYSLIB0021</NoWarn>
<Assets>$(MSBuildThisFileDirectory)..\Assets\</Assets>
<!-- Unity.Mathematics ships only as a Unity package; its sources come from the package cache Unity resolved -->
<MathematicsSource>$(MSBuildThisFileDirectory)..\Library\PackageCache\com.unity.mathematics@*\Unity.Mathematics\</MathematicsSource>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(Assets)Scripts\ServerShared\**\*.cs" />
<Compile Include="$(Assets)Plugins\MessagePack\**\*.cs" Exclude="$(Assets)Plugins\MessagePack\Unity\**" />
<Compile Include="$(Assets)Plugins\UniRx\Scripts\**\*.cs" Exclude="$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\**;$(Assets)Plugins\UniRx\Scripts\UnityWinRTBridge\**;$(Assets)Plugins\UniRx\Scripts\InternalUtil\MicroCoroutine.cs" />
<!-- The reactive collections live in UniRx's Unity bridge but fence their Unity parts behind UniRxLibrary -->
<Compile Include="$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveProperty.cs;$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveCollection.cs;$(Assets)Plugins\UniRx\Scripts\UnityEngineBridge\ReactiveDictionary.cs" />
<Compile Include="$(Assets)Plugins\Ink\**\*.cs" />
<Compile Include="$(MathematicsSource)**\*.cs" Exclude="$(MathematicsSource)math_unity_conversion.cs;$(MathematicsSource)PropertyAttributes.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<!-- Bundled by Unity; .NET Standard 2.1 lacks it -->
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" />
</ItemGroup>
</Project>

View file

@ -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.

View file

@ -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.");
}
}

View file

@ -0,0 +1,14 @@
<Project>
<!-- Applies to every script under tools/AetherDb. Scripts are file-based programs: pass a script path to dotnet run
with its file option. They load game data through the headless Aetheria.Shared build of ServerShared. -->
<PropertyGroup>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- File-based programs default to AOT, which disables the dynamic code MessagePack's resolvers generate -->
<PublishAot>false</PublishAot>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildThisFileDirectory)AetherDb.cs" />
<ProjectReference Include="$(MSBuildThisFileDirectory)..\..\Aetheria.Shared\Aetheria.Shared.csproj" />
</ItemGroup>
</Project>

View file

@ -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<DockingBayData>().ToArray();
var failures = 0;
foreach (var hull in db.Cache.GetAll<HullData>().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;