Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
using GameCult.Caching;
|
2021-03-12 05:12:01 +00:00
|
|
|
using System;
|
2021-03-10 09:18:07 +00:00
|
|
|
using System.Collections.Generic;
|
2021-04-30 14:24:01 +01:00
|
|
|
using System.IO;
|
2021-03-11 01:36:08 +00:00
|
|
|
using System.Linq;
|
2021-03-15 11:40:46 +00:00
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2021-03-12 05:12:01 +00:00
|
|
|
using DataStructures.ViliWonka.Heap;
|
2021-05-14 03:03:28 +01:00
|
|
|
using Ink.Runtime;
|
2021-03-11 01:36:08 +00:00
|
|
|
using JM.LinqFaster;
|
2021-04-09 14:01:34 +01:00
|
|
|
using UniRx;
|
2026-09-14 17:39:12 +02:00
|
|
|
using CultMath;
|
|
|
|
|
using static CultMath.math;
|
|
|
|
|
using Random = CultMath.Random;
|
2021-03-10 09:18:07 +00:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public class Galaxy
|
2021-03-10 09:18:07 +00:00
|
|
|
{
|
2021-05-14 03:03:28 +01:00
|
|
|
public Dictionary<Faction, GalaxyZone> HomeZones = new Dictionary<Faction, GalaxyZone>();
|
|
|
|
|
public Dictionary<Faction, GalaxyZone> BossZones = new Dictionary<Faction, GalaxyZone>();
|
|
|
|
|
public HashSet<GalaxyZone> DiscoveredZones = new HashSet<GalaxyZone>();
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
public SectorBackgroundSettings Background { get; }
|
|
|
|
|
public NameGeneratorSettings NameGeneratorSettings { get; }
|
2021-03-20 04:20:17 +00:00
|
|
|
public Faction[] Factions { get; }
|
2021-05-14 03:03:28 +01:00
|
|
|
public GalaxyZone[] Zones { get; }
|
|
|
|
|
public GalaxyZone Entrance { get; }
|
2026-10-06 19:32:02 +02:00
|
|
|
public GalaxyZone Exit { get; private set; }
|
2021-05-02 20:59:45 +01:00
|
|
|
public Dictionary<Faction, FactionRelationship> FactionRelationships { get; } = new Dictionary<Faction, FactionRelationship>();
|
2021-05-16 05:24:51 +01:00
|
|
|
private Action<string> Log { get; }
|
2022-08-08 22:43:55 +01:00
|
|
|
public bool IsPrelude { get; }
|
A fixed seed fixes every fit: RunStart seeds the run's item draws from the galaxy's seed
ScenarioStage.Generated picked from ItemManager.Random, which is clock-seeded per process and advances in play, so a
relaunch of Djinni Shakedown or Long Haul in one session gave the player a different fit; every branded lot's
workmanship and role fills came from the same clock stream, so arena gear and explicit fits varied in quality too.
Galaxy now keeps the seed it was generated at, and RunStart.GenerateArena and Stage each reseed the item manager's
random from it, one stream per step, so what runs between them cannot shift either.
SeedRules relaunches every fixed-seed scenario around a clock-free Main Galaxy launch and a scrambled item random,
and compares the galaxy (links, factions, homes, exit too), the arena down to each item's maker and workmanship, and
the staged fits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:01:17 +02:00
|
|
|
|
|
|
|
|
// The seed this galaxy was generated at, which also seeds its run's item draws (RunStart); zero for a galaxy
|
|
|
|
|
// restored from a save.
|
|
|
|
|
public uint Seed { get; }
|
2022-08-08 22:43:55 +01:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
private GalaxyZone[] _exitPath;
|
2021-03-20 04:20:17 +00:00
|
|
|
private Dictionary<Faction, MarkovNameGenerator> _nameGenerators = new Dictionary<Faction, MarkovNameGenerator>();
|
2026-08-10 14:57:50 +01:00
|
|
|
private readonly CultCache _cache;
|
2021-03-12 05:12:01 +00:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public GalaxyZone[] ExitPath
|
2021-03-12 05:12:01 +00:00
|
|
|
{
|
|
|
|
|
get
|
|
|
|
|
{
|
2021-04-22 02:53:06 +01:00
|
|
|
if(Entrance!=null && Exit != null)
|
|
|
|
|
return _exitPath ??= FindPath(Entrance, Exit);
|
|
|
|
|
return null;
|
2021-03-12 05:12:01 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
public Galaxy(CultCache cultCache, SavedGame savedGame, Action<string> log)
|
|
|
|
|
{
|
|
|
|
|
IsPrelude = savedGame.IsTutorial;
|
|
|
|
|
_cache = cultCache;
|
|
|
|
|
Log = log;
|
|
|
|
|
Background = savedGame.Background;
|
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
Factions = savedGame.Factions.Select(faction => cultCache.Get(faction)).ToArray();
|
2026-08-10 14:57:50 +01:00
|
|
|
for (var i = 0; i < Factions.Length; i++)
|
|
|
|
|
{
|
|
|
|
|
FactionRelationships[Factions[i]] = savedGame.Relationships[i];
|
|
|
|
|
}
|
|
|
|
|
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
var savedZones = savedGame.Zones.Select(zone => cultCache.Get(zone)).ToArray();
|
|
|
|
|
Zones = savedZones.Select(zone =>
|
2026-08-10 14:57:50 +01:00
|
|
|
{
|
|
|
|
|
return new GalaxyZone
|
|
|
|
|
{
|
|
|
|
|
Name = zone.Name,
|
|
|
|
|
Position = zone.Position,
|
|
|
|
|
PackedContents = zone.Contents
|
|
|
|
|
};
|
|
|
|
|
}).ToArray();
|
|
|
|
|
foreach (var i in savedGame.DiscoveredZones) DiscoveredZones.Add(Zones[i]);
|
|
|
|
|
for (var i = 0; i < Zones.Length; i++)
|
|
|
|
|
{
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
Zones[i].AdjacentZones = savedZones[i].AdjacentZones.Select(azi => Zones[azi]).ToList();
|
|
|
|
|
Zones[i].Factions = savedZones[i].Factions.Select(mi => Factions[mi]).ToArray();
|
|
|
|
|
Zones[i].Owner = savedZones[i].Owner < 0 ? null : Factions[savedZones[i].Owner];
|
2026-08-10 14:57:50 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
HomeZones = savedGame.HomeZones.ToDictionary(
|
|
|
|
|
x => Factions[x.Key],
|
|
|
|
|
x => Zones[x.Value]);
|
|
|
|
|
|
|
|
|
|
BossZones = savedGame.BossZones.ToDictionary(
|
|
|
|
|
x => Factions[x.Key],
|
|
|
|
|
x => Zones[x.Value]);
|
|
|
|
|
|
|
|
|
|
Entrance = Zones[savedGame.Entrance];
|
|
|
|
|
if(savedGame.Exit != -1)
|
|
|
|
|
Exit = Zones[savedGame.Exit];
|
|
|
|
|
|
|
|
|
|
CalculateDistanceMatrix();
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public Galaxy(
|
2021-04-22 02:53:06 +01:00
|
|
|
SectorGenerationSettings settings,
|
|
|
|
|
SectorBackgroundSettings background,
|
|
|
|
|
NameGeneratorSettings nameGeneratorSettings,
|
2026-08-10 14:57:50 +01:00
|
|
|
CultCache cache,
|
2021-05-16 05:24:51 +01:00
|
|
|
Action<string> log,
|
2021-04-22 02:53:06 +01:00
|
|
|
Action<string> progressCallback = null,
|
|
|
|
|
uint seed = 0)
|
2021-03-14 03:56:49 +00:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_cache = cache;
|
2022-08-08 22:43:55 +01:00
|
|
|
IsPrelude = false;
|
2021-04-22 02:53:06 +01:00
|
|
|
Background = background;
|
2021-05-16 05:24:51 +01:00
|
|
|
Log = log;
|
2026-08-10 14:57:50 +01:00
|
|
|
var factions = cache.GetAll<Faction>();
|
A fixed seed fixes every fit: RunStart seeds the run's item draws from the galaxy's seed
ScenarioStage.Generated picked from ItemManager.Random, which is clock-seeded per process and advances in play, so a
relaunch of Djinni Shakedown or Long Haul in one session gave the player a different fit; every branded lot's
workmanship and role fills came from the same clock stream, so arena gear and explicit fits varied in quality too.
Galaxy now keeps the seed it was generated at, and RunStart.GenerateArena and Stage each reseed the item manager's
random from it, one stream per step, so what runs between them cannot shift either.
SeedRules relaunches every fixed-seed scenario around a clock-free Main Galaxy launch and a scrambled item random,
and compares the galaxy (links, factions, homes, exit too), the arena down to each item's maker and workmanship, and
the staged fits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:01:17 +02:00
|
|
|
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
|
|
|
|
var random = new Random(Seed);
|
2026-08-10 14:57:50 +01:00
|
|
|
Factions = factions.OrderBy(x => random.NextFloat()).Take(settings.MegaCount).ToArray();
|
2021-05-02 20:59:45 +01:00
|
|
|
foreach (var f in Factions) FactionRelationships[f] = FactionRelationship.Neutral;
|
2021-03-15 11:40:46 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
Zones = GenerateZones(settings.ZoneCount, ref random, progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
GenerateLinks(settings.LinkDensity, progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
CalculateDistanceMatrix(progressCallback);
|
|
|
|
|
|
|
|
|
|
// Exit is the most isolated zone (highest total distance to all other zones)
|
|
|
|
|
Exit = Zones.MaxBy(z => z.Isolation);
|
2021-03-15 11:40:46 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
// Entrance is the zone furthest from the exit
|
|
|
|
|
Entrance = Zones.MaxBy(z => Exit.Distance[z]);
|
|
|
|
|
|
|
|
|
|
DiscoveredZones.Add(Entrance);
|
|
|
|
|
foreach(var z in Entrance.AdjacentZones) DiscoveredZones.Add(z);
|
|
|
|
|
|
|
|
|
|
PlaceFactionsMain(settings.BossCount, progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
CalculateFactionInfluence(progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
GenerateNames(cache, nameGeneratorSettings, ref random, progressCallback);
|
2021-03-18 22:48:33 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
progressCallback?.Invoke("Done!");
|
|
|
|
|
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
}
|
2022-08-08 22:43:55 +01:00
|
|
|
|
|
|
|
|
public Faction ResolveFaction(string name)
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
return _cache.GetAll<Faction>().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase));
|
2022-08-08 22:43:55 +01:00
|
|
|
}
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
// A cast member of a prelude galaxy, by name. A name the catalog does not hold refuses the galaxy, naming the field.
|
|
|
|
|
private Faction ResolveCast(string field, string name) =>
|
|
|
|
|
ResolveFaction(name) ?? throw new InvalidOperationException($"cast faction {name} ({field}) is not in the catalog");
|
|
|
|
|
|
|
|
|
|
// Gives a prelude galaxy its gate: the antagonist's home is the boss zone and the exit. The only writer of Exit
|
|
|
|
|
// besides the constructors.
|
|
|
|
|
public void PlaceGate(Faction antagonist)
|
|
|
|
|
{
|
|
|
|
|
BossZones[antagonist] = HomeZones[antagonist];
|
|
|
|
|
Exit = HomeZones[antagonist];
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 20:25:26 +02:00
|
|
|
// How many jumps a faction's influence reaches in this galaxy: its authored distance, halved (rounding up) in a
|
|
|
|
|
// prelude. Derived, never written back to the catalog's Faction, so one galaxy's generation cannot change the next.
|
|
|
|
|
public int InfluenceOf(Faction faction) =>
|
|
|
|
|
IsPrelude ? (faction.InfluenceDistance + 1) / 2 : faction.InfluenceDistance;
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public Galaxy(
|
2021-04-22 02:53:06 +01:00
|
|
|
TutorialGenerationSettings settings,
|
|
|
|
|
SectorBackgroundSettings background,
|
|
|
|
|
NameGeneratorSettings nameGeneratorSettings,
|
2026-08-10 14:57:50 +01:00
|
|
|
CultCache cache,
|
|
|
|
|
PlayerSettings playerSettings,
|
2021-04-30 14:24:01 +01:00
|
|
|
DirectoryInfo narrativeDirectory,
|
2021-05-16 05:24:51 +01:00
|
|
|
Action<string> log,
|
2021-04-22 02:53:06 +01:00
|
|
|
Action<string> progressCallback = null,
|
|
|
|
|
uint seed = 0)
|
|
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
_cache = cache;
|
2022-08-08 22:43:55 +01:00
|
|
|
IsPrelude = true;
|
2021-04-27 16:44:44 +01:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
Background = background;
|
2021-05-16 05:24:51 +01:00
|
|
|
Log = log;
|
A fixed seed fixes every fit: RunStart seeds the run's item draws from the galaxy's seed
ScenarioStage.Generated picked from ItemManager.Random, which is clock-seeded per process and advances in play, so a
relaunch of Djinni Shakedown or Long Haul in one session gave the player a different fit; every branded lot's
workmanship and role fills came from the same clock stream, so arena gear and explicit fits varied in quality too.
Galaxy now keeps the seed it was generated at, and RunStart.GenerateArena and Stage each reseed the item manager's
random from it, one stream per step, so what runs between them cannot shift either.
SeedRules relaunches every fixed-seed scenario around a clock-free Main Galaxy launch and a scrambled item random,
and compares the galaxy (links, factions, homes, exit too), the arena down to each item's maker and workmanship, and
the staged fits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:01:17 +02:00
|
|
|
Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed;
|
|
|
|
|
var random = new Random(Seed);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
var factions = new List<Faction>();
|
2021-04-27 16:44:44 +01:00
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
var protagonistFaction = ResolveCast(nameof(settings.ProtagonistFaction), settings.ProtagonistFaction);
|
2021-04-22 02:53:06 +01:00
|
|
|
factions.Add(protagonistFaction);
|
|
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
var antagonistFaction = ResolveCast(nameof(settings.AntagonistFaction), settings.AntagonistFaction);
|
2021-04-22 02:53:06 +01:00
|
|
|
factions.Add(antagonistFaction);
|
|
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
var bufferFaction = ResolveCast(nameof(settings.BufferFaction), settings.BufferFaction);
|
2021-04-22 02:53:06 +01:00
|
|
|
factions.Add(bufferFaction);
|
|
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
// A cast without a quest faction places no quest zone.
|
|
|
|
|
var questFaction = string.IsNullOrEmpty(settings.QuestFaction)
|
|
|
|
|
? null
|
|
|
|
|
: ResolveCast(nameof(settings.QuestFaction), settings.QuestFaction);
|
|
|
|
|
if (questFaction != null) factions.Add(questFaction);
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
var neutralFactions = settings.NeutralFactions
|
2026-10-06 19:32:02 +02:00
|
|
|
.Select(name => ResolveCast(nameof(settings.NeutralFactions), name))
|
2021-04-22 02:53:06 +01:00
|
|
|
.ToArray();
|
|
|
|
|
factions.AddRange(neutralFactions);
|
|
|
|
|
|
|
|
|
|
Factions = factions.ToArray();
|
2026-10-03 20:25:26 +02:00
|
|
|
foreach (var faction in Factions) FactionRelationships[faction] = FactionRelationship.Neutral;
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
Zones = GenerateZones(settings.ZoneCount, ref random, progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
GenerateLinks(settings.LinkDensity, progressCallback);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
CalculateDistanceMatrix(progressCallback);
|
|
|
|
|
|
|
|
|
|
HomeZones[protagonistFaction] = Zones
|
2026-10-03 20:25:26 +02:00
|
|
|
.MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count);
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
HomeZones[antagonistFaction] = Zones
|
2026-10-03 20:25:26 +02:00
|
|
|
.MaxBy(z => ConnectedRegion(z, InfluenceOf(antagonistFaction)).Count * z.Distance[HomeZones[protagonistFaction]]);
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
HomeZones[protagonistFaction] = Zones
|
2026-10-03 20:25:26 +02:00
|
|
|
.MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count * sqrt(z.Distance[HomeZones[antagonistFaction]]));
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
// var antagonistRegion = ConnectedRegion(HomeZones[antagonistFaction], antagonistFaction.InfluenceDistance);
|
|
|
|
|
// var protagonistRegion = ConnectedRegion(HomeZones[protagonistFaction], protagonistFaction.InfluenceDistance);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
// Place the buffer faction in a zone where it has equal distance to the pro/antagonist HQs and where it can control the most territory
|
2026-08-10 14:57:50 +01:00
|
|
|
var bufferDistance = Zones.Min(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]]));
|
2021-04-22 02:53:06 +01:00
|
|
|
var potentialBufferZones = Zones
|
2026-08-10 14:57:50 +01:00
|
|
|
.Where(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]]) == bufferDistance);
|
2026-10-03 20:25:26 +02:00
|
|
|
HomeZones[bufferFaction] = potentialBufferZones.MaxBy(z => ConnectedRegion(z, InfluenceOf(bufferFaction)).Count);
|
2021-03-18 22:48:33 +00:00
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
// Place neutral headquarters away from existing factions while also maximizing territory
|
|
|
|
|
foreach (var faction in neutralFactions)
|
|
|
|
|
{
|
|
|
|
|
HomeZones[faction] = Zones.MaxBy(z =>
|
2026-10-03 20:25:26 +02:00
|
|
|
ConnectedRegion(z, InfluenceOf(faction)).Count *
|
2026-08-10 14:57:50 +01:00
|
|
|
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
2021-04-22 02:53:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CalculateFactionInfluence(progressCallback);
|
|
|
|
|
|
2026-10-06 19:32:02 +02:00
|
|
|
if (questFaction != null)
|
|
|
|
|
{
|
|
|
|
|
var potentialQuestZones = Zones
|
|
|
|
|
.Where(z => z.Factions.Contains(antagonistFaction) && z.Factions.Contains(bufferFaction));
|
|
|
|
|
if (potentialQuestZones.Any())
|
|
|
|
|
HomeZones[questFaction] = potentialQuestZones
|
|
|
|
|
.MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, InfluenceOf(questFaction)).Count);
|
|
|
|
|
else
|
|
|
|
|
HomeZones[questFaction] = Zones
|
|
|
|
|
.Where(z => z.Factions.Contains(antagonistFaction))
|
|
|
|
|
.MinBy(z=>z.Distance[HomeZones[bufferFaction]]);
|
|
|
|
|
|
|
|
|
|
CalculateFactionInfluence(progressCallback);
|
|
|
|
|
}
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2021-04-30 11:50:47 +01:00
|
|
|
Entrance = Zones.Where(z => z.Owner == null).MinBy(z => z.Distance[HomeZones[protagonistFaction]]);
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2021-04-09 14:01:34 +01:00
|
|
|
DiscoveredZones.Add(Entrance);
|
|
|
|
|
foreach(var z in Entrance.AdjacentZones) DiscoveredZones.Add(z);
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
CalculateFactionInfluence(progressCallback);
|
|
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
GenerateNames(cache, nameGeneratorSettings, ref random, progressCallback);
|
2021-04-30 14:24:01 +01:00
|
|
|
|
2021-10-27 02:58:22 +01:00
|
|
|
// progressCallback?.Invoke("Weaving Narrative");
|
|
|
|
|
// var processor = new StoryProcessor(playerSettings, narrativeDirectory, this, ref random, Log);
|
|
|
|
|
// processor.ProcessStories();
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
progressCallback?.Invoke("Done!");
|
|
|
|
|
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
private GalaxyZone[] GenerateZones(int zoneCount, ref Random random, Action<string> progressCallback = null)
|
2021-04-22 02:53:06 +01:00
|
|
|
{
|
|
|
|
|
var outputSamples = WeightedSampleElimination.GeneratePoints(zoneCount,
|
|
|
|
|
ref random,
|
|
|
|
|
Background.CloudDensity,
|
2026-08-10 14:57:50 +01:00
|
|
|
v => (.2f - lengthsq(v - float2(.5f))) * 4,
|
2021-04-22 02:53:06 +01:00
|
|
|
progressCallback);
|
2026-08-10 14:57:50 +01:00
|
|
|
return outputSamples.Select(v => new GalaxyZone {Position = v}).ToArray();
|
2026-06-13 13:00:15 +01:00
|
|
|
}
|
|
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
private void PlaceFactionsMain(int bossCount, Action<string> progressCallback = null)
|
|
|
|
|
{
|
|
|
|
|
progressCallback?.Invoke("Finding Chokepoints");
|
|
|
|
|
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
2021-04-09 14:01:34 +01:00
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Find all zones on the exit path where removing that zone would disconnect the entrance from the exit
|
|
|
|
|
// Disregard "corridor" zones with only two adjacent zones
|
|
|
|
|
var chokePoints = ExitPath
|
|
|
|
|
.Where(z => z.AdjacentZones.Count > 2 && !ConnectedRegion(Entrance, z).Contains(Exit));
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2021-03-15 11:40:46 +00:00
|
|
|
progressCallback?.Invoke("Placing Factions");
|
2021-04-22 02:53:06 +01:00
|
|
|
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Choose some megas to have bosses placed based on whether a boss hull is assigned
|
2021-03-15 11:40:46 +00:00
|
|
|
var bossMegas = Factions
|
Move the ServerShared data model onto CultLib's CultCache
Cut 8b. Aetheria.Shared builds headless against GameCult.Caching and
GameCult.Caching.MessagePack at the pinned CultLib; the pin guard also
checks the Caching project exists.
Documents: [CultDocument("aetheria.<type>", "1")] with [MessagePackObject]
and integer [Key]s on the Q7 concrete types (the eight ItemData leaves,
Faction, FactionProductData, PersonalityAttribute, NameFile, OrbitData, the
four BodyData leaves, SavedZone, SavedGame, PlayerSettings, InputLayout).
SavedGame and PlayerSettings are [CultGlobal]; Name members are [CultName].
ItemData, CraftedItemData, EquippableItemData, BodyData and AgentTask lose
[MessagePackObject]; BodyData and AgentTask lose their [Union]s. InputLayout
moves into ServerShared so AetheriaStores can route it.
References: Guid and DatabaseLink<T> members become CultRecordRef<T>;
Guid-keyed maps become ref-keyed with [CultReference(..., many: true)];
ZonePack holds refs to orbit and body records. Runtime maps (Zone orbits,
bodies and belts, cargo ItemsOfType, agent targets) key by CultRecordKey.
Identity is the record key: ID comparisons become reference or key
comparisons. StatModifierData.RequireBehavior is a simple type name.
AetheriaStores.Open composes the catalog (read-only unless writable), run
and player stores, one home per type. CultRecordRefs carries IsSet, a
null-safe Get(ref), RefOf (key from TryGetHandle) and Upsert.
ItemManager.GetData is the one item resolution path. MathResolver is
declared by [assembly: CultCacheFormatterResolver].
Inspector attributes: text, range and asset path map onto CultLib's
CultInspector attributes (asset path only on string members); every
replaced Aetheria attribute keeps [Inspectable] for the runtime panel.
Builds: Aetheria.Shared. Does not build: AetherDb and the Unity project,
which follow.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:00:42 +02:00
|
|
|
.Where(m => m.BossHull.IsSet())
|
2021-04-22 02:53:06 +01:00
|
|
|
.Take(bossCount)
|
2021-03-14 03:56:49 +00:00
|
|
|
.ToArray();
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Place boss zones along the critical path as far apart from each other as possible
|
|
|
|
|
foreach (var mega in bossMegas)
|
|
|
|
|
{
|
|
|
|
|
BossZones[mega] = chokePoints.MaxBy(z =>
|
2021-04-22 02:53:06 +01:00
|
|
|
Exit.Distance[z] * Entrance.Distance[z] *
|
2021-03-14 03:56:49 +00:00
|
|
|
BossZones.Values.Aggregate(1, (i, os) => i * os.Distance[z]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Place boss mega headquarters such that their sphere of influence encompasses their boss zone
|
|
|
|
|
// While occupying as much territory as possible
|
|
|
|
|
foreach (var mega in bossMegas)
|
|
|
|
|
{
|
2026-10-03 20:25:26 +02:00
|
|
|
HomeZones[mega] = ConnectedRegion(BossZones[mega], InfluenceOf(mega))
|
2021-03-14 03:56:49 +00:00
|
|
|
.MaxBy(z =>
|
2026-10-03 20:25:26 +02:00
|
|
|
ConnectedRegion(z, InfluenceOf(mega)).Count *
|
2026-08-10 14:57:50 +01:00
|
|
|
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
2021-03-14 03:56:49 +00:00
|
|
|
}
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Place remaining headquarters away from existing megas while also maximizing territory
|
2021-04-22 02:53:06 +01:00
|
|
|
foreach (var mega in Factions.Where(m => !bossMegas.Contains(m)))
|
2021-03-14 03:56:49 +00:00
|
|
|
{
|
|
|
|
|
HomeZones[mega] = Zones.MaxBy(z =>
|
2026-10-03 20:25:26 +02:00
|
|
|
pow(ConnectedRegion(z, InfluenceOf(mega)).Count, HomeZones.Count) *
|
2021-04-22 02:53:06 +01:00
|
|
|
Exit.Distance[z] * Entrance.Distance[z] *
|
2026-08-10 14:57:50 +01:00
|
|
|
HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])) *
|
|
|
|
|
BossZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])));
|
2021-03-14 03:56:49 +00:00
|
|
|
}
|
2021-04-22 02:53:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void CalculateFactionInfluence(Action<string> progressCallback = null)
|
|
|
|
|
{
|
2021-03-15 11:40:46 +00:00
|
|
|
progressCallback?.Invoke("Calculating Faction Influence");
|
2021-04-22 02:53:06 +01:00
|
|
|
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
2021-03-14 03:56:49 +00:00
|
|
|
|
|
|
|
|
// Assign faction presence
|
|
|
|
|
foreach (var zone in Zones)
|
|
|
|
|
{
|
|
|
|
|
// Factions are present in all zones within their sphere of influence
|
2021-03-15 11:40:46 +00:00
|
|
|
zone.Factions = Factions
|
2021-04-22 02:53:06 +01:00
|
|
|
.Where(f => HomeZones.ContainsKey(f))
|
2026-10-03 20:25:26 +02:00
|
|
|
.Where(f => zone.Distance[HomeZones[f]] <= InfluenceOf(f))
|
2021-03-14 03:56:49 +00:00
|
|
|
.ToArray();
|
2021-04-22 02:53:06 +01:00
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Owner of a zone is the faction with the nearest headquarters
|
2021-04-22 02:53:06 +01:00
|
|
|
var nearestFaction = Factions
|
|
|
|
|
.Where(f => HomeZones.ContainsKey(f))
|
|
|
|
|
.MinBy(f => (float)zone.Distance[HomeZones[f]]);
|
2026-10-03 20:25:26 +02:00
|
|
|
if (zone.Distance[HomeZones[nearestFaction]] <= InfluenceOf(nearestFaction))
|
2021-03-14 03:56:49 +00:00
|
|
|
zone.Owner = nearestFaction;
|
|
|
|
|
}
|
2021-04-22 02:53:06 +01:00
|
|
|
}
|
2021-03-14 03:56:49 +00:00
|
|
|
|
2026-08-10 14:57:50 +01:00
|
|
|
private void GenerateNames(CultCache cache,
|
|
|
|
|
NameGeneratorSettings nameGeneratorSettings,
|
|
|
|
|
ref Random random,
|
2021-04-22 02:53:06 +01:00
|
|
|
Action<string> progressCallback = null)
|
|
|
|
|
{
|
2026-10-03 20:45:26 +02:00
|
|
|
var used = new HashSet<string>();
|
2021-03-15 11:40:46 +00:00
|
|
|
for (var i = 0; i < Factions.Length; i++)
|
|
|
|
|
{
|
2021-04-22 02:53:06 +01:00
|
|
|
progressCallback?.Invoke($"Feeding Markov Chains: {i + 1} / {Factions.Length}");
|
2021-03-15 11:40:46 +00:00
|
|
|
//if(progressCallback!=null) Thread.Sleep(250); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
var faction = Factions[i];
|
2026-10-03 20:45:26 +02:00
|
|
|
_nameGenerators[faction] = new MarkovNameGenerator(ref random, cache.Get(faction.GeonameFile).Names, nameGeneratorSettings, used);
|
2021-03-15 11:40:46 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-14 03:56:49 +00:00
|
|
|
// Generate zone name using the owner's name generator, otherwise assign catalogue ID
|
2021-03-15 11:40:46 +00:00
|
|
|
foreach (var zone in Zones)
|
|
|
|
|
{
|
|
|
|
|
if (zone.Owner != null)
|
|
|
|
|
{
|
|
|
|
|
zone.Name = _nameGenerators[zone.Owner].NextName.Trim();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2021-03-18 22:48:33 +00:00
|
|
|
zone.Name = $"EAC-{random.NextInt(9999).ToString()}";
|
2021-03-15 11:40:46 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-22 02:53:06 +01:00
|
|
|
private void GenerateLinks(float linkDensity, Action<string> progressCallback = null)
|
2021-03-15 11:40:46 +00:00
|
|
|
{
|
2021-04-22 02:53:06 +01:00
|
|
|
progressCallback?.Invoke("Triangulating Zone Positions");
|
|
|
|
|
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
|
2026-10-03 21:59:09 +02:00
|
|
|
// The Delaunay triangulation connects adjacent sectors, every zone among them
|
|
|
|
|
var links = Delaunay.Edges(Zones.Select(z => z.Position).ToArray())
|
|
|
|
|
.Select(edge => (Zones[edge.a], Zones[edge.b]))
|
|
|
|
|
.ToList();
|
2021-04-22 02:53:06 +01:00
|
|
|
|
|
|
|
|
progressCallback?.Invoke("Eliminating Zone Links");
|
|
|
|
|
if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
|
|
|
|
// foreach (var link in links.ToArray())
|
|
|
|
|
// {
|
|
|
|
|
// foreach (var zone in Zones)
|
|
|
|
|
// {
|
|
|
|
|
// if(zone != link.Item1 && zone != link.Item2)
|
Delete AetheriaMath in favour of CultMath
AetheriaMath predates the CultMath swap and re-implemented helpers CultMath
now owns. Callers use CultMath directly: decay (Ship, AetherDrive, Entity),
damp (VelocityConversion, FieldTester, FieldDriver), first_order_intercept
(TurretController, Combat, GuidedProjectile), quadratic_bezier (FieldDriver)
and smootherstep (Ship). Parameter order matches in every case, and those
formulas have the same float evaluation order, so behaviour is unchanged.
Galaxy's commented-out segment check names distance_to_segment.
The other overloads had no callers and are dropped without a CultMath
counterpart being needed: the double Damp overloads, FindDistanceToSegment,
GetCatmullRomPosition, GetCubicSplinePosition, Smoothstep(x), and the
Deg2Rad/Rad2Deg constants.
AetherDb census, factions and hardpoint-fit are byte-identical to the
pre-cut captures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:02:41 +02:00
|
|
|
// if (distance_to_segment(zone.Position, link.Item1.Position, link.Item2.Position, out _) < minLineSeparation)
|
2021-04-22 02:53:06 +01:00
|
|
|
// links.Remove(link);
|
|
|
|
|
// }
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
foreach (var link in links)
|
|
|
|
|
{
|
|
|
|
|
link.Item1.AdjacentZones.Add(link.Item2);
|
|
|
|
|
link.Item2.AdjacentZones.Add(link.Item1);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
float LinkWeight((GalaxyZone, GalaxyZone) link)
|
2021-04-22 02:53:06 +01:00
|
|
|
{
|
2026-08-10 14:57:50 +01:00
|
|
|
return 1 / saturate(Background.CloudDensity((link.Item1.Position + link.Item2.Position) / 2)) *
|
|
|
|
|
lengthsq(link.Item1.Position - link.Item2.Position) *
|
2021-04-22 02:53:06 +01:00
|
|
|
(link.Item1.AdjacentZones.Count - 1) * (link.Item2.AdjacentZones.Count - 1);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
var heap = new MaxHeap<(GalaxyZone, GalaxyZone)>(links.Count);
|
2021-04-22 02:53:06 +01:00
|
|
|
foreach (var link in links) heap.PushObj(link, LinkWeight(link));
|
|
|
|
|
while (heap.Count > linkDensity * links.Count)
|
|
|
|
|
{
|
|
|
|
|
var link = heap.PopObj();
|
|
|
|
|
if (ConnectedRegion(link.Item1, link.Item1, link.Item2).Contains(link.Item2))
|
|
|
|
|
{
|
|
|
|
|
link.Item1.AdjacentZones.Remove(link.Item2);
|
|
|
|
|
link.Item2.AdjacentZones.Remove(link.Item1);
|
|
|
|
|
foreach (var secondary in link.Item1.AdjacentZones)
|
|
|
|
|
{
|
|
|
|
|
heap.SetValue((link.Item1, secondary), LinkWeight((link.Item1, secondary)));
|
|
|
|
|
heap.SetValue((secondary, link.Item1), LinkWeight((secondary, link.Item1)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var secondary in link.Item2.AdjacentZones)
|
|
|
|
|
{
|
|
|
|
|
heap.SetValue((link.Item2, secondary), LinkWeight((link.Item2, secondary)));
|
|
|
|
|
heap.SetValue((secondary, link.Item2), LinkWeight((secondary, link.Item2)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-14 03:56:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cache distance matrix and calculate isolation for every zone (used extensively for placing stuff)
|
2021-04-22 02:53:06 +01:00
|
|
|
private void CalculateDistanceMatrix(Action<string> progressCallback = null)
|
2021-03-14 03:56:49 +00:00
|
|
|
{
|
2021-04-22 02:53:06 +01:00
|
|
|
progressCallback?.Invoke("Calculating Distance Matrix");
|
|
|
|
|
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
|
2021-03-14 03:56:49 +00:00
|
|
|
foreach (var zone in Zones)
|
|
|
|
|
{
|
|
|
|
|
zone.Distance = ConnectedRegionDistance(zone);
|
|
|
|
|
zone.Isolation = zone.Distance.Sum(x => x.Value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-11 01:36:08 +00:00
|
|
|
class DijkstraVertex
|
|
|
|
|
{
|
|
|
|
|
public float Cost;
|
|
|
|
|
public DijkstraVertex Parent;
|
2021-05-14 03:03:28 +01:00
|
|
|
public GalaxyZone Zone;
|
2021-03-11 01:36:08 +00:00
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public Dictionary<GalaxyZone, int> ConnectedRegionDistance(GalaxyZone v)
|
2021-03-11 01:36:08 +00:00
|
|
|
{
|
2021-05-14 03:03:28 +01:00
|
|
|
var members = new Dictionary<GalaxyZone, int> {{v, 0}};
|
2021-03-12 05:12:01 +00:00
|
|
|
int cost = 1;
|
2021-03-11 01:36:08 +00:00
|
|
|
while (true)
|
|
|
|
|
{
|
2021-03-11 11:18:21 +00:00
|
|
|
var lastCount = members.Count;
|
2021-03-11 01:36:08 +00:00
|
|
|
// For each member, add all vertices that are connected to it but are not already a member
|
|
|
|
|
// Also, if there is a zone being ignored, do not traverse across it
|
2021-03-12 05:12:01 +00:00
|
|
|
foreach (var member in members.Keys.ToArray())
|
|
|
|
|
foreach (var adjacentZone in member.AdjacentZones)
|
|
|
|
|
{
|
|
|
|
|
if(!members.ContainsKey(adjacentZone))
|
|
|
|
|
members.Add(adjacentZone, cost);
|
|
|
|
|
}
|
2021-03-11 01:36:08 +00:00
|
|
|
// If we have stopped finding neighbors, stop traversing
|
|
|
|
|
if (members.Count == lastCount)
|
|
|
|
|
return members;
|
2021-03-12 05:12:01 +00:00
|
|
|
cost++;
|
2021-03-11 01:36:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-03-11 11:18:21 +00:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public HashSet<GalaxyZone> ConnectedRegion(
|
|
|
|
|
GalaxyZone v,
|
|
|
|
|
GalaxyZone ignoreLinkSource,
|
|
|
|
|
GalaxyZone ignoreLinkTarget,
|
2021-03-12 05:12:01 +00:00
|
|
|
int maxDistance = int.MaxValue)
|
|
|
|
|
{
|
2021-03-14 03:56:49 +00:00
|
|
|
return ConnectedRegion(v, x => x.Item1 != ignoreLinkSource || x.Item2 != ignoreLinkTarget, maxDistance);
|
2021-03-12 05:12:01 +00:00
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public HashSet<GalaxyZone> ConnectedRegion(
|
|
|
|
|
GalaxyZone v,
|
|
|
|
|
GalaxyZone ignoreZone,
|
2021-03-12 05:12:01 +00:00
|
|
|
int maxDistance = int.MaxValue)
|
|
|
|
|
{
|
|
|
|
|
return ConnectedRegion(v, x => x.Item2 != ignoreZone, maxDistance);
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public HashSet<GalaxyZone> ConnectedRegion(
|
|
|
|
|
GalaxyZone v,
|
2021-03-12 05:12:01 +00:00
|
|
|
int maxDistance = int.MaxValue)
|
|
|
|
|
{
|
|
|
|
|
return ConnectedRegion(v, x => true, maxDistance);
|
|
|
|
|
}
|
2021-03-11 11:18:21 +00:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public HashSet<GalaxyZone> ConnectedRegion(
|
|
|
|
|
GalaxyZone v,
|
|
|
|
|
Predicate<(GalaxyZone source, GalaxyZone target)> linkFilter,
|
2021-03-12 05:12:01 +00:00
|
|
|
int maxDistance = int.MaxValue)
|
2021-03-11 11:18:21 +00:00
|
|
|
{
|
2021-05-14 03:03:28 +01:00
|
|
|
var visited = new HashSet<GalaxyZone> {v};
|
2021-03-11 11:18:21 +00:00
|
|
|
int cost = 1;
|
|
|
|
|
while (true)
|
|
|
|
|
{
|
2021-03-12 05:12:01 +00:00
|
|
|
var lastCount = visited.Count;
|
|
|
|
|
// For each member, add all nodes that are connected to it but have not been visited
|
2021-03-11 11:18:21 +00:00
|
|
|
// Also, if there is a zone being ignored, do not traverse across it
|
2021-03-12 05:12:01 +00:00
|
|
|
foreach (var zone in visited.ToArray())
|
|
|
|
|
foreach (var adjacentZone in zone.AdjacentZones)
|
2021-03-11 11:18:21 +00:00
|
|
|
{
|
2021-03-12 05:12:01 +00:00
|
|
|
if(!visited.Contains(adjacentZone) && linkFilter((zone, adjacentZone)))
|
|
|
|
|
visited.Add(adjacentZone);
|
2021-03-11 11:18:21 +00:00
|
|
|
}
|
|
|
|
|
// If we have stopped finding neighbors, stop traversing
|
2021-03-12 05:12:01 +00:00
|
|
|
if (visited.Count == lastCount || cost == maxDistance)
|
|
|
|
|
return visited;
|
2021-03-11 11:18:21 +00:00
|
|
|
cost++;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-11 01:36:08 +00:00
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public GalaxyZone[] FindPath(GalaxyZone source, GalaxyZone target, bool bestFirst = false)
|
2021-03-11 01:36:08 +00:00
|
|
|
{
|
2021-03-12 05:12:01 +00:00
|
|
|
MinHeap<DijkstraVertex> unsearchedNodes = new MinHeap<DijkstraVertex>();
|
|
|
|
|
unsearchedNodes.PushObj(new DijkstraVertex{Zone = source}, 0);
|
2021-05-14 03:03:28 +01:00
|
|
|
var searched = new HashSet<GalaxyZone>();
|
2021-03-11 01:36:08 +00:00
|
|
|
while (true)
|
|
|
|
|
{
|
2021-03-12 05:12:01 +00:00
|
|
|
if(unsearchedNodes.Count == 0) return null; // No nodes left unsearched
|
|
|
|
|
var s = unsearchedNodes.PopObj(); // Lowest cost unsearched node
|
2021-03-11 01:36:08 +00:00
|
|
|
if (s.Zone == target) // We found the path
|
|
|
|
|
{
|
|
|
|
|
Stack<DijkstraVertex> path = new Stack<DijkstraVertex>(); // Since we start at the end, use a LIFO collection
|
|
|
|
|
path.Push(s);
|
|
|
|
|
while(path.Peek().Parent!=null) // Keep pushing until we reach the start, which has no parent
|
|
|
|
|
path.Push(path.Peek().Parent);
|
2021-03-12 05:12:01 +00:00
|
|
|
return path.Select(dv => dv.Zone).ToArray();
|
2021-03-11 01:36:08 +00:00
|
|
|
}
|
|
|
|
|
// For each adjacent star (filter already visited stars unless heuristic is in use)
|
2021-05-14 03:03:28 +01:00
|
|
|
IEnumerable<GalaxyZone> zonesToSearch = s.Zone.AdjacentZones;
|
2021-03-12 05:12:01 +00:00
|
|
|
if (!bestFirst) zonesToSearch = zonesToSearch.Where(z => !searched.Contains(z));
|
|
|
|
|
foreach (var dijkstraStar in zonesToSearch
|
2021-03-11 01:36:08 +00:00
|
|
|
// Cost is parent cost plus distance squared
|
2026-08-10 14:57:50 +01:00
|
|
|
.Select(zone => new DijkstraVertex {Parent = s, Zone = zone, Cost = s.Cost + lengthsq(s.Zone.Position - zone.Position)}))
|
2025-01-30 01:58:05 +00:00
|
|
|
// Add new member to list, sorted by cost plus optional heuristic
|
2026-08-10 14:57:50 +01:00
|
|
|
unsearchedNodes.PushObj(dijkstraStar, bestFirst ? dijkstraStar.Cost + lengthsq(dijkstraStar.Zone.Position - target.Position) : dijkstraStar.Cost);
|
2021-03-12 05:12:01 +00:00
|
|
|
searched.Add(s.Zone);
|
2021-03-11 01:36:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
2021-03-10 09:18:07 +00:00
|
|
|
}
|
|
|
|
|
|
2021-05-14 03:03:28 +01:00
|
|
|
public class GalaxyZone
|
2021-03-10 09:18:07 +00:00
|
|
|
{
|
|
|
|
|
public string Name;
|
|
|
|
|
public float2 Position;
|
2021-05-14 03:03:28 +01:00
|
|
|
public List<GalaxyZone> AdjacentZones = new List<GalaxyZone>();
|
2021-03-11 11:18:21 +00:00
|
|
|
public int Isolation;
|
2021-05-14 03:03:28 +01:00
|
|
|
public Dictionary<GalaxyZone, int> Distance;
|
2021-03-20 04:20:17 +00:00
|
|
|
public Faction[] Factions;
|
|
|
|
|
public Faction Owner;
|
2026-08-10 14:57:50 +01:00
|
|
|
public Zone Contents;
|
|
|
|
|
public ZonePack PackedContents;
|
2021-05-14 03:03:28 +01:00
|
|
|
public bool NamedZone;
|
|
|
|
|
public List<LocationStory> Locations = new List<LocationStory>();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class GalaxyQuest
|
|
|
|
|
{
|
|
|
|
|
public Story Story;
|
2021-05-16 05:24:51 +01:00
|
|
|
public Dictionary<string, LocationStory> KnotLocations = new Dictionary<string, LocationStory>();
|
2021-05-14 03:03:28 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class LocationStory
|
|
|
|
|
{
|
2021-05-16 05:24:51 +01:00
|
|
|
public GalaxyZone Zone;
|
2021-05-14 03:03:28 +01:00
|
|
|
public string FileName;
|
|
|
|
|
public string Name;
|
|
|
|
|
public Story Story;
|
|
|
|
|
public Faction Faction;
|
|
|
|
|
public SecurityLevel Security;
|
|
|
|
|
public LocationType Type;
|
2021-05-16 05:24:51 +01:00
|
|
|
public int Turrets;
|
|
|
|
|
public Dictionary<string, List<GalaxyQuest>> KnotQuests = new Dictionary<string, List<GalaxyQuest>>();
|
2025-01-30 01:58:05 +00:00
|
|
|
}
|