diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 974b33d08..e1c95d66f 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -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(); + 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, diff --git a/Aetheria.State.Daemon/AetheriaDaemonRegularTopologyGenerator.cs b/Aetheria.State.Daemon/AetheriaDaemonRegularTopologyGenerator.cs new file mode 100644 index 000000000..ce21c3aab --- /dev/null +++ b/Aetheria.State.Daemon/AetheriaDaemonRegularTopologyGenerator.cs @@ -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); +} + +/// +/// 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. +/// +public static class AetheriaDaemonRegularTopologyGenerator +{ + public static AetheriaDaemonTutorialTopology GenerateFossil( + IReadOnlyList 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 ResolveFossilFactions( + GameCult.Aetheria.State.Verse.AetheriaRuntimeCatalogSnapshot catalog) + { + if (catalog == null) throw new ArgumentNullException(nameof(catalog)); + return (catalog.Corporations ?? Array.Empty()) + .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 PlaceBosses( + IReadOnlyList factions, + int bossCount, + int entrance, + int exit, + IReadOnlySet 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(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 PlaceHomes( + IReadOnlyList factions, + int entrance, + int exit, + IReadOnlyDictionary bossZones, + int[,] distances) + { + var homes = new Dictionary(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 links) + { + var queue = new Queue(); + var parents = new Dictionary(); + 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(); + 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 links, + int zoneCount) + { + if (source == ignored || target == ignored) return false; + var queue = new Queue(); + 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; + } +} diff --git a/Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs b/Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs index 22d369aa4..8e35243ec 100644 --- a/Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs +++ b/Aetheria.State.Daemon/AetheriaDaemonTutorialTopologyGenerator.cs @@ -7,6 +7,7 @@ public sealed record AetheriaDaemonTutorialFactionInput( int InfluenceDistance) { public IReadOnlyList TrainingNames { get; init; } = Array.Empty(); + public string BossHullItemKey { get; init; } = ""; } public sealed record AetheriaDaemonTutorialTopologySettings( @@ -43,7 +44,12 @@ public sealed record AetheriaDaemonTutorialTopology( int EntranceZoneIndex, IReadOnlyList DiscoveredZoneIndices, IReadOnlyDictionary HomeZoneByFactionKey, - IReadOnlyList Zones); + IReadOnlyList Zones) +{ + public int ExitZoneIndex { get; init; } = -1; + public IReadOnlyDictionary BossZoneByFactionKey { get; init; } = + new Dictionary(StringComparer.Ordinal); +} /// /// Daemon-owned migration of the fossil tutorial galaxy topology. This organ @@ -52,7 +58,7 @@ public sealed record AetheriaDaemonTutorialTopology( /// public static class AetheriaDaemonTutorialTopologyGenerator { - private const int CandidateMultiplier = 8; + internal const int CandidateMultiplier = 8; public static AetheriaDaemonTutorialTopology GenerateFossil( IReadOnlyList 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 factions, IReadOnlyList ownerKeys, - uint seed) + uint seed, + int order = 3) { var generators = new Dictionary(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 density) @@ -359,7 +366,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator return MathF.Pow(1f - distance / dMax, 5f + 6f * density); } - private static HashSet DelaunayLinks(IReadOnlyList points) + internal static HashSet DelaunayLinks(IReadOnlyList 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 points, HashSet links, float linkDensity, @@ -541,7 +548,7 @@ public static class AetheriaDaemonTutorialTopologyGenerator .ToArray(); } - private static int[,] BuildDistances(int zoneCount, IReadOnlySet links) + internal static int[,] BuildDistances(int zoneCount, IReadOnlySet 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 links, int source, @@ -598,20 +605,20 @@ public static class AetheriaDaemonTutorialTopologyGenerator return false; } - private static IEnumerable Neighbors(int zone, IEnumerable links) + internal static IEnumerable Neighbors(int zone, IEnumerable 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 score) => + internal static int MaxZone(int zoneCount, Func score) => MaxZone(Enumerable.Range(0, zoneCount), score); - private static int MaxZone(IEnumerable zones, Func score) => + internal static int MaxZone(IEnumerable zones, Func 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) { diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 57c8679ca..e7961c991 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -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, diff --git a/docs/original-game-migration-matrix.md b/docs/original-game-migration-matrix.md index 5700ffc08..0f8a12235 100644 --- a/docs/original-game-migration-matrix.md +++ b/docs/original-game-migration-matrix.md @@ -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 | diff --git a/docs/renderless-aetheria-architecture.md b/docs/renderless-aetheria-architecture.md index 39ed34937..71d009ddb 100644 --- a/docs/renderless-aetheria-architecture.md +++ b/docs/renderless-aetheria-architecture.md @@ -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.