Own regular sector topology in daemon

This commit is contained in:
Meta Vens 2026-07-17 09:50:05 +01:00
commit 0bad9ea78e
6 changed files with 406 additions and 17 deletions

View file

@ -34,6 +34,11 @@ else if (args.Contains("--tutorial-topology", StringComparer.Ordinal))
checks.RunTutorialTopology();
Console.WriteLine("Daemon tutorial topology smoke passed.");
}
else if (args.Contains("--regular-topology", StringComparer.Ordinal))
{
checks.RunRegularTopology();
Console.WriteLine("Daemon regular-sector topology smoke passed.");
}
else if (args.Contains("--zone-bodies", StringComparer.Ordinal))
{
checks.RunZoneBodies();
@ -98,6 +103,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
}
public void RunTutorialTopology() => RunCheck(TutorialTopologyIsDeterministicConnectedAndRoleOwned);
public void RunRegularTopology() => RunCheck(RegularTopologyOwnsFossilSectorStructure);
public void RunZoneBodies() => RunCheck(GeneratedZoneBodiesPreserveFossilHierarchy);
public void RunTutorialWorld() => RunCheck(TutorialWorldMaterializesEveryTopologyZone);
public void RunTutorialMaterialization() => RunCheck(TutorialPopulationPreservesFossilEntityMechanics);
@ -153,6 +159,92 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
}
}
private static void RegularTopologyOwnsFossilSectorStructure()
{
var factions = Enumerable.Range(0, 12)
.Select(index => new AetheriaDaemonTutorialFactionInput(
$"faction.{index}", $"Faction {index}", $"F{index}", 3 + index % 3)
{
BossHullItemKey = index < 3 ? $"boss-hull-{index}" : "",
TrainingNames = Enumerable.Range(0, 256)
.Select(value => string.Concat(
(char)('a' + value % 26),
(char)('a' + value / 26 % 26),
(char)('a' + index % 26),
(char)('a' + (value * 7 + index) % 26),
(char)('a' + (value * 11 + index) % 26),
(char)('a' + (value * 17 + index) % 26)))
.ToArray()
})
.ToArray();
const uint seed = 0x5EC7_0206u;
var first = AetheriaDaemonRegularTopologyGenerator.GenerateFossil(factions, seed);
var second = AetheriaDaemonRegularTopologyGenerator.GenerateFossil(factions, seed);
RequireEqual(196, first.Zones.Count,
"regular generation must use the fossil 196-zone sector setting");
RequireEqual(12, first.HomeZoneByFactionKey.Count,
"regular generation must place all twelve selected corporations");
RequireEqual(3, first.BossZoneByFactionKey.Count,
"regular generation must place the fossil three boss corporations");
Require(first.EntranceZoneIndex >= 0 && first.ExitZoneIndex >= 0 &&
first.EntranceZoneIndex != first.ExitZoneIndex,
"regular generation must own distinct entrance and exit zones");
Require(first.Zones.All(zone => !string.IsNullOrWhiteSpace(zone.Name)),
"regular generation must name every owned and catalogue zone");
Require(first.Zones.All(zone => zone.AdjacentZoneIndices.Count > 0 &&
zone.AdjacentZoneIndices.All(adjacent =>
first.Zones[adjacent].AdjacentZoneIndices.Contains(zone.ZoneIndex))),
"regular generation must publish one connected undirected link graph");
int[] DistancesFrom(int source)
{
var distances = Enumerable.Repeat(-1, first.Zones.Count).ToArray();
var queue = new Queue<int>();
distances[source] = 0;
queue.Enqueue(source);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var adjacent in first.Zones[current].AdjacentZoneIndices)
{
if (distances[adjacent] >= 0) continue;
distances[adjacent] = distances[current] + 1;
queue.Enqueue(adjacent);
}
}
return distances;
}
var isolation = first.Zones
.Select(zone => DistancesFrom(zone.ZoneIndex).Sum())
.ToArray();
RequireEqual(isolation.Max(), isolation[first.ExitZoneIndex],
"regular exit must be a maximally isolated zone");
var exitDistances = DistancesFrom(first.ExitZoneIndex);
RequireEqual(exitDistances.Max(), exitDistances[first.EntranceZoneIndex],
"regular entrance must be furthest from the exit");
Require(first.DiscoveredZoneIndices.OrderBy(value => value).SequenceEqual(
new[] { first.EntranceZoneIndex }
.Concat(first.Zones[first.EntranceZoneIndex].AdjacentZoneIndices)
.Distinct()
.OrderBy(value => value)),
"regular discovery must begin with only the entrance and its neighbors");
foreach (var home in first.HomeZoneByFactionKey)
Require(first.Zones[home.Value].FactionKeys.Contains(home.Key),
"each regular corporation must be present at its daemon-owned home zone");
Require(first.Zones.Select(zone => (zone.X, zone.Y, Links: string.Join(',', zone.AdjacentZoneIndices),
zone.OwnerFactionKey, zone.Name)).SequenceEqual(
second.Zones.Select(zone => (zone.X, zone.Y, Links: string.Join(',', zone.AdjacentZoneIndices),
zone.OwnerFactionKey, zone.Name))) &&
first.HomeZoneByFactionKey.OrderBy(value => value.Key).SequenceEqual(
second.HomeZoneByFactionKey.OrderBy(value => value.Key)) &&
first.BossZoneByFactionKey.OrderBy(value => value.Key).SequenceEqual(
second.BossZoneByFactionKey.OrderBy(value => value.Key)),
"regular topology, faction placement, boss placement, and names must be seed deterministic");
}
private static void TutorialPopulationPreservesFossilEntityMechanics()
{
var catalog = TutorialPopulationCatalog(out var factions);
@ -580,6 +672,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
[
PositiveGravityDepthAttractsAndProjectsAsAWell,
TutorialTopologyIsDeterministicConnectedAndRoleOwned,
RegularTopologyOwnsFossilSectorStructure,
GeneratedZoneBodiesPreserveFossilHierarchy,
TutorialWorldMaterializesEveryTopologyZone,
GeneratedOrbitsAdvanceOrbitalWorldTruth,

View file

@ -0,0 +1,257 @@
public sealed record AetheriaDaemonRegularTopologySettings(
int ZoneCount,
float LinkDensity,
int MegaCount,
int BossCount)
{
public static AetheriaDaemonRegularTopologySettings Fossil { get; } =
new(196, 0.5f, 12, 3);
}
/// <summary>
/// Daemon-owned port of the fossil non-tutorial Galaxy constructor. It owns
/// regular-sector corporation selection, entrance/exit, boss chokepoints,
/// full-radius influence, discovery, and names. Zone contents consume this
/// graph and cannot revise it.
/// </summary>
public static class AetheriaDaemonRegularTopologyGenerator
{
public static AetheriaDaemonTutorialTopology GenerateFossil(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> availableFactions,
uint seed,
AetheriaDaemonRegularTopologySettings? settings = null)
{
settings ??= AetheriaDaemonRegularTopologySettings.Fossil;
if (settings.ZoneCount < 3)
throw new ArgumentOutOfRangeException(nameof(settings), "Regular topology requires at least three zones.");
if (settings.MegaCount < 1)
throw new ArgumentOutOfRangeException(nameof(settings), "Regular topology requires at least one corporation.");
if (availableFactions == null || availableFactions.Count == 0)
throw new InvalidDataException("Regular topology requires typed corporations.");
var random = new CultMath.Random(seed);
var factions = availableFactions
.OrderBy(_ => random.NextFloat())
.Take(settings.MegaCount)
.ToArray();
var points = AetheriaDaemonTutorialTopologyGenerator.GeneratePoints(
settings.ZoneCount, ref random, MainCloudDensity);
var links = AetheriaDaemonTutorialTopologyGenerator.DelaunayLinks(points);
AetheriaDaemonTutorialTopologyGenerator.PruneLinks(
points, links, settings.LinkDensity, MainCloudDensity);
var distances = AetheriaDaemonTutorialTopologyGenerator.BuildDistances(points.Length, links);
var exit = AetheriaDaemonTutorialTopologyGenerator.MaxZone(points.Length, zone =>
Enumerable.Range(0, points.Length).Sum(other => distances[zone, other]));
var entrance = AetheriaDaemonTutorialTopologyGenerator.MaxZone(points.Length, zone =>
distances[exit, zone]);
var bossZones = PlaceBosses(
factions, settings.BossCount, entrance, exit, links, distances);
var homes = PlaceHomes(factions, entrance, exit, bossZones, distances);
var zoneFactions = new string[points.Length][];
var owners = new string[points.Length];
for (var zoneIndex = 0; zoneIndex < points.Length; zoneIndex++)
{
zoneFactions[zoneIndex] = factions
.Where(faction => distances[zoneIndex, homes[faction.CorporationKey]] <= faction.InfluenceDistance)
.Select(faction => faction.CorporationKey)
.ToArray();
owners[zoneIndex] = factions
.Where(faction => distances[zoneIndex, homes[faction.CorporationKey]] <= faction.InfluenceDistance)
.OrderBy(faction => distances[zoneIndex, homes[faction.CorporationKey]])
.ThenBy(faction => Array.IndexOf(factions, faction))
.Select(faction => faction.CorporationKey)
.FirstOrDefault() ?? "";
}
var discovered = new[] { entrance }
.Concat(AetheriaDaemonTutorialTopologyGenerator.Neighbors(entrance, links))
.Distinct()
.OrderBy(index => index)
.ToArray();
var nameSeed = (uint)random.NextInt(1, int.MaxValue);
var zoneNames = AetheriaDaemonTutorialTopologyGenerator.GenerateNames(
factions, owners, nameSeed, order: 4);
var zones = Enumerable.Range(0, points.Length)
.Select(index => new AetheriaDaemonTutorialZoneTopology(
index,
zoneNames[index],
points[index].X,
points[index].Y,
AetheriaDaemonTutorialTopologyGenerator.Neighbors(index, links).OrderBy(value => value).ToArray(),
zoneFactions[index],
owners[index]))
.ToArray();
return new AetheriaDaemonTutorialTopology(
seed, 91.29439f, entrance, discovered, homes, zones)
{
ExitZoneIndex = exit,
BossZoneByFactionKey = bossZones
};
}
public static IReadOnlyList<AetheriaDaemonTutorialFactionInput> ResolveFossilFactions(
GameCult.Aetheria.State.Verse.AetheriaRuntimeCatalogSnapshot catalog)
{
if (catalog == null) throw new ArgumentNullException(nameof(catalog));
return (catalog.Corporations ?? Array.Empty<GameCult.Aetheria.State.Verse.AetheriaRuntimeCorporation>())
.Where(corporation => corporation != null && !string.IsNullOrWhiteSpace(corporation.CorporationKey))
.Select(corporation =>
{
var names = catalog.FindNameFile(corporation.GeonameFileKey);
if (names == null || names.Names.Count == 0)
throw new InvalidDataException(
$"Regular-sector corporation '{corporation.Name}' has no typed geoname corpus.");
return new AetheriaDaemonTutorialFactionInput(
corporation.CorporationKey,
corporation.Name ?? "",
corporation.ShortName ?? "",
corporation.InfluenceDistance)
{
TrainingNames = names.Names,
BossHullItemKey = corporation.BossHullItemKey ?? ""
};
})
.ToArray();
}
public static float MainCloudDensity(float x, float y)
{
var point = new CultMath.float2(x + 91.29439f, y + 91.29439f);
var frequency = 1f;
var amplitude = 0.5f;
var sum = 0f;
for (var octave = 0; octave < 10; octave++)
{
var noise = MathF.Abs(CultMath.math.simplex_noise(point * frequency));
sum += (octave < 4 ? 1f - noise : noise) * amplitude;
frequency *= 2f;
amplitude *= 0.7f;
}
return MathF.Pow(sum + 0.3f, 10f) * 0.01f;
}
private static Dictionary<string, int> PlaceBosses(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
int bossCount,
int entrance,
int exit,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links,
int[,] distances)
{
var path = ShortestPath(entrance, exit, links);
var chokepoints = path
.Where(zone => AetheriaDaemonTutorialTopologyGenerator.Neighbors(zone, links).Count() > 2)
.Where(zone => !ConnectedWithoutZone(entrance, exit, zone, links, distances.GetLength(0)))
.ToArray();
var bossZones = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var faction in factions
.Where(value => !string.IsNullOrWhiteSpace(value.BossHullItemKey))
.Take(Math.Max(0, bossCount)))
{
var candidates = chokepoints.Length == 0 ? path : chokepoints;
bossZones[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(candidates, zone =>
{
var score = (double)distances[exit, zone] * distances[entrance, zone];
foreach (var occupied in bossZones.Values)
score *= distances[occupied, zone];
return score;
});
}
return bossZones;
}
private static Dictionary<string, int> PlaceHomes(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
int entrance,
int exit,
IReadOnlyDictionary<string, int> bossZones,
int[,] distances)
{
var homes = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var faction in factions.Where(value => bossZones.ContainsKey(value.CorporationKey)))
{
var candidates = Enumerable.Range(0, distances.GetLength(0))
.Where(zone => distances[zone, bossZones[faction.CorporationKey]] <= faction.InfluenceDistance);
homes[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(candidates, zone =>
{
var score = (double)AetheriaDaemonTutorialTopologyGenerator.RegionSize(
zone, faction.InfluenceDistance, distances);
foreach (var occupied in homes.Values)
score *= Math.Sqrt(distances[occupied, zone]);
return score;
});
}
foreach (var faction in factions.Where(value => !homes.ContainsKey(value.CorporationKey)))
{
homes[faction.CorporationKey] = AetheriaDaemonTutorialTopologyGenerator.MaxZone(
distances.GetLength(0), zone =>
{
var score = Math.Pow(
AetheriaDaemonTutorialTopologyGenerator.RegionSize(
zone, faction.InfluenceDistance, distances), homes.Count);
score *= distances[exit, zone] * distances[entrance, zone];
foreach (var occupied in homes.Values)
score *= Math.Sqrt(distances[occupied, zone]);
foreach (var boss in bossZones.Values)
score *= Math.Sqrt(distances[boss, zone]);
return score;
});
}
return homes;
}
private static int[] ShortestPath(
int source,
int target,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links)
{
var queue = new Queue<int>();
var parents = new Dictionary<int, int>();
queue.Enqueue(source);
parents[source] = -1;
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (current == target) break;
foreach (var neighbor in AetheriaDaemonTutorialTopologyGenerator.Neighbors(current, links).OrderBy(value => value))
{
if (parents.ContainsKey(neighbor)) continue;
parents[neighbor] = current;
queue.Enqueue(neighbor);
}
}
var path = new List<int>();
for (var cursor = target; cursor >= 0; cursor = parents[cursor])
path.Add(cursor);
path.Reverse();
return path.ToArray();
}
private static bool ConnectedWithoutZone(
int source,
int target,
int ignored,
IReadOnlySet<AetheriaDaemonTutorialTopologyGenerator.Edge> links,
int zoneCount)
{
if (source == ignored || target == ignored) return false;
var queue = new Queue<int>();
var visited = new bool[zoneCount];
queue.Enqueue(source);
visited[source] = true;
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in AetheriaDaemonTutorialTopologyGenerator.Neighbors(current, links))
{
if (neighbor == ignored || visited[neighbor]) continue;
if (neighbor == target) return true;
visited[neighbor] = true;
queue.Enqueue(neighbor);
}
}
return false;
}
}

View file

@ -7,6 +7,7 @@ public sealed record AetheriaDaemonTutorialFactionInput(
int InfluenceDistance)
{
public IReadOnlyList<string> TrainingNames { get; init; } = Array.Empty<string>();
public string BossHullItemKey { get; init; } = "";
}
public sealed record AetheriaDaemonTutorialTopologySettings(
@ -43,7 +44,12 @@ public sealed record AetheriaDaemonTutorialTopology(
int EntranceZoneIndex,
IReadOnlyList<int> DiscoveredZoneIndices,
IReadOnlyDictionary<string, int> HomeZoneByFactionKey,
IReadOnlyList<AetheriaDaemonTutorialZoneTopology> Zones);
IReadOnlyList<AetheriaDaemonTutorialZoneTopology> Zones)
{
public int ExitZoneIndex { get; init; } = -1;
public IReadOnlyDictionary<string, int> BossZoneByFactionKey { get; init; } =
new Dictionary<string, int>(StringComparer.Ordinal);
}
/// <summary>
/// Daemon-owned migration of the fossil tutorial galaxy topology. This organ
@ -52,7 +58,7 @@ public sealed record AetheriaDaemonTutorialTopology(
/// </summary>
public static class AetheriaDaemonTutorialTopologyGenerator
{
private const int CandidateMultiplier = 8;
internal const int CandidateMultiplier = 8;
public static AetheriaDaemonTutorialTopology GenerateFossil(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
@ -252,10 +258,11 @@ public static class AetheriaDaemonTutorialTopologyGenerator
return new TutorialRoles(protagonist, antagonist, buffer, neutrals, quest, all);
}
private static string[] GenerateNames(
internal static string[] GenerateNames(
IReadOnlyList<AetheriaDaemonTutorialFactionInput> factions,
IReadOnlyList<string> ownerKeys,
uint seed)
uint seed,
int order = 3)
{
var generators = new Dictionary<string, TutorialMarkovNameGenerator>(StringComparer.Ordinal);
foreach (var faction in factions)
@ -265,7 +272,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
generators[faction.CorporationKey] = new TutorialMarkovNameGenerator(
seed,
faction.TrainingNames,
order: 3,
order: order,
minimumLength: 5,
maximumLength: 10);
}
@ -290,7 +297,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
return output;
}
private static Point[] GeneratePoints(
internal static Point[] GeneratePoints(
int count,
ref CultMath.Random random,
Func<float, float, float> density)
@ -359,7 +366,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
return MathF.Pow(1f - distance / dMax, 5f + 6f * density);
}
private static HashSet<Edge> DelaunayLinks(IReadOnlyList<Point> points)
internal static HashSet<Edge> DelaunayLinks(IReadOnlyList<Point> points)
{
var vertices = points.Concat(new[]
{
@ -423,7 +430,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
yield return new Edge(triangle.C, triangle.A);
}
private static void PruneLinks(
internal static void PruneLinks(
IReadOnlyList<Point> points,
HashSet<Edge> links,
float linkDensity,
@ -541,7 +548,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
.ToArray();
}
private static int[,] BuildDistances(int zoneCount, IReadOnlySet<Edge> links)
internal static int[,] BuildDistances(int zoneCount, IReadOnlySet<Edge> links)
{
var output = new int[zoneCount, zoneCount];
for (var source = 0; source < zoneCount; source++)
@ -568,7 +575,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator
return output;
}
private static bool IsConnectedWithout(
internal static bool IsConnectedWithout(
int zoneCount,
IReadOnlySet<Edge> links,
int source,
@ -598,20 +605,20 @@ public static class AetheriaDaemonTutorialTopologyGenerator
return false;
}
private static IEnumerable<int> Neighbors(int zone, IEnumerable<Edge> links)
internal static IEnumerable<int> Neighbors(int zone, IEnumerable<Edge> links)
{
return links.Where(edge => edge.Contains(zone)).Select(edge => edge.Other(zone));
}
private static int RegionSize(int zone, int maxDistance, int[,] distances)
internal static int RegionSize(int zone, int maxDistance, int[,] distances)
{
return Enumerable.Range(0, distances.GetLength(0)).Count(other => distances[zone, other] <= maxDistance);
}
private static int MaxZone(int zoneCount, Func<int, double> score) =>
internal static int MaxZone(int zoneCount, Func<int, double> score) =>
MaxZone(Enumerable.Range(0, zoneCount), score);
private static int MaxZone(IEnumerable<int> zones, Func<int, double> score) =>
internal static int MaxZone(IEnumerable<int> zones, Func<int, double> score) =>
zones.Select(zone => new { Zone = zone, Score = score(zone) })
.OrderByDescending(value => value.Score)
.ThenBy(value => value.Zone)
@ -629,9 +636,9 @@ public static class AetheriaDaemonTutorialTopologyGenerator
private static float Saturate(float value) => value < 0 ? 0 : value > 1 ? 1 : value;
private readonly record struct Point(float X, float Y);
internal readonly record struct Point(float X, float Y);
private readonly record struct Triangle(int A, int B, int C);
private readonly record struct Edge
internal readonly record struct Edge
{
public Edge(int a, int b)
{

View file

@ -1179,6 +1179,32 @@ the Terminus proof arena cannot award completion.
provenance, all 64 persisted zones, canonical starter identity, discovery,
and adjacency.
### Daemon Regular-Sector Topology Ownership
- Owner: `AetheriaDaemonRegularTopologyGenerator` owns the non-tutorial sector
graph. It is distinct from tutorial role placement and from the Terminus
witness fixture.
- Inputs: the typed corporation/name catalog, the authored regular-sector
settings (`196` zones, `0.5` link density, `12` corporations, `3` bosses),
the serialized main cloud field, and one generation seed.
- Outputs: deterministic weighted-sample positions, pruned Delaunay links,
maximally isolated exit and furthest entrance, initial discovery, full-radius
corporation influence and ownership, boss chokepoints, headquarters, and
order-four Markov zone names.
- Derived state: sector-map rendering and later zone population consume the
topology. They cannot select corporations, move homes or bosses, alter links,
or reinterpret entrance and exit.
- Forbidden writers: Unity `Galaxy`, menus, map renderers, and Terminus scenario
generation cannot decide regular-sector topology.
- Cut line: this topology owner is implemented and deterministic, but the
regular population/run writer is not wired yet. Post-tutorial New Game still
selects Terminus until that writer can persist a playable sector; topology
existence alone is not claimed as a playable run.
- Verification layer: focused smoke generates the full sector twice and proves
exact seeded repeatability, connected symmetric links, twelve homes, three
boss zones, exit isolation, entrance distance, discovery, names, and home
presence.
### Daemon Generated Zone Body Ownership
- Owner: the daemon zone-body generator owns each generated zone's radius,

View file

@ -15,7 +15,7 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
| Organ | Original authority/evidence | Target owner | Eve contract | Runtime lowering | Current status | Required proof |
|---|---|---|---|---|---|---|
| Run/session state | `ServerShared`, `ActionGameManager` | Aetheria daemon/CultCache | provider/session advertisement plus run lifecycle bindings | all clients | active/failed lifecycle is daemon-owned and restart-stable: controlled-entity destruction commits the fossil cause, terminal frame, one `run.failed` fact, releases control, rejects later gameplay commands, and freezes the world; Eve receives terminal presentation/input levers without becoming an owner. Terminus keeps persistent requested simulation rate distinct from effective rate: compressed batches inspect authoritative facts after each fixed step, stop at the causal damage/destruction/cargo/thermal/docking/weapon/wormhole/run event, pause effectively without erasing the request, and project the exact cause and executed-step count through Eve. Docked, travelling, fighting, mining, looting, and refitting remain derived world conditions rather than counterfeit durable phases. The distinct tutorial is now daemon-owned and selectable: authored 64-zone topology, bodies, post-body station/turret/agent population, resolved orbital placement, deterministic half-zone NPC distribution, starter entry ship, loadout receipts, discovery, and canonical persistence feed an Aetheria-mode session. Generated orbit facts carry the fossil distance-derived period; daemon fixed steps advance phase and project the shared graph into orbital pose/velocity before Ymir while Eve and body queries observe the same graph. New Game submits its Eve operation and waits for the new tutorial sector-map publication. Terminus preserves `TutorialPassed` and cannot impersonate completion | source-backed tutorial completion rule (none survived in the fossil); replace the post-tutorial Terminus fallback with the authored regular-sector generator; full-daemon death/restart witness, broader attention taxonomy, and multi-client convergence |
| Run/session state | `ServerShared`, `ActionGameManager` | Aetheria daemon/CultCache | provider/session advertisement plus run lifecycle bindings | all clients | active/failed lifecycle is daemon-owned and restart-stable: controlled-entity destruction commits the fossil cause, terminal frame, one `run.failed` fact, releases control, rejects later gameplay commands, and freezes the world; Eve receives terminal presentation/input levers without becoming an owner. Terminus keeps persistent requested simulation rate distinct from effective rate: compressed batches inspect authoritative facts after each fixed step, stop at the causal damage/destruction/cargo/thermal/docking/weapon/wormhole/run event, pause effectively without erasing the request, and project the exact cause and executed-step count through Eve. Docked, travelling, fighting, mining, looting, and refitting remain derived world conditions rather than counterfeit durable phases. The distinct tutorial is now daemon-owned and selectable: authored 64-zone topology, bodies, post-body station/turret/agent population, resolved orbital placement, deterministic half-zone NPC distribution, starter entry ship, loadout receipts, discovery, and canonical persistence feed an Aetheria-mode session. Generated orbit facts carry the fossil distance-derived period; daemon fixed steps advance phase and project the shared graph into orbital pose/velocity before Ymir while Eve and body queries observe the same graph. New Game submits its Eve operation and waits for the new tutorial sector-map publication. The daemon now also owns the distinct fossil regular-sector topology: 196 weighted zones, pruned Delaunay graph, twelve selected corporations, three boss chokepoints, full influence radii, isolated exit, furthest entrance, discovery, and order-four names are seed deterministic. Its population/run writer is not wired yet, so Terminus remains the explicit post-tutorial fallback rather than being misreported as a regular run | source-backed tutorial completion rule (none survived in the fossil); materialize and persist the regular sector, then delete the post-tutorial Terminus fallback; full-daemon death/restart witness, broader attention taxonomy, and multi-client convergence |
| Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | daemon-owned hull identity, exact installed placement/rotation and instance state, cargo stacks, weapon membership/held state, equipment wear, executable behavior state, weapon chronology, active-effect state, and loadout-generation provenance survive a hard CultCache close/reopen through the durable latest-frame checkpoint. An advertised generic Eve equip operation now translates into the typed daemon transaction before that checkpoint; its accepted placement survives restart and its consumed station-cargo source does not resurrect. Generic Eve rebuilds weapon and scalar thermostat controls from that restored truth; legacy per-entity documents are bootstrap inputs only when no live frame exists | live client refit witness and multi-client convergence |
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role. Prelude generation retains the fossil exception that allied catalog manufacturers need not own one of the six tutorial home zones; unknown manufacturers remain zero-weight. The April 2021 catalog fossil restores the distinct common `Longinus`, rare AetherDrive-compatible `LonginusX`, and `Djinni` rows with their original IDs, shapes, hardpoints, prices, drag, and prefabs, plus missing Thruster rows required to fill their authored mounts; typed `rarity:rare` tags apply a 0.05 generation multiplier to `LonginusX` and AetherDrive equipment; the generator owns one mutable CultMath stream, fixing the readonly-struct defensive-copy bug that restarted every weighted draw; placement rotation survives durable state and reconnect; generation fills the largest compatible footprint before economic weighting; cargo/capacitor plus station docking equipment must fit remaining hull space; stations pack faction-available inventory largest-first; ordinary ships begin without invented scenario cargo; every generated entity persists a source/selection receipt that reaches Eve as `loadout.item` semantics | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil |
| Flight intent | `Ship`, `Thruster`, `AetherDrive`, `VelocityConversion`, `VelocityLimit`, input paths | daemon actuator simulation + Ymir integration | analog local helm/look/tractor plus actuator state | generic Eve input and scene lowerers | direct faction-speed velocity ownership is deleted; daemon persists local strafe/forward helm and desired look, allocates ordinary directional thruster banks by authored rotation, derives torque from hull/item placement, compensates lateral torque, gates execution through the shared energy ledger, deposits equipment-cell heat, publishes plume visibility and actuator state, applies hull drag, then runs operational velocity conversion with the fossil exponential damp equation and clamps the result through evaluated velocity limits before giving velocity/direction to Ymir. The evaluated limit remains in durable behavior state for reconnect and Eve inspection. Autonomous tasks translate world travel into the same local helm axes and issue look commands instead of mutating heading. AetherDrive rotor/coupling simulation is additive and explicitly non-default: common `Longinus` and `Djinni` hulls use ordinary thrusters; the rare `LonginusX` replaces those mounts with the single rare `Traction` AetherDrive path | hardpoint-transform direction parity, exact cross-equipment behavior execution ordering, CultMath/float32 parity harness, live production-frequency evidence, generic thrust/turn feedback lowering, and live handling screenshots |

View file

@ -170,6 +170,12 @@ fixture and the fallback for the still-unmigrated post-tutorial sector path.
No surviving source defines tutorial completion, so entering this run does not
change `TutorialPassed`.
The non-tutorial sector topology now has a separate daemon owner matching the
fossil's 196-zone settings, corporation selection, entrance/exit isolation,
boss chokepoints, full influence radii, discovery, and order-four names. It is
not yet a selectable run: population and persistence must be wired before the
post-tutorial Terminus fixture can be deleted as a fallback.
## Current Mechanism
The repo is mid-migration.