From c37d6adfa8c5750fdc1b0ad5a0cc0583ce37e486 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:25:26 +0200 Subject: [PATCH 01/12] Galaxy derives a prelude's halved faction influence instead of writing it into the catalog's Faction A prelude halved every faction's InfluenceDistance on the shared catalog object, so each later generation in the same session, prelude or main, started from an already-halved value, and a continued prelude read the unhalved one. A fixed-seed scenario relaunched in one session therefore could not reproduce its galaxy. Galaxy.InfluenceOf derives the reach from IsPrelude; Galaxy and ZoneGenerator read it. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Galaxy.cs | 33 ++++++++++---------- Assets/Scripts/ServerShared/ZoneGenerator.cs | 2 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index 730daadf0..89fa80794 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -139,6 +139,11 @@ public class Galaxy return _cache.GetAll().FirstOrDefault(f => f.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase)); } + // 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; + public Galaxy( TutorialGenerationSettings settings, SectorBackgroundSettings background, @@ -177,11 +182,7 @@ public class Galaxy factions.AddRange(neutralFactions); Factions = factions.ToArray(); - foreach (var faction in Factions) - { - FactionRelationships[faction] = FactionRelationship.Neutral; - faction.InfluenceDistance = (faction.InfluenceDistance + 1) / 2; - } + foreach (var faction in Factions) FactionRelationships[faction] = FactionRelationship.Neutral; Zones = GenerateZones(settings.ZoneCount, ref random, progressCallback); @@ -190,13 +191,13 @@ public class Galaxy CalculateDistanceMatrix(progressCallback); HomeZones[protagonistFaction] = Zones - .MaxBy(z => ConnectedRegion(z, protagonistFaction.InfluenceDistance).Count); + .MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count); HomeZones[antagonistFaction] = Zones - .MaxBy(z => ConnectedRegion(z, antagonistFaction.InfluenceDistance).Count * z.Distance[HomeZones[protagonistFaction]]); + .MaxBy(z => ConnectedRegion(z, InfluenceOf(antagonistFaction)).Count * z.Distance[HomeZones[protagonistFaction]]); HomeZones[protagonistFaction] = Zones - .MaxBy(z => ConnectedRegion(z, protagonistFaction.InfluenceDistance).Count * sqrt(z.Distance[HomeZones[antagonistFaction]])); + .MaxBy(z => ConnectedRegion(z, InfluenceOf(protagonistFaction)).Count * sqrt(z.Distance[HomeZones[antagonistFaction]])); // var antagonistRegion = ConnectedRegion(HomeZones[antagonistFaction], antagonistFaction.InfluenceDistance); // var protagonistRegion = ConnectedRegion(HomeZones[protagonistFaction], protagonistFaction.InfluenceDistance); @@ -205,13 +206,13 @@ public class Galaxy var bufferDistance = Zones.Min(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]])); var potentialBufferZones = Zones .Where(z => abs(z.Distance[HomeZones[antagonistFaction]] - z.Distance[HomeZones[protagonistFaction]]) == bufferDistance); - HomeZones[bufferFaction] = potentialBufferZones.MaxBy(z => ConnectedRegion(z, bufferFaction.InfluenceDistance).Count); + HomeZones[bufferFaction] = potentialBufferZones.MaxBy(z => ConnectedRegion(z, InfluenceOf(bufferFaction)).Count); // Place neutral headquarters away from existing factions while also maximizing territory foreach (var faction in neutralFactions) { HomeZones[faction] = Zones.MaxBy(z => - ConnectedRegion(z, faction.InfluenceDistance).Count * + ConnectedRegion(z, InfluenceOf(faction)).Count * HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z]))); } @@ -221,7 +222,7 @@ public class Galaxy .Where(z => z.Factions.Contains(antagonistFaction) && z.Factions.Contains(bufferFaction)); if (potentialQuestZones.Any()) HomeZones[questFaction] = potentialQuestZones - .MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, questFaction.InfluenceDistance).Count); + .MaxBy(z => z.Distance[HomeZones[antagonistFaction]] * ConnectedRegion(z, InfluenceOf(questFaction)).Count); else HomeZones[questFaction] = Zones .Where(z => z.Factions.Contains(antagonistFaction)) @@ -287,9 +288,9 @@ public class Galaxy // While occupying as much territory as possible foreach (var mega in bossMegas) { - HomeZones[mega] = ConnectedRegion(BossZones[mega], mega.InfluenceDistance) + HomeZones[mega] = ConnectedRegion(BossZones[mega], InfluenceOf(mega)) .MaxBy(z => - ConnectedRegion(z, mega.InfluenceDistance).Count * + ConnectedRegion(z, InfluenceOf(mega)).Count * HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z]))); } @@ -297,7 +298,7 @@ public class Galaxy foreach (var mega in Factions.Where(m => !bossMegas.Contains(m))) { HomeZones[mega] = Zones.MaxBy(z => - pow(ConnectedRegion(z, mega.InfluenceDistance).Count, HomeZones.Count) * + pow(ConnectedRegion(z, InfluenceOf(mega)).Count, HomeZones.Count) * Exit.Distance[z] * Entrance.Distance[z] * HomeZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z])) * BossZones.Values.Aggregate(1f, (i, os) => i * sqrt(os.Distance[z]))); @@ -315,14 +316,14 @@ public class Galaxy // Factions are present in all zones within their sphere of influence zone.Factions = Factions .Where(f => HomeZones.ContainsKey(f)) - .Where(f => zone.Distance[HomeZones[f]] <= f.InfluenceDistance) + .Where(f => zone.Distance[HomeZones[f]] <= InfluenceOf(f)) .ToArray(); // Owner of a zone is the faction with the nearest headquarters var nearestFaction = Factions .Where(f => HomeZones.ContainsKey(f)) .MinBy(f => (float)zone.Distance[HomeZones[f]]); - if (zone.Distance[HomeZones[nearestFaction]] <= nearestFaction.InfluenceDistance) + if (zone.Distance[HomeZones[nearestFaction]] <= InfluenceOf(nearestFaction)) zone.Owner = nearestFaction; } } diff --git a/Assets/Scripts/ServerShared/ZoneGenerator.cs b/Assets/Scripts/ServerShared/ZoneGenerator.cs index d57939168..ff25223fe 100644 --- a/Assets/Scripts/ServerShared/ZoneGenerator.cs +++ b/Assets/Scripts/ServerShared/ZoneGenerator.cs @@ -206,7 +206,7 @@ public static class ZoneGenerator var nearestFaction = galaxy.Factions.MinBy(f => galaxy.HomeZones[f].Distance[galaxyZone]); var nearestFactionHomeZone = galaxy.HomeZones[nearestFaction]; - var factionPresence = nearestFaction.InfluenceDistance - nearestFactionHomeZone.Distance[galaxyZone] + 1; + var factionPresence = galaxy.InfluenceOf(nearestFaction) - nearestFactionHomeZone.Distance[galaxyZone] + 1; var storyStations = galaxyZone.Locations.Where(story => story.Type == LocationType.Station).ToArray(); var stationCount = (int)(random.NextFloat() * (factionPresence + 1)) + storyStations.Length; From 8a0a29e0deefad920e7ef1da7298ddd1de6a7418 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:26:30 +0200 Subject: [PATCH 02/12] Galaxy.IsPrelude is the one owner of whether a run is the prelude: ActionGameManager.IsTutorial and PlayerSettings.TutorialPassed are cut IsTutorial duplicated Galaxy.IsPrelude; its readers read CurrentGalaxy.IsPrelude, and RunSave.Capture writes SavedGame.IsTutorial from galaxy.IsPrelude with no isTutorial argument. TutorialPassed was never written and its only reader was New Game's unreachable main-galaxy branch, which goes with it (S2 brings the main galaxy back as a scenario). spawnturret's two identical generator branches collapse to one. Old player.cc files still load. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/ActionGameManager.cs | 21 ++------ Assets/Scripts/ServerShared/PlayerSettings.cs | 2 +- Assets/Scripts/ServerShared/SavedGame.cs | 4 +- Assets/Scripts/UI/MainMenu.cs | 25 --------- tests/Aetheria.Shared.Tests/RunSaveTests.cs | 2 +- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 52 +++++++++++++++++++ 6 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 tests/Aetheria.Shared.Tests/ScenarioTests.cs diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index c1ee9cac4..de34c830d 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -106,7 +106,6 @@ public class ActionGameManager : MonoBehaviour } public static Galaxy CurrentGalaxy; - public static bool IsTutorial; public GameSettings Settings; //public string StarterShipTemplate = "Longinus"; @@ -250,7 +249,7 @@ public class ActionGameManager : MonoBehaviour { if (CurrentGalaxy != null) { - var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity, IsTutorial, + var (game, zones) = RunSave.Capture(CultCache, CurrentGalaxy, Zone, DockedEntity ?? CurrentEntity, _actionBarSlots.Select(s => s.Save()).ToArray()); RunSave.Commit(CultCache, game, zones, ItemManager.Lots); } @@ -578,19 +577,7 @@ public class ActionGameManager : MonoBehaviour { var nearestFaction = CurrentGalaxy.Factions.MinBy(f => CurrentGalaxy.HomeZones[f].Distance[Zone.GalaxyZone]); - var loadoutGenerator = IsTutorial ? new LoadoutGenerator( - ref ItemManager.Random, - ItemManager, - CurrentGalaxy, - Zone.GalaxyZone, - nearestFaction, - .5f) : new LoadoutGenerator( - ref ItemManager.Random, - ItemManager, - CurrentGalaxy, - Zone.GalaxyZone, - nearestFaction, - .5f); + var loadoutGenerator = new LoadoutGenerator(ref ItemManager.Random, ItemManager, CurrentGalaxy, Zone.GalaxyZone, nearestFaction, .5f); var turret = EntitySerializer.Unpack(ItemManager, Zone, loadoutGenerator.GenerateTurretLoadout()); turret.Position.xz = _currentEntity.Position.xz + @@ -728,7 +715,7 @@ public class ActionGameManager : MonoBehaviour Settings.ZoneSettings, CurrentGalaxy, galaxyZone, - IsTutorial + CurrentGalaxy.IsPrelude ); galaxyZone.Contents = new Zone(ItemManager, Settings.PlanetSettings, galaxyZone.PackedContents, galaxyZone, CurrentGalaxy); } @@ -815,7 +802,7 @@ public class ActionGameManager : MonoBehaviour PopulateLevel(CurrentGalaxy.Entrance); var failures = new List(); var staged = RunStart.Stage(ItemManager, Zone, null, Settings.StartingHullName, - IsTutorial ? CurrentGalaxy.ResolveFaction(Settings.TutorialGenerationSettings.ProtagonistFaction) : null, failures) + CurrentGalaxy.IsPrelude ? CurrentGalaxy.ResolveFaction(Settings.TutorialGenerationSettings.ProtagonistFaction) : null, failures) ?? throw new InvalidOperationException($"The new run could not stage: {string.Join("; ", failures)}"); BindToEntity(staged.Player); } diff --git a/Assets/Scripts/ServerShared/PlayerSettings.cs b/Assets/Scripts/ServerShared/PlayerSettings.cs index d3c809fed..2566dcf59 100644 --- a/Assets/Scripts/ServerShared/PlayerSettings.cs +++ b/Assets/Scripts/ServerShared/PlayerSettings.cs @@ -10,7 +10,7 @@ using static CultMath.math; public class PlayerSettings { [Key(0)] public string Name = "Anonymous"; - [Key(2)] public bool TutorialPassed; + // Keys 1 and 2 are retired; an older player.cc that still carries them loads, and they are dropped. [Key(3)] public Dictionary HashedStoryFiles = new Dictionary(); [Key(4)] public PlayerGameplaySettings GameplaySettings = new PlayerGameplaySettings(); [Key(5)] public PlayerInputSettings InputSettings = new PlayerInputSettings(); diff --git a/Assets/Scripts/ServerShared/SavedGame.cs b/Assets/Scripts/ServerShared/SavedGame.cs index 71bd625af..a674df81d 100644 --- a/Assets/Scripts/ServerShared/SavedGame.cs +++ b/Assets/Scripts/ServerShared/SavedGame.cs @@ -55,7 +55,7 @@ public static class RunSave { // The live run as plain documents. Writes nothing. public static (SavedGame Game, SavedZone[] Zones) Capture(CultCache cache, Galaxy galaxy, Zone currentZone, - Entity currentEntity, bool isTutorial, SavedActionBarBinding[] actionBar) + Entity currentEntity, SavedActionBarBinding[] actionBar) { var factions = galaxy.HomeZones.Keys.ToArray(); var game = new SavedGame @@ -74,7 +74,7 @@ public static class RunSave CurrentZoneEntity = currentZone.Entities.IndexOf(currentEntity), Entrance = Array.IndexOf(galaxy.Zones, galaxy.Entrance), Exit = Array.IndexOf(galaxy.Zones, galaxy.Exit), - IsTutorial = isTutorial, + IsTutorial = galaxy.IsPrelude, ActionBarBindings = actionBar }; diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 859d17c95..9fcff8f14 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -111,7 +111,6 @@ public class MainMenu : MonoBehaviour { try { RunSave.RequireDesigns(cache, saved); } catch (InvalidOperationException error) { Refuse("Cannot continue this run", error.Message); return; } - ActionGameManager.IsTutorial = saved.IsTutorial; ActionGameManager.CurrentGalaxy = new Galaxy(cache, saved, Debug.Log); EnterGame(); }); @@ -130,29 +129,6 @@ public class MainMenu : MonoBehaviour Dialog.AddProperty(() => generatorState); Dialog.Show(); - if (ActionGameManager.PlayerSettings.TutorialPassed) - { - var backgroundSettings = MessagePackSerializer.Deserialize( - MessagePackSerializer.Serialize(Settings.SectorBackgroundSettings)); - backgroundSettings.NoisePosition = Random.value * 1000; - ActionGameManager.IsTutorial = false; - Task.Run(() => - { - var sector = new Galaxy( - Settings.SectorGenerationSettings, - backgroundSettings, - Settings.NameGeneratorSettings, - ActionGameManager.CultCache, - Debug.Log, - setState); - Observable.NextFrame().Subscribe(_ => - { - ActionGameManager.CurrentGalaxy = sector; - EnterGame(); - }); - }).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted); - } - else { var backgroundSettings = MessagePackSerializer.Deserialize( MessagePackSerializer.Serialize(Settings.TutorialBackgroundSettings)); @@ -163,7 +139,6 @@ public class MainMenu : MonoBehaviour setState($"Finding Galaxy Position: iteration {iteration++}"); } while (backgroundSettings.CloudDensity(float2(0.5f)) < .5f); - ActionGameManager.IsTutorial = true; Task.Run(() => { var sector = new Galaxy( diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 9384b93ed..6ce426995 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -91,7 +91,7 @@ public sealed class RunSaveTests : IDisposable var galaxy = new Galaxy(cache, cache.GetGlobal(), _ => { }); var itemManager = new ItemManager(cache, new ProvenanceLedger(), TestSettings(), _ => { }); galaxy.Zones[0].Contents = new Zone(itemManager, new PlanetSettings(), new ZonePack(), galaxy.Zones[0], galaxy); - var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, false, new SavedActionBarBinding[0]); + var (game, zones) = RunSave.Capture(cache, galaxy, galaxy.Zones[0].Contents, null, new SavedActionBarBinding[0]); RunSave.Commit(cache, game, zones, RunSave.Lots(cache)); } diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs new file mode 100644 index 000000000..f35cded44 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +using System.Buffers; +using System.Collections.Generic; +using GameCult.Caching.MessagePack; +using MessagePack; +using Xunit; + +// Scenarios S2 (docs/scenarios-cut.md, R): every new run is a scenario. The fixture is RunStartTests': the shipped +// catalog, the authored settings and a prelude galaxy at a fixed seed. +public sealed partial class RunStartTests +{ + // Whether a run is the prelude is the galaxy's fact (Galaxy.IsPrelude); a save records it with no second owner. + [Fact] + public void SaveReadsPrelude() + { + var arena = Arena(null); + var (prelude, _) = RunSave.Capture(_cache, _galaxy, arena, null, new SavedActionBarBinding[0]); + Assert.True(prelude.IsTutorial); + + var (main, _) = RunSave.Capture(_cache, MainGalaxy(), arena, null, new SavedActionBarBinding[0]); + Assert.False(main.IsTutorial); + } + + // PlayerSettings Key 2 (TutorialPassed) is retired: a player.cc written with it still loads, every other field intact. + [Fact] + public void OldPlayerSettingsLoad() + { + var options = CultDocumentMessagePackSerialization.OptionsFor(typeof(PlayerSettings).Assembly); + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(7); + writer.Write("Pilot"); + writer.WriteNil(); // Key 1, retired before this cut + writer.Write(true); // Key 2, TutorialPassed + MessagePackSerializer.Serialize(ref writer, new Dictionary { ["story"] = "hash" }, options); + MessagePackSerializer.Serialize(ref writer, new PlayerGameplaySettings { TemperatureUnit = TemperatureUnit.Kelvin, SignificantDigits = 5 }, options); + MessagePackSerializer.Serialize(ref writer, new PlayerInputSettings { ActionBarInputs = { "/1" } }, options); + MessagePackSerializer.Serialize(ref writer, new PlayerGraphicsSettings { NebulaQuality = Quality.High, ShowAsteroidsInMinimap = true }, options); + writer.Flush(); + + var settings = MessagePackSerializer.Deserialize(buffer.WrittenMemory, options); + + Assert.Equal("Pilot", settings.Name); + Assert.Equal("hash", settings.HashedStoryFiles["story"]); + Assert.Equal((TemperatureUnit.Kelvin, 5), (settings.GameplaySettings.TemperatureUnit, settings.GameplaySettings.SignificantDigits)); + Assert.Equal("/1", Assert.Single(settings.InputSettings.ActionBarInputs)); + Assert.Equal((Quality.High, true), (settings.GraphicsSettings.NebulaQuality, settings.GraphicsSettings.ShowAsteroidsInMinimap)); + } +} From d0782a7d14df241e0eec39f9986130a71ffe84ba Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:33:55 +0200 Subject: [PATCH 03/12] Every new run is a scenario: the script harness, the tutorial and main galaxies as scenarios, the New Game submenu and six test arenas A Scenario is an abstract C# class compiled with the game: Generate picks the galaxy through GalaxyStage (Main or Prelude, at the scenario's seed, the background noise drawn from the same seed), Stage places the run's first entities through ScenarioStage (Fit, Bare, Generated, Preset, Player, Place, Cargo), which collects named failures and admits nothing. RunStart generates the galaxy, the arena under Ambient and admits everything through Zone.Admit, all or nothing. The aetheria.scenario record, ScenarioShip, ScenarioEntity, RunStart.Check and the null-scenario branch are cut. New Game opens a submenu: Tutorial Galaxy and Main Galaxy, then (editor and development builds) Djinni Shakedown, Starved Reactor, Arcs, Duel, Launcher Angles and Long Haul, each with its brief. MainMenu.Launch is the one way into a new run; ActionGameManager.PendingScenario carries the scenario to StartGame once. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Gameplay/ActionGameManager.cs | 13 +- Assets/Scripts/ServerShared/AetheriaStores.cs | 2 +- Assets/Scripts/ServerShared/GalaxyStage.cs | 86 ++++ .../Scripts/ServerShared/GalaxyStage.cs.meta | 11 + Assets/Scripts/ServerShared/RunStart.cs | 139 ++---- Assets/Scripts/ServerShared/Scenario.cs | 53 +-- Assets/Scripts/ServerShared/ScenarioStage.cs | 168 +++++++ .../ServerShared/ScenarioStage.cs.meta | 11 + Assets/Scripts/ServerShared/Scenarios.meta | 8 + Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 46 ++ .../ServerShared/Scenarios/Arcs.cs.meta | 11 + .../ServerShared/Scenarios/DjinniShakedown.cs | 15 + .../Scenarios/DjinniShakedown.cs.meta | 11 + Assets/Scripts/ServerShared/Scenarios/Duel.cs | 35 ++ .../ServerShared/Scenarios/Duel.cs.meta | 11 + .../ServerShared/Scenarios/LauncherAngles.cs | 33 ++ .../Scenarios/LauncherAngles.cs.meta | 11 + .../ServerShared/Scenarios/LongHaul.cs | 14 + .../ServerShared/Scenarios/LongHaul.cs.meta | 11 + .../ServerShared/Scenarios/MainGalaxy.cs | 13 + .../ServerShared/Scenarios/MainGalaxy.cs.meta | 11 + .../ServerShared/Scenarios/Scenarios.cs | 11 + .../ServerShared/Scenarios/Scenarios.cs.meta | 11 + .../ServerShared/Scenarios/StarvedReactor.cs | 29 ++ .../Scenarios/StarvedReactor.cs.meta | 11 + .../ServerShared/Scenarios/TutorialGalaxy.cs | 16 + .../Scenarios/TutorialGalaxy.cs.meta | 11 + Assets/Scripts/UI/MainMenu.cs | 108 +++-- .../HardpointOwnershipTests.cs | 2 +- tests/Aetheria.Shared.Tests/RunStartTests.cs | 446 ++---------------- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 426 ++++++++++++++++- 31 files changed, 1173 insertions(+), 611 deletions(-) create mode 100644 Assets/Scripts/ServerShared/GalaxyStage.cs create mode 100644 Assets/Scripts/ServerShared/GalaxyStage.cs.meta create mode 100644 Assets/Scripts/ServerShared/ScenarioStage.cs create mode 100644 Assets/Scripts/ServerShared/ScenarioStage.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/Arcs.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/Arcs.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/Duel.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/LongHaul.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/Scenarios.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta create mode 100644 Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs create mode 100644 Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index de34c830d..8ebcf413a 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -106,6 +106,8 @@ public class ActionGameManager : MonoBehaviour } public static Galaxy CurrentGalaxy; + // The scenario a new run starts from: MainMenu.Launch sets it, and StartGame's new-run branch consumes it once. + public static Scenario PendingScenario; public GameSettings Settings; //public string StarterShipTemplate = "Longinus"; @@ -796,14 +798,15 @@ public class ActionGameManager : MonoBehaviour var saved = CultCache.GetGlobal(); if (saved == null) { + var scenario = PendingScenario ?? throw new InvalidOperationException("A new run needs a scenario; MainMenu.Launch sets one."); + PendingScenario = null; SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance)); - // RunStart owns what a new run starts with (docs/scenarios-cut.md, 2.2); this only enters and binds. - RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, null); + // RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this only enters and binds. + RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, scenario); PopulateLevel(CurrentGalaxy.Entrance); var failures = new List(); - var staged = RunStart.Stage(ItemManager, Zone, null, Settings.StartingHullName, - CurrentGalaxy.IsPrelude ? CurrentGalaxy.ResolveFaction(Settings.TutorialGenerationSettings.ProtagonistFaction) : null, failures) - ?? throw new InvalidOperationException($"The new run could not stage: {string.Join("; ", failures)}"); + var staged = RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures) + ?? throw new InvalidOperationException($"Scenario {scenario.Name} could not stage: {string.Join("; ", failures)}"); BindToEntity(staged.Player); } else diff --git a/Assets/Scripts/ServerShared/AetheriaStores.cs b/Assets/Scripts/ServerShared/AetheriaStores.cs index 55d8c2f92..8f0816ef9 100644 --- a/Assets/Scripts/ServerShared/AetheriaStores.cs +++ b/Assets/Scripts/ServerShared/AetheriaStores.cs @@ -6,7 +6,7 @@ using GameCult.Caching.MessagePack; // Aetheria's stores: one home store per type, composed here and nowhere else. public static class AetheriaStores { - public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout), typeof(Scenario) }; + public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(NameFile), typeof(InputLayout), typeof(Loadout) }; public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) }; public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) }; diff --git a/Assets/Scripts/ServerShared/GalaxyStage.cs b/Assets/Scripts/ServerShared/GalaxyStage.cs new file mode 100644 index 000000000..f4ac7d5f8 --- /dev/null +++ b/Assets/Scripts/ServerShared/GalaxyStage.cs @@ -0,0 +1,86 @@ +using System; +using System.IO; +using GameCult.Caching; +using MessagePack; +using static CultMath.math; +using Random = CultMath.Random; + +// The galaxy half of a scenario's vocabulary (docs/scenarios-cut.md, R.4): the two kinds of galaxy the game generates, +// at the scenario's seed. The menu hands it the generation inputs; RunStart.Generate sets the seed and hands it to +// Scenario.Generate. The background's noise position derives from the same seed, so a fixed seed fixes the whole +// galaxy, and a clock seed makes every launch a new one. +public sealed class GalaxyStage +{ + private readonly SectorGenerationSettings _sector; + private readonly SectorBackgroundSettings _sectorBackground; + private readonly TutorialGenerationSettings _prelude; + private readonly SectorBackgroundSettings _preludeBackground; + private readonly NameGeneratorSettings _names; + private readonly CultCache _cache; + private readonly PlayerSettings _playerSettings; + private readonly DirectoryInfo _narrativeDirectory; + private readonly Action _log; + private readonly Action _progress; + private readonly Func _clock; + + public GalaxyStage( + SectorGenerationSettings sector, + SectorBackgroundSettings sectorBackground, + TutorialGenerationSettings prelude, + SectorBackgroundSettings preludeBackground, + NameGeneratorSettings names, + CultCache cache, + PlayerSettings playerSettings, + DirectoryInfo narrativeDirectory, + Action log, + Action progress = null, + Func clock = null) + { + _sector = sector; + _sectorBackground = sectorBackground; + _prelude = prelude; + _preludeBackground = preludeBackground; + _names = names; + _cache = cache; + _playerSettings = playerSettings; + _narrativeDirectory = narrativeDirectory; + _log = log; + _progress = progress; + _clock = clock ?? (() => (uint) (DateTime.Now.Ticks % uint.MaxValue)); + } + + // The seed this generation runs at, never zero: the scenario's, or the clock's when the scenario's is zero. + public uint Seed { get; private set; } + + internal void SeedFrom(Scenario scenario) + { + var clock = _clock(); + Seed = scenario.Seed != 0 ? scenario.Seed : clock != 0 ? clock : 1; + } + + // A main galaxy: the sector settings, factions drawn from the catalog, an exit to reach. + public Galaxy Main() + { + var background = Copy(_sectorBackground); + var random = new Random(Seed); + background.NoisePosition = random.NextFloat() * 1000; + return new Galaxy(_sector, background, _names, _cache, _log, _progress, Seed); + } + + // A prelude galaxy: the tutorial settings and their fixed factions, centred in dense cloud. + public Galaxy Prelude() + { + var background = Copy(_preludeBackground); + var random = new Random(Seed); + var iteration = 1; + do + { + background.NoisePosition = random.NextFloat() * 1000; + _progress?.Invoke($"Finding Galaxy Position: iteration {iteration++}"); + } while (background.CloudDensity(float2(.5f)) < .5f); + return new Galaxy(_prelude, background, _names, _cache, _playerSettings, _narrativeDirectory, _log, _progress, Seed); + } + + private static SectorBackgroundSettings Copy(SectorBackgroundSettings settings) => + MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(settings)); +} diff --git a/Assets/Scripts/ServerShared/GalaxyStage.cs.meta b/Assets/Scripts/ServerShared/GalaxyStage.cs.meta new file mode 100644 index 000000000..9d23d3f4a --- /dev/null +++ b/Assets/Scripts/ServerShared/GalaxyStage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f5cf458f47074786bf11e72b5d93c545 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/RunStart.cs b/Assets/Scripts/ServerShared/RunStart.cs index 53807b562..1eab125bb 100644 --- a/Assets/Scripts/ServerShared/RunStart.cs +++ b/Assets/Scripts/ServerShared/RunStart.cs @@ -1,131 +1,54 @@ using System; using System.Collections.Generic; using System.Linq; -using CultMath; -using static CultMath.math; -using float2 = CultMath.float2; -using float3 = CultMath.float3; -// The owner of what a new run starts with (docs/scenarios-cut.md, 2.2): how the arena zone is generated, the player -// ship, and a scenario's other entities with their stance, pilot and cargo. A plain New Game is the absent-scenario -// case of the same path. Nothing else generates, unpacks or admits a new run's player; everything it stages joins the -// arena through Zone.Admit. It holds no Unity type. +// The owner of a new run (docs/scenarios-cut.md, R.4). Every new run is a scenario: RunStart generates its galaxy, +// generates its arena (the galaxy's entrance zone) under the scenario's Ambient rule, and admits what the scenario +// stages through Zone.Admit, all of it or none of it. A scenario only declares; nothing else generates a new run's +// galaxy, generates or admits its player, or chooses its hull or faction. It holds no Unity type. public static class RunStart { public sealed class Staged { public Ship Player; - public List Entities; // the scenario's entities, in its order; empty with no scenario + public List Entities; // what the scenario placed besides the player, in its order + } + + // The scenario's galaxy, at the scenario's seed or, when that is zero, the clock's. + public static Galaxy Generate(Scenario scenario, GalaxyStage stage) + { + stage.SeedFrom(scenario); + return scenario.Generate(stage) ?? throw new InvalidOperationException($"Scenario {scenario.Name} generated no galaxy."); } - // The arena is the galaxy's entrance zone, generated under the scenario's Ambient rule; with no scenario, ambient. public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario) => galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone( - itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario?.Ambient ?? true); + itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient); - // The check entry: every failure staging this scenario would hit, found with no zone. With no galaxy there is no - // availability to filter by (LoadoutGenerator.IsAvailable), and a scenario's arena is a prelude, which offers every - // product anyway. It touches no store, so the menu can call it before it clears the saved run. - public static List Check(ItemManager itemManager, Scenario scenario) - { - var failures = new List(); - if (scenario != null) Build(itemManager, null, scenario, _ => true, failures); - return failures; - } - - // Stages a new run into its arena, all-or-nothing: null, with every failure listed and the zone unchanged, unless - // everything builds. With no scenario: a generated ship of the startingHull (any ship hull when it is empty) for - // defaultFaction, at the origin. With one: its player and entities, each at its authored position and heading with - // its cargo, each entity's stance set both ways with the player, and a Minion for each piloted one. + // Runs the scenario's Stage, then admits its player and everything it placed: each entity's stance set both ways + // with the player, a pilot for each piloted one. Returns null, with every failure listed and the arena unchanged, + // unless the whole scenario staged. public static Staged Stage(ItemManager itemManager, Zone arena, Scenario scenario, string startingHull, - Faction defaultFaction, List failures) - { - if (scenario == null) - { - var generator = new LoadoutGenerator(ref itemManager.Random, itemManager, arena.Galaxy, arena.GalaxyZone, defaultFaction, 2); - var pack = generator.GenerateShipLoadout(data => string.IsNullOrEmpty(startingHull) || data.Name == startingHull); - if (pack == null) - { - failures.Add($"player: no ship hull named {startingHull}"); - return null; - } - - var ship = (Ship) EntitySerializer.Unpack(itemManager, arena, pack); - ship.IsPlayerShip = true; - ship.Position = float3.zero; - arena.Admit(ship, piloted: false); - return new Staged { Player = ship, Entities = new List() }; - } - - // Presets carry no faction, so availability is the galaxy's alone. - var availability = new LoadoutGenerator(ref itemManager.Random, itemManager, arena.Galaxy, arena.GalaxyZone, null, 2); - var built = Build(itemManager, arena, scenario, availability.IsAvailable, failures); - if (built == null) return null; - - var (player, entities) = built.Value; - player.IsPlayerShip = true; - arena.Admit(player, piloted: false); - for (var i = 0; i < entities.Count; i++) - { - var hostile = scenario.Entities[i].Stance == ScenarioStance.Hostile; - entities[i].SetIff(player, hostile); - player.SetIff(entities[i], hostile); - arena.Admit(entities[i], scenario.Entities[i].Piloted); - } - return new Staged { Player = player, Entities = entities }; - } - - // Everything a scenario places, built and positioned but admitted nowhere; null when anything failed. - private static (Ship player, List entities)? Build(ItemManager itemManager, Zone zone, Scenario scenario, - Predicate isAvailable, List failures) + TutorialGenerationSettings tutorialGenerationSettings, List failures) { + var stage = new ScenarioStage(itemManager, arena, startingHull, tutorialGenerationSettings); + scenario.Stage(stage); var reported = failures.Count; - Entity player = null; - if (scenario.Player == null) failures.Add("player: the scenario places no player"); - else - { - player = Place(itemManager, zone, scenario.Player, "player", isAvailable, failures); - if (player != null && !(player is Ship)) failures.Add($"player: {player.Name} is not a ship hull"); - } - - var entities = new List(); - for (var i = 0; i < scenario.Entities.Count; i++) - { - var where = $"entity {i}"; - var entity = Place(itemManager, zone, scenario.Entities[i], where, isAvailable, failures); - if (entity != null && scenario.Entities[i].Piloted && !(entity is Ship)) - failures.Add($"{where}: {entity.Name} is not a ship hull, so it cannot be piloted"); - entities.Add(entity); - } - return failures.Count > reported ? null : ((Ship) player, entities); - } - - private static Entity Place(ItemManager itemManager, Zone zone, ScenarioShip placed, string where, - Predicate isAvailable, List failures) - { - var loadout = itemManager.ItemData.Get(placed.Loadout); - if (loadout == null) - { - failures.Add($"{where}: preset {placed.Loadout.Key} is not in the catalog"); - return null; - } - - var reported = failures.Count; - var materialized = new List(); - var entity = Loadouts.Materialize(itemManager, zone, loadout, isAvailable, materialized); - failures.AddRange(materialized.Select(failure => $"{where} ({loadout.Name}): {failure}")); - var cargo = placed.Cargo.Select(design => Loadouts.Resolve(itemManager, design, isAvailable, $"{where} cargo", failures)).ToList(); + failures.AddRange(stage.Failures); + if (!stage.PlayerPlaced) + failures.Add("player: the scenario places no player"); if (failures.Count > reported) return null; - entity.Position.xz = placed.Position; - if (lengthsq(placed.Direction) > 0) entity.Direction = normalize(placed.Direction); - var bay = entity.CargoBays.FirstOrDefault(); - foreach (var build in cargo) + var player = stage.PlayerShip; + player.IsPlayerShip = true; + arena.Admit(player, piloted: false); + foreach (var placed in stage.Placed) { - var item = build(); - if (bay == null || !bay.TryStore(item)) - failures.Add($"{where} cargo: no room for {itemManager.GetData(item).Name}"); + var hostile = placed.Stance == ScenarioStance.Hostile; + placed.Entity.SetIff(player, hostile); + player.SetIff(placed.Entity, hostile); + arena.Admit(placed.Entity, placed.Piloted); } - return failures.Count > reported ? null : entity; + return new Staged { Player = player, Entities = stage.Placed.Select(placed => placed.Entity).ToList() }; } } diff --git a/Assets/Scripts/ServerShared/Scenario.cs b/Assets/Scripts/ServerShared/Scenario.cs index d2d0da48f..ba7165a19 100644 --- a/Assets/Scripts/ServerShared/Scenario.cs +++ b/Assets/Scripts/ServerShared/Scenario.cs @@ -1,38 +1,27 @@ -using System.Collections.Generic; -using GameCult.Caching; -using MessagePack; -using CultMath; - -// A scenario is an arena whose conditions are present at spawn (docs/scenarios-cut.md): the player's ship and every -// other entity already fitted and placed. Authored catalog data, read at runtime; RunStart stages it into a new run. -// It sets conditions and nothing else: once staged, its entities are ordinary run state. -[CultDocument("aetheria.scenario", "1"), MessagePackObject] -public class Scenario +// A scenario is one way to set up a new run (docs/scenarios-cut.md, R): a script, compiled with the game, that says +// which galaxy to generate and what the run starts with. New Game lists them (Scenarios). RunStart runs one: it +// generates the galaxy through GalaxyStage, the arena under Ambient, and admits what Stage placed through a +// ScenarioStage. A scenario only declares; once staged, everything it placed is ordinary run state. +public abstract class Scenario { - [CultName, Key(0)] public string Name; - [Key(1)] public string Brief; // the conditions in a line or two, shown in the menu - [Key(2)] public uint Seed; // the arena galaxy derives from it, so every launch has one layout - [Key(3)] public bool Ambient; // false: planets, orbits and stations, but no generated ship or turret - [Key(4)] public ScenarioShip Player; - [Key(5)] public List Entities = new List(); -} + public abstract string Name { get; } -// A preset placed in the arena. A ship hull stages a Ship; a turret hull an orbital entity with no orbit, which stays -// where it is put. -[MessagePackObject] -public class ScenarioShip -{ - [Key(0)] public CultRecordRef Loadout; - [Key(1)] public float2 Position; // zone xz - [Key(2)] public float2 Direction; // zero keeps the entity's default heading - [Key(3)] public List> Cargo = new List>(); // first cargo bay -} + // The conditions and what to verify in them, shown under the scenario's button. + public abstract string Brief { get; } -[MessagePackObject] -public class ScenarioEntity : ScenarioShip -{ - [Key(4)] public ScenarioStance Stance; // applied both ways between this entity and the player - [Key(5)] public bool Piloted; // a Minion flies it; otherwise it has no agent and stays put + // Zero draws the seed from the clock, so each launch is a new galaxy. Anything else fixes the galaxy, its + // background and its arena, so every launch has the same layout. + public virtual uint Seed => 0; + + // False: the arena keeps its planets, orbits and stations but generates no ship or turret, so nothing wanders into + // the scenario's conditions. + public virtual bool Ambient => true; + + // Which galaxy the run is in: stage.Main() or stage.Prelude(). + public abstract Galaxy Generate(GalaxyStage stage); + + // What the run starts with: exactly one stage.Player, and anything else the scenario places. + public abstract void Stage(ScenarioStage stage); } public enum ScenarioStance diff --git a/Assets/Scripts/ServerShared/ScenarioStage.cs b/Assets/Scripts/ServerShared/ScenarioStage.cs new file mode 100644 index 000000000..241684a35 --- /dev/null +++ b/Assets/Scripts/ServerShared/ScenarioStage.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CultMath; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; + +// The run half of a scenario's vocabulary (docs/scenarios-cut.md, R.4). A scenario's Stage builds its fits and places +// them here; nothing joins the arena until Stage returns with no failure, and then RunStart admits it all at once. +// Every verb names what it could not do instead of throwing, so one bad name in a script lists every bad name. +// Designs and hulls are named as the catalog names them. Only RunStart builds a stage. +public sealed class ScenarioStage +{ + private readonly ItemManager _items; + private readonly Predicate _available; + private readonly List _failures = new List(); + private readonly List _placed = new List(); + private int _placements; + + internal ScenarioStage(ItemManager items, Zone arena, string startingHull, TutorialGenerationSettings tutorialGenerationSettings) + { + _items = items; + Arena = arena; + StartingHull = startingHull; + TutorialGenerationSettings = tutorialGenerationSettings; + // A fit names designs, not manufacturers, so what the galaxy offers decides which product builds each one. + _available = new LoadoutGenerator(ref items.Random, items, arena.Galaxy, arena.GalaxyZone, null, 2).IsAvailable; + } + + public Galaxy Galaxy => Arena.Galaxy; + public Zone Arena { get; } + public string StartingHull { get; } + public TutorialGenerationSettings TutorialGenerationSettings { get; } + + internal IReadOnlyList Failures => _failures; + internal bool PlayerPlaced { get; private set; } + internal Ship PlayerShip { get; private set; } + internal IReadOnlyList Placed => _placed; + + internal sealed class Placement + { + public Entity Entity; + public ScenarioStance Stance; + public bool Piloted; + } + + // A hull fitted slot by slot: each slot a design, the hull cell it goes in, and its rotation. + public ScenarioFit Fit(string hull, params (string design, int2 cell, ItemRotation rotation)[] slots) + { + var where = $"fit {hull}"; + var hullData = Named(hull, where); + var loadout = new Loadout { Name = hull, WeaponGroups = new int[0][] }; + if (hullData != null) loadout.Hull = _items.ItemData.RefOf(hullData); + foreach (var (design, cell, rotation) in slots) + { + var data = Named(design, where); + if (data == null) continue; + loadout.Slots.Add(new LoadoutSlot { Design = _items.ItemData.RefOf(data), Position = cell, Rotation = rotation }); + } + return hullData != null && loadout.Slots.Count == slots.Length ? new ScenarioFit(loadout) : null; + } + + // A hull with nothing fitted. + public ScenarioFit Bare(string hull) => Fit(hull); + + // A fit generated as the game generates its ships, for the faction when one is given. An empty hull means any + // ship hull. + public ScenarioFit Generated(string hull, Faction faction = null) + { + var generator = new LoadoutGenerator(ref _items.Random, _items, Galaxy, Arena.GalaxyZone, faction, 2); + var pack = generator.GenerateShipLoadout(data => string.IsNullOrEmpty(hull) || data.Name == hull); + if (pack != null) return new ScenarioFit(pack); + _failures.Add($"generated: no ship hull named {hull}"); + return null; + } + + // A loadout preset from the catalog, as capturepreset or Studio wrote it. + public ScenarioFit Preset(string name) + { + var preset = _items.ItemData.GetAll().FirstOrDefault(loadout => loadout.Name == name); + if (preset != null) return new ScenarioFit(preset); + _failures.Add($"preset {name}: no such preset"); + return null; + } + + // The player's ship. A run has exactly one, and it is a ship hull. + public Ship Player(ScenarioFit fit, float2 at, float2 facing = default) + { + if (PlayerPlaced) + { + _failures.Add("player: the scenario places a second player"); + return null; + } + PlayerPlaced = true; + var entity = Build(fit, "player", at, facing); + if (entity == null) return null; + if (entity is Ship ship) return PlayerShip = ship; + _failures.Add($"player: {entity.Name} is not a ship hull"); + return null; + } + + // Anything else in the arena. A stance holds both ways between it and the player. A piloted ship gets the zone's + // pilot; an unpiloted entity has none and goes nowhere on its own. A turret hull stays where it is put. + public Entity Place(ScenarioFit fit, float2 at, float2 facing = default, ScenarioStance stance = ScenarioStance.Neutral, bool piloted = false) + { + var where = $"entity {_placements++}"; + var entity = Build(fit, where, at, facing); + if (entity == null) return null; + if (piloted && !(entity is Ship)) _failures.Add($"{where}: {entity.Name} is not a ship hull, so it cannot be piloted"); + _placed.Add(new Placement { Entity = entity, Stance = stance, Piloted = piloted }); + return entity; + } + + // Items in the entity's first cargo bay, one per design named. + public void Cargo(Entity entity, params string[] designs) + { + if (entity == null) return; + var where = $"cargo of {entity.Name}"; + var bay = entity.CargoBays.FirstOrDefault(); + foreach (var design in designs) + { + var data = Named(design, where); + if (data == null) continue; + var build = Loadouts.Resolve(_items, _items.ItemData.RefOf(data), _available, where, _failures); + if (build != null && (bay == null || !bay.TryStore(build()))) + _failures.Add($"{where}: no room for {design}"); + } + } + + private Entity Build(ScenarioFit fit, string where, float2 at, float2 facing) + { + if (fit == null) return null; // the verb that made it named why + + Entity entity; + if (fit.Pack != null) entity = EntitySerializer.Unpack(_items, Arena, fit.Pack); + else + { + var failures = new List(); + entity = Loadouts.Materialize(_items, Arena, fit.Loadout, _available, failures); + _failures.AddRange(failures.Select(failure => $"{where} ({fit.Loadout.Name}): {failure}")); + if (entity == null) return null; + } + + entity.Position = float3(at.x, 0, at.y); + if (lengthsq(facing) > 0) entity.Direction = normalize(facing); + return entity; + } + + private T Named(string name, string where) where T : EquippableItemData + { + var named = _items.ItemData.GetAll().Where(data => data.Name == name).Take(2).ToArray(); + if (named.Length == 1) return named[0]; + _failures.Add(named.Length == 0 ? $"{where}: no design named {name}" : $"{where}: more than one design is named {name}"); + return null; + } +} + +// What a scenario places: a fit built from named designs or a preset, or a ship generated as the game generates them. +// Only ScenarioStage makes one. +public sealed class ScenarioFit +{ + internal readonly Loadout Loadout; + internal readonly EntityPack Pack; + + internal ScenarioFit(Loadout loadout) => Loadout = loadout; + internal ScenarioFit(EntityPack pack) => Pack = pack; +} diff --git a/Assets/Scripts/ServerShared/ScenarioStage.cs.meta b/Assets/Scripts/ServerShared/ScenarioStage.cs.meta new file mode 100644 index 000000000..91b73afb6 --- /dev/null +++ b/Assets/Scripts/ServerShared/ScenarioStage.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d8cd00dc23604dfb91e128e1a9a4a3d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios.meta b/Assets/Scripts/ServerShared/Scenarios.meta new file mode 100644 index 000000000..d6472c75c --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 35866d82219241ae9fdb0030039afbed +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs new file mode 100644 index 000000000..d1ceb61e0 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -0,0 +1,46 @@ +using static CultMath.math; +using static ItemRotation; + +public sealed class Arcs : Scenario +{ + public override string Name => "Arcs"; + public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the bow " + + "hull can be hit, nothing fires at the stern one through the ship, the turret tracks you all the way round."; + public override uint Seed => 103; + public override bool Ambient => false; + + // Where the targets sit: the player is at the origin facing +z, and every mount fires forward. + public static readonly CultMath.float2 Bow = float2(0, 400), Stern = float2(0, -400), Turret = float2(600, 0); + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) + { + var fighter = stage.Fit("LonginusX", + ("Cockpit 2x2", int2(2, 6), None), + ("Traction", int2(2, 3), None), + ("Core Power", int2(2, 1), None), + ("GT 3K", int2(0, 5), None), + ("GT 3K", int2(5, 5), None), + ("FastBlast+-", int2(1, 8), None), + ("FastBlast+-", int2(4, 8), None), + ("Iapyx", int2(1, 2), CounterClockwise), + ("Iapyx", int2(4, 2), Clockwise), + ("not if i see you first", int2(3, 10), None), + ("Fire Control Array", int2(2, 5), None)); + var turret = stage.Fit("Turret", + ("ClearPath", int2(1, 2), None), + ("ClearPath", int2(5, 2), None), + ("Skiron", int2(0, 2), CounterClockwise), + ("Skiron", int2(7, 2), Clockwise), + ("Core Power", int2(3, 3), None), + ("Turret Control Module", int2(3, 5), None), + ("not if i see you first", int2(4, 5), None), + ("Fire Control Array", int2(3, 6), None)); + + stage.Player(fighter, float2(0, 0), facing: float2(0, 1)); + stage.Place(stage.Bare("LonginusX"), Bow); + stage.Place(stage.Bare("LonginusX"), Stern); + stage.Place(turret, Turret, stance: ScenarioStance.Hostile); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs.meta b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs.meta new file mode 100644 index 000000000..70bff8250 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 724a07b881f34e8c9b32b3f2f338ac78 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs new file mode 100644 index 000000000..720fa2d08 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs @@ -0,0 +1,15 @@ +using float2 = CultMath.float2; + +public sealed class DjinniShakedown : Scenario +{ + public override string Name => "Djinni Shakedown"; + public override string Brief => "A fitted Djinni in empty space. Verify: its thrusters move and turn it; with the hull " + + "offline and thrusters firing, its speed does not run away."; + public override uint Seed => 101; + public override bool Ambient => false; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) => + stage.Player(stage.Generated("Djinni"), float2.zero); +} diff --git a/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs.meta b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs.meta new file mode 100644 index 000000000..4f25ab345 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/DjinniShakedown.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1afb559630c241ed813ddd3c444c618b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/Duel.cs b/Assets/Scripts/ServerShared/Scenarios/Duel.cs new file mode 100644 index 000000000..fa270a4bd --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Duel.cs @@ -0,0 +1,35 @@ +using static CultMath.math; +using static ItemRotation; + +public sealed class Duel : Scenario +{ + public override string Name => "Duel"; + public override string Brief => "One piloted, hostile LonginusX with slow GT 3K missiles and a Spectra in its hold. Verify: rolled " + + "hits and near-misses, subsystem aim, beam reads above bow, armour takes the missile, loot keeps its brand."; + public override uint Seed => 104; + public override bool Ambient => false; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) + { + var fighter = stage.Fit("LonginusX", + ("Cockpit 2x2", int2(2, 6), None), + ("Traction", int2(2, 3), None), + ("Core Power", int2(2, 1), None), + ("GT 3K", int2(0, 5), None), + ("GT 3K", int2(5, 5), None), + ("FastBlast+-", int2(1, 8), None), + ("FastBlast+-", int2(4, 8), None), + ("Iapyx", int2(1, 2), CounterClockwise), + ("Iapyx", int2(4, 2), Clockwise), + ("not if i see you first", int2(3, 10), None), + ("PotaT+-", int2(1, 5), None), + ("Fire Control Array", int2(2, 5), None), + ("Store-All Plus", int2(2, 8), None)); + + stage.Player(fighter, float2(0, 0)); + var rival = stage.Place(fighter, float2(0, 900), facing: float2(0, -1), stance: ScenarioStance.Hostile, piloted: true); + stage.Cargo(rival, "Spectra"); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta b/Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta new file mode 100644 index 000000000..4acea5ed7 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Duel.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f6ba813d4a9447c89e1bbb6997b7ac35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs new file mode 100644 index 000000000..a734be6e1 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs @@ -0,0 +1,33 @@ +using static CultMath.math; +using static ItemRotation; + +public sealed class LauncherAngles : Scenario +{ + public override string Name => "Launcher Angles"; + public override string Brief => "A GT 3K, a pswarm and two FastBlasts; a hostile LonginusX broadside on, a bare Longinus beside it. " + + "Verify: hits pulse only the facing edge; a pswarm round whose target dies fades unexploded."; + public override uint Seed => 105; + public override bool Ambient => false; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) + { + var launcher = stage.Fit("LonginusX", + ("Cockpit 2x2", int2(2, 6), None), + ("Traction", int2(2, 3), None), + ("Core Power", int2(2, 1), None), + ("GT 3K", int2(0, 5), None), + ("pswarm", int2(5, 5), None), + ("FastBlast+-", int2(1, 8), None), + ("FastBlast+-", int2(4, 8), None), + ("Iapyx", int2(1, 2), CounterClockwise), + ("Iapyx", int2(4, 2), Clockwise), + ("not if i see you first", int2(3, 10), None), + ("Fire Control Array", int2(2, 5), None)); + + stage.Player(launcher, float2(0, 0)); + stage.Place(stage.Bare("LonginusX"), float2(0, 900), facing: float2(1, 0), stance: ScenarioStance.Hostile); + stage.Place(stage.Bare("Longinus"), float2(300, 700)); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta new file mode 100644 index 000000000..82a27fcac --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/LauncherAngles.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 58cfaf062ddf458c99c12f3652243b45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs b/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs new file mode 100644 index 000000000..bb5381b10 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs @@ -0,0 +1,14 @@ +using float2 = CultMath.float2; + +public sealed class LongHaul : Scenario +{ + public override string Name => "Long Haul"; + public override string Brief => "A standard ship in a populated galaxy. Verify: memory stays flat across zones with kills; " + + "warp, quit and Continue keep items, tier and brand; death disables Continue."; + public override uint Seed => 106; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) => + stage.Player(stage.Generated(stage.StartingHull), float2.zero); +} diff --git a/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta b/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta new file mode 100644 index 000000000..984d274d6 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/LongHaul.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a7911611f9845709e9ae5ba34d91dfb +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs b/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs new file mode 100644 index 000000000..f32e205d3 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs @@ -0,0 +1,13 @@ +using float2 = CultMath.float2; + +// The full galaxy: factions drawn from the catalog, bosses on the road to the exit, entered in the starting hull. +public sealed class MainGalaxy : Scenario +{ + public override string Name => "Main Galaxy"; + public override string Brief => "The full galaxy: factions drawn at random, bosses guarding the road to the exit."; + + public override Galaxy Generate(GalaxyStage stage) => stage.Main(); + + public override void Stage(ScenarioStage stage) => + stage.Player(stage.Generated(stage.StartingHull), float2.zero); +} diff --git a/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta b/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta new file mode 100644 index 000000000..4e862188d --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/MainGalaxy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c7f6bdb3ed7b4b1aa137569c7e79bccd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs new file mode 100644 index 000000000..31890d789 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs @@ -0,0 +1,11 @@ +// Every way to set up a new run, in the order New Game lists them. Modes are the game; Tests are arenas built to check +// one system each, listed only in editor and development builds. +public static class Scenarios +{ + public static readonly Scenario[] Modes = { new TutorialGalaxy(), new MainGalaxy() }; + + public static readonly Scenario[] Tests = + { + new DjinniShakedown(), new StarvedReactor(), new Arcs(), new Duel(), new LauncherAngles(), new LongHaul() + }; +} diff --git a/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta new file mode 100644 index 000000000..4c856d4d9 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/Scenarios.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12f60a9ad25c4aa0a9b8b098d385ea0e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs new file mode 100644 index 000000000..ccbf9baa8 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs @@ -0,0 +1,29 @@ +using static CultMath.math; +using static ItemRotation; + +public sealed class StarvedReactor : Scenario +{ + public override string Name => "Starved Reactor"; + public override string Brief => "Two Spectras on a MoveOnPro, which can fall to a quarter of Core Power's charge, and a bare hull " + + "to shoot. Verify: starvation reads as starvation, brownout degrades, refills stutter."; + public override uint Seed => 102; + public override bool Ambient => false; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) + { + var starved = stage.Fit("LonginusX", + ("Cockpit 2x2", int2(2, 6), None), + ("Traction", int2(2, 3), None), + ("MoveOnPro", int2(2, 1), None), + ("Spectra", int2(1, 8), None), + ("Spectra", int2(4, 8), None), + ("Iapyx", int2(1, 2), CounterClockwise), + ("Iapyx", int2(4, 2), Clockwise), + ("not if i see you first", int2(3, 10), None), + ("Fire Control Array", int2(2, 5), None)); + stage.Player(starved, float2(0, 0)); + stage.Place(stage.Bare("LonginusX"), float2(0, 400)); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta new file mode 100644 index 000000000..e13077120 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/StarvedReactor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91a81ac4784c4066969cac11ac1937b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs b/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs new file mode 100644 index 000000000..161a52348 --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs @@ -0,0 +1,16 @@ +using float2 = CultMath.float2; + +// New Game as it has always been: a prelude galaxy, entered in the starting hull of the protagonist faction. +public sealed class TutorialGalaxy : Scenario +{ + public override string Name => "Tutorial Galaxy"; + public override string Brief => "A small galaxy of fixed factions, entered in a ship of the protagonist faction."; + + public override Galaxy Generate(GalaxyStage stage) => stage.Prelude(); + + public override void Stage(ScenarioStage stage) + { + var protagonist = stage.Galaxy.ResolveFaction(stage.TutorialGenerationSettings.ProtagonistFaction); + stage.Player(stage.Generated(stage.StartingHull, protagonist), float2.zero); + } +} diff --git a/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta b/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta new file mode 100644 index 000000000..3f7c8777a --- /dev/null +++ b/Assets/Scripts/ServerShared/Scenarios/TutorialGalaxy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 94d688b37c5044dd9b67865c706d8918 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index 9fcff8f14..b5bdab2ee 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -5,15 +5,12 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using MessagePack; using TMPro; using UniRx; using UnityEngine; using UnityEngine.SceneManagement; using UnityEngine.UI; using static CultMath.math; -using float2 = CultMath.float2; -using Random = UnityEngine.Random; public class MainMenu : MonoBehaviour { @@ -118,45 +115,8 @@ public class MainMenu : MonoBehaviour _nextMenu.panel.AddButton("New Game", () => { - RunSave.Clear(ActionGameManager.CultCache); - var generatorState = "Loading Database Contents"; - Action setState = s => generatorState = s; - + ShowScenarios(); Fade(true); - _nextMenu.panel.gameObject.SetActive(false); - Dialog.Clear(); - Dialog.Title.text = "Generating Galaxy"; - Dialog.AddProperty(() => generatorState); - Dialog.Show(); - - { - var backgroundSettings = MessagePackSerializer.Deserialize( - MessagePackSerializer.Serialize(Settings.TutorialBackgroundSettings)); - int iteration = 1; - do - { - backgroundSettings.NoisePosition = Random.value * 1000; - setState($"Finding Galaxy Position: iteration {iteration++}"); - } while (backgroundSettings.CloudDensity(float2(0.5f)) < .5f); - - Task.Run(() => - { - var sector = new Galaxy( - Settings.TutorialGenerationSettings, - backgroundSettings, - Settings.NameGeneratorSettings, - ActionGameManager.CultCache, - ActionGameManager.PlayerSettings, - ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), - Debug.Log, - setState); - Observable.NextFrame().Subscribe(_ => - { - ActionGameManager.CurrentGalaxy = sector; - EnterGame(); - }); - }).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted); - } }); _nextMenu.panel.AddButton("Settings", () => @@ -167,6 +127,72 @@ public class MainMenu : MonoBehaviour _nextMenu.panel.AddButton("Quit", Application.Quit); } + // New Game: every way to set up a run, each with its brief. The test arenas are listed only in editor and + // development builds. + private void ShowScenarios() + { + _nextMenu.panel.Clear(); + _nextMenu.panel.Title.text = TitleSubtitle("new game", "scenarios"); + AddScenarios(Scenarios.Modes); + if (Debug.isDebugBuild) + { + _nextMenu.panel.AddSection("test arenas"); + AddScenarios(Scenarios.Tests); + } + _nextMenu.panel.AddButton("Back", + () => + { + ShowMain(); + Fade(false); + }); + } + + private void AddScenarios(IEnumerable scenarios) + { + foreach (var scenario in scenarios) + { + _nextMenu.panel.AddButton(scenario.Name, () => Launch(scenario)); + _nextMenu.panel.AddProperty(() => scenario.Brief); + } + } + + // The only way into a new run: clears the saved run, generates the scenario's galaxy off the main thread, and + // enters the game, where StartGame has RunStart stage it. + private void Launch(Scenario scenario) + { + RunSave.Clear(ActionGameManager.CultCache); + var generatorState = "Loading Database Contents"; + + Fade(true); + _nextMenu.panel.gameObject.SetActive(false); + Dialog.Clear(); + Dialog.Title.text = "Generating Galaxy"; + Dialog.AddProperty(() => generatorState); + Dialog.Show(); + + var stage = new GalaxyStage( + Settings.SectorGenerationSettings, + Settings.SectorBackgroundSettings, + Settings.TutorialGenerationSettings, + Settings.TutorialBackgroundSettings, + Settings.NameGeneratorSettings, + ActionGameManager.CultCache, + ActionGameManager.PlayerSettings, + ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), + Debug.Log, + state => generatorState = state); + Task.Run(() => + { + var galaxy = RunStart.Generate(scenario, stage); + Observable.NextFrame().Subscribe(_ => + { + ActionGameManager.CurrentGalaxy = galaxy; + ActionGameManager.PendingScenario = scenario; + EnterGame(); + }); + }).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted); + } + // The one way into the game scene. Mod ship prototypes import asynchronously at boot, so the scene waits for them, // and a preload that could not run at all stops here with its reason rather than in the first zone. private void EnterGame() => StartCoroutine(EnterGameWhenModShipsAreReady()); diff --git a/tests/Aetheria.Shared.Tests/HardpointOwnershipTests.cs b/tests/Aetheria.Shared.Tests/HardpointOwnershipTests.cs index 383425c60..12daaee32 100644 --- a/tests/Aetheria.Shared.Tests/HardpointOwnershipTests.cs +++ b/tests/Aetheria.Shared.Tests/HardpointOwnershipTests.cs @@ -229,7 +229,7 @@ public sealed partial class RunStartTests var low = Heater("P30 Low Heater", 260, 10); var high = Heater("P30 High Heater", 320, 7); var hull = Hull("Djinni"); - var zone = Arena(new Scenario { Ambient = false }); + var zone = Arena(false); List Bills(params GearData[] order) { diff --git a/tests/Aetheria.Shared.Tests/RunStartTests.cs b/tests/Aetheria.Shared.Tests/RunStartTests.cs index c3ac81ff5..bfb71e59d 100644 --- a/tests/Aetheria.Shared.Tests/RunStartTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartTests.cs @@ -16,10 +16,9 @@ using static CultMath.math; using float2 = CultMath.float2; using float3 = CultMath.float3; -// Scenarios Cut 1 (docs/scenarios-cut.md, 2.2): RunStart owns what a new run starts with, and Zone.Admit is the one -// admission into a zone. Every test stages into a real arena: the entrance zone of a prelude galaxy at a fixed seed, -// built from the shipped catalog and the authored settings (Assets/Resources/Settings.asset). The presets and -// scenarios live in a scratch copy of the catalog, because the shipped one holds none. +// RunStart owns what a new run starts with, and Zone.Admit is the one admission into a zone (docs/scenarios-cut.md, +// 2.2 and R). Every test stages into a real arena: the entrance zone of a prelude galaxy at a fixed seed, built from a +// scratch copy of the shipped catalog and the authored settings (Assets/Resources/Settings.asset). public sealed partial class RunStartTests : IDisposable { private const uint GalaxySeed = 1; @@ -32,6 +31,7 @@ public sealed partial class RunStartTests : IDisposable private readonly PlanetSettings _planetSettings; private readonly string _startingHull; private readonly Faction _protagonist; + private readonly TutorialGenerationSettings _tutorialSettings; public RunStartTests() { @@ -49,7 +49,7 @@ public sealed partial class RunStartTests : IDisposable var authored = AuthoredSettings.Load(repo); _zoneSettings = authored.Read("ZoneSettings"); _planetSettings = authored.Read("PlanetSettings"); - var tutorial = authored.Read("TutorialGenerationSettings"); + var tutorial = _tutorialSettings = authored.Read("TutorialGenerationSettings"); _galaxy = new Galaxy(tutorial, authored.Read("TutorialBackgroundSettings"), authored.Read("NameGeneratorSettings"), _cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, GalaxySeed); @@ -80,8 +80,9 @@ public sealed partial class RunStartTests : IDisposable throw new DirectoryNotFoundException("Run from inside the Aetheria repository."); } - private Zone Arena(Scenario scenario) => - new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, _galaxy, scenario), _galaxy.Entrance, _galaxy); + // The fixture galaxy's arena, with or without its generated ships and turrets. + private Zone Arena(bool ambient) => + new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(ambient, _ => { })), _galaxy.Entrance, _galaxy); private HullData Hull(string name) => _cache.GetAll().Single(hull => hull.Name == name); @@ -92,314 +93,12 @@ public sealed partial class RunStartTests : IDisposable .OrderBy(hull => hull.Name, StringComparer.Ordinal) .First(); - private CultRecordRef Preset(Loadout loadout) - { - var existing = _cache.GetAll().FirstOrDefault(other => other.Name == loadout.Name); - if (existing != null) return _cache.RefOf(existing); - _cache.Upsert(loadout); - return _cache.RefOf(loadout); - } - - private CultRecordRef Bare(HullData hull) => - Preset(new Loadout { Name = "bare " + hull.Name, Hull = _cache.RefOf(hull), WeaponGroups = new int[0][] }); - - // A fully fitted preset: a generated ship of this hull, captured. - private Loadout Fitted(string hullName) - { - var generator = new LoadoutGenerator(ref _items.Random, _items, null, null, null, 2); - var ship = (Ship) EntitySerializer.Unpack(_items, null, generator.GenerateShipLoadout(hull => hull.Name == hullName)); - Assert.NotEmpty(ship.CargoBays); - return Loadouts.Capture(_items, ship, "fitted " + hullName); - } - - private static ScenarioShip At(CultRecordRef preset, float x, float z) => - new ScenarioShip { Loadout = preset, Position = float2(x, z) }; - - private static ScenarioEntity Placed(CultRecordRef preset, float x, float z, ScenarioStance stance, bool piloted) => - new ScenarioEntity { Loadout = preset, Position = float2(x, z), Stance = stance, Piloted = piloted }; - - private Scenario Quiet(ScenarioShip player, params ScenarioEntity[] entities) => - new Scenario { Name = "test", Seed = GalaxySeed, Ambient = false, Player = player, Entities = entities.ToList() }; - - private RunStart.Staged Stage(Zone zone, Scenario scenario) - { - var failures = new List(); - var staged = RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures); - Assert.Empty(failures); - Assert.NotNull(staged); - return staged; - } + // A loadout with only its hull, in memory. + private Loadout BareLoadout(HullData hull) => + new Loadout { Name = "bare " + hull.Name, Hull = _cache.RefOf(hull), WeaponGroups = new int[0][] }; private static int AgentsOf(Zone zone, Entity entity) => zone.Agents.Count(agent => agent.Ship == entity); - // New Game is the absent-scenario case: the authored starting hull, flagged as the player's, admitted at the - // origin, for the protagonist faction, with no agent. The hull name is an input: staging honours another one too. - [Fact] - public void WithNoScenarioThePlayerIsTheStartingHullAtTheOrigin() - { - Assert.Equal("LonginusX", _startingHull); - foreach (var hullName in new[] { _startingHull, "Djinni" }) - { - var zone = Arena(null); - var failures = new List(); - var staged = RunStart.Stage(_items, zone, null, hullName, _protagonist, failures); - Assert.Empty(failures); - var player = staged.Player; - Assert.Equal(hullName, player.HullData.Name); - Assert.True(player.IsPlayerShip); - SafeAssert.In(zone, player, "the player is admitted"); - Assert.True(zone == player.Zone, "the player's zone is the arena"); - Assert.Equal(float3.zero, player.Position); - Assert.Same(_protagonist, player.Faction); - Assert.Equal(0, AgentsOf(zone, player)); - Assert.Empty(staged.Entities); - } - } - - // A scenario's player is its preset, fitted slot for slot, at the authored position and heading, carrying its cargo. - [Fact] - public void AScenarioPlayerIsItsPresetAtItsPlaceWithItsCargo() - { - var fitted = Fitted("Djinni"); - var preset = Preset(fitted); - // Two one-cell designs a manufacturer makes, by name, so the cargo is ordinary branded stock - var cargo = _cache.GetAll() - .Where(design => design.Shape.Coordinates.Length == 1) - .Where(design => _cache.GetAll().Any(p => p.Design.Key.Equals(_cache.RefOf(design).Key))) - .OrderBy(design => design.Name, StringComparer.Ordinal) - .Take(2) - .Select(design => new CultRecordRef(_cache.RefOf(design).Key)) - .ToList(); - Assert.Equal(2, cargo.Count); - var zone = Arena(null); - var scenario = Quiet(new ScenarioShip { Loadout = preset, Position = float2(120, -40), Direction = float2(3, 4), Cargo = cargo }); - - var player = Stage(zone, scenario).Player; - - Assert.Equal("Djinni", player.HullData.Name); - Assert.True(player.IsPlayerShip); - SafeAssert.In(zone, player, "the player is admitted"); - Assert.Equal(float3(120, 0, -40), player.Position); - Assert.True(length(player.Direction - float2(.6f, .8f)) < 1e-5f, $"heading {player.Direction}"); - Assert.Equal( - fitted.Slots.Select(slot => (slot.Position, slot.Design.Key)).OrderBy(s => s.ToString()), - player.Equipment.Concat(player.CargoBays).Concat(player.DockingBays) - .Where(item => item != player.EquippedHull) - .Select(item => (item.Position, _cache.RefOf(_items.GetData(item.EquippableItem)).Key)) - .OrderBy(s => s.ToString())); - Assert.Equal(cargo.Select(design => design.Key.Value).OrderBy(k => k), - player.CargoBays.First().Cargo.Keys.Select(item => item.Data.Key.Value).OrderBy(k => k)); - } - - // A piloted entity gets exactly one agent; an unpiloted one gets none, and nothing steers it. (It is not frozen: - // the zone's gravity still moves it, as it moves any ship, so the rule is observed on its steering inputs.) - [Fact] - public void APilotedEntityHasOneAgentAndAnUnpilotedOneNoneAndStaysPut() - { - var zone = Arena(null); - var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0), - Placed(Preset(Fitted("LonginusX")), 300, 0, ScenarioStance.Neutral, piloted: true), - Placed(Bare(Hull("LonginusX")), -300, 0, ScenarioStance.Neutral, piloted: false))); - var piloted = staged.Entities[0]; - var unpiloted = staged.Entities[1]; - - Assert.Equal(1, AgentsOf(zone, piloted)); - Assert.Equal(0, AgentsOf(zone, unpiloted)); - Assert.Equal(0, AgentsOf(zone, staged.Player)); - SafeAssert.In(zone, piloted, "the piloted entity is admitted"); - SafeAssert.In(zone, unpiloted, "the unpiloted entity is admitted"); - - var look = unpiloted.LookDirection; - for (var tick = 0; tick < 50; tick++) - { - zone.Update(.1f); - Assert.Equal(float2.zero, ((Ship) unpiloted).MovementDirection); - Assert.Equal(look, unpiloted.LookDirection); - } - Assert.Equal(0, AgentsOf(zone, unpiloted)); - } - - // A stance holds both ways between the entity and the player. - [Fact] - public void AStanceIsSetBothWays() - { - var longinus = Bare(Hull("LonginusX")); - var staged = Stage(Arena(null), Quiet(At(Bare(Hull("Djinni")), 0, 0), - Placed(longinus, 300, 0, ScenarioStance.Hostile, piloted: false), - Placed(longinus, -300, 0, ScenarioStance.Neutral, piloted: false))); - var player = staged.Player; - var hostile = staged.Entities[0]; - var neutral = staged.Entities[1]; - - Assert.True(player.IsHostileTo(hostile)); - Assert.True(hostile.IsHostileTo(player)); - Assert.False(player.IsHostileTo(neutral)); - Assert.False(neutral.IsHostileTo(player)); - - // Admission activates: the live hostility each side tracks (what targeting and grudges read) agrees. - Assert.True(player.EntityHostility[hostile]); - Assert.True(hostile.EntityHostility[player]); - Assert.False(player.EntityHostility[neutral]); - Assert.False(neutral.EntityHostility[player]); - } - - // A stance lasts the sitting: the player docking and undocking leaves it standing both ways. It goes with an entity - // gone for good: once an entity is destroyed, the player holds no stance toward it. - [Fact] - public void AStanceSurvivesDockingButNotDestruction() - { - var zone = Arena(null); - var longinus = Bare(Hull("LonginusX")); - var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0), - Placed(longinus, 300, 0, ScenarioStance.Hostile, piloted: false), - Placed(longinus, -300, 0, ScenarioStance.Hostile, piloted: false))); - var player = staged.Player; - var hostile = staged.Entities[0]; - var doomed = staged.Entities[1]; - var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); - - Assert.NotNull(station.TryDock(player)); - SafeAssert.NotIn(zone, player, "a docked ship is out of the zone"); - Assert.True(station.TryUndock(player)); - Assert.True(player.IsHostileTo(hostile), "the player's stance was lost over docking"); - Assert.True(hostile.IsHostileTo(player), "the entity's stance was lost over docking"); - Assert.True(player.EntityHostility[hostile], "the player's live hostility was lost over docking"); - Assert.True(hostile.EntityHostility[player], "the entity's live hostility was lost over docking"); - - // Presets carry no faction, so without its stance the derived rule is not hostile - Assert.True(player.Faction == null && doomed.Faction == null, "the fixture's ships are factionless"); - Assert.True(player.IsHostileTo(doomed)); - doomed.Hull.Durability = 0; - doomed.HullDamage.OnNext(1); - SafeAssert.NotIn(zone, doomed, "a destroyed entity leaves the zone"); - Assert.False(player.IsHostileTo(doomed), "the stance toward a destroyed entity outlived it"); - } - - // Undocking admits the ship unpiloted: back in the zone, active (its live hostility is tracked), with no agent. - [Fact] - public void UndockingAdmitsWithNoAgent() - { - var zone = Arena(null); - var player = Stage(zone, null).Player; - var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); - var agents = zone.Agents.Count; - - Assert.NotNull(station.TryDock(player)); - SafeAssert.NotIn(zone, player, "a docked ship is out of the zone"); - Assert.True(station.TryUndock(player)); - SafeAssert.In(zone, player, "an undocked ship is back in the zone"); - Assert.Equal(agents, zone.Agents.Count); - Assert.Equal(0, AgentsOf(zone, player)); - Assert.True(player.EntityHostility.ContainsKey(station), "an undocked ship is active"); - } - - // Ambient: false keeps the stations and drops every generated ship and turret; ambient, and a plain New Game, keep - // them. The arena is the galaxy's entrance pack. The entrance holds a story station, placed as the narrative would - // place one, so a story station's own turrets are dropped too. - [Fact] - public void AQuietArenaHasNoGeneratedShipOrTurretButKeepsItsStations() - { - (int ships, int turrets, int stations) Census(ZonePack pack) => ( - pack.Entities.OfType().Count(), - pack.Entities.OfType().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Turret), - pack.Entities.OfType().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Station)); - - _galaxy.Entrance.Locations.Add(new LocationStory - { - Zone = _galaxy.Entrance, Name = "story", Type = LocationType.Station, Faction = _protagonist, Turrets = 2 - }); - var plain = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, null)); - var ambient = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scenario { Ambient = true })); - var quietPack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scenario { Ambient = false }); - var quiet = Census(quietPack); - - Assert.Same(quietPack, _galaxy.Entrance.PackedContents); - Assert.True(plain.ships > 0 && plain.turrets > 0, $"a plain arena generated {plain}"); - Assert.True(ambient.ships > 0 && ambient.turrets > 0, $"an ambient arena generated {ambient}"); - Assert.Equal(0, quiet.ships); - Assert.Equal(0, quiet.turrets); - Assert.True(quiet.stations > 0); - Assert.Equal(ambient.stations, quiet.stations); - Assert.Contains(quietPack.Entities.OfType(), station => station.Story == 0); - } - - // A turret preset stages as an orbital entity with no orbit, which stays where it is put. - [Fact] - public void ATurretPresetStagesAsAStationaryOrbitalEntity() - { - var zone = Arena(null); - var staged = Stage(zone, Quiet(At(Bare(Hull("Djinni")), 0, 0), - Placed(Bare(TurretHull()), 200, 50, ScenarioStance.Hostile, piloted: false))); - var turret = Assert.IsType(staged.Entities[0]); - Assert.False(turret.OrbitData.IsSet()); - SafeAssert.In(zone, turret, "the turret is admitted"); - for (var tick = 0; tick < 20; tick++) zone.Update(.1f); - Assert.Equal(float2(200, 50), turret.Position.xz); - } - - // A station preset is refused, and so is a turret as the player or a piloted turret: each is a staging failure - // naming the hull, with nothing admitted. - [Fact] - public void AStationPresetOrAMisplacedTurretIsAStagingFailure() - { - var station = _cache.GetAll().First(hull => hull.HullType == HullType.Station); - var turret = TurretHull(); - var djinni = Bare(Hull("Djinni")); - foreach (var (scenario, expected) in new[] - { - (Quiet(At(djinni, 0, 0), Placed(Bare(station), 200, 0, ScenarioStance.Neutral, false)), $"{station.Name} is a station hull"), - (Quiet(At(Bare(turret), 0, 0)), $"player: {turret.Name} is not a ship hull"), - (Quiet(At(djinni, 0, 0), Placed(Bare(turret), 200, 0, ScenarioStance.Neutral, true)), "cannot be piloted"), - }) - { - var zone = Arena(null); - var entities = zone.Entities.ToList(); - var agents = zone.Agents.ToList(); - var failures = new List(); - Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures)); - Assert.Contains(failures, failure => failure.Contains(expected)); - SafeAssert.Unchanged(zone, entities, agents); - Assert.Contains(RunStart.Check(_items, scenario), failure => failure.Contains(expected)); - } - } - - // A scenario naming a missing design or preset fails with each named; the check entry finds the same failures with - // no zone and writes no document, and staging admits nothing, not even the parts that built: a player and a piloted - // entity that build cleanly stay out when a later entity fails. - [Fact] - public void AMissingDesignFailsNamedAndChangesNothing() - { - var djinni = Bare(Hull("Djinni")); - var scenario = Quiet( - new ScenarioShip { Loadout = djinni, Cargo = { new CultRecordRef(new CultRecordKey("absent-design")) } }, - Placed(Bare(Hull("LonginusX")), 300, 0, ScenarioStance.Hostile, piloted: true), - Placed(new CultRecordRef(new CultRecordKey("absent-preset")), -300, 0, ScenarioStance.Neutral, false)); - var zone = Arena(null); - var entities = zone.Entities.ToList(); - var agents = zone.Agents.ToList(); - var documents = _cache.AllStoredDocuments.Count(); - - var checkFailures = RunStart.Check(_items, scenario); - Assert.Equal(2, checkFailures.Count); - Assert.Contains("absent-design", checkFailures[0]); - Assert.Contains("absent-preset", checkFailures[1]); - Assert.Equal(documents, _cache.AllStoredDocuments.Count()); - - var failures = new List(); - Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _protagonist, failures)); - Assert.Equal(checkFailures, failures); - SafeAssert.Unchanged(zone, entities, agents); - - var lateFailure = Quiet(At(djinni, 0, 0), - Placed(Bare(Hull("LonginusX")), 300, 0, ScenarioStance.Hostile, piloted: true), - Placed(new CultRecordRef(new CultRecordKey("absent-preset")), -300, 0, ScenarioStance.Neutral, false)); - failures.Clear(); - Assert.Null(RunStart.Stage(_items, zone, lateFailure, _startingHull, _protagonist, failures)); - Assert.Contains("absent-preset", Assert.Single(failures)); - SafeAssert.Unchanged(zone, entities, agents); - Assert.Empty(RunStart.Check(_items, null)); - } - // A heater is gear whose thermostat runs its heat only below a target: a low-pass thermotoggle, then heat. private static bool IsHeater(EquippedItem item) => item.Behaviors.OfType().Any(t => !t.ThermotoggleData.HighPass) && item.Behaviors.OfType().Any(); @@ -408,7 +107,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void EveryGeneratedStationCarriesOneHeaterAndAReactor() { - var zone = Arena(null); + var zone = Arena(true); var generator = new LoadoutGenerator(ref _items.Random, _items, _galaxy, _galaxy.Entrance, _protagonist, .5f); for (var i = 0; i < 8; i++) { @@ -422,7 +121,7 @@ public sealed partial class RunStartTests : IDisposable // A main-sector galaxy (not the prelude, which offers every product): each station's gear must come from // manufacturers its faction can reach, the galaxy's factions its allegiance names. - private Galaxy MainGalaxy() + private Galaxy MainSectorGalaxy() { var authored = AuthoredSettings.Load(FindRepoRoot()); return new Galaxy(authored.Read("SectorGenerationSettings"), @@ -443,9 +142,9 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void EveryFactionsStationsCarryOnlyGearItCanReachIncludingAHeaterAndAReactor() { - var galaxy = MainGalaxy(); + var galaxy = MainSectorGalaxy(); Assert.False(galaxy.IsPrelude); - var zone = Arena(null); + var zone = Arena(true); foreach (var faction in galaxy.Factions) { var generator = new LoadoutGenerator(ref _items.Random, _items, galaxy, galaxy.Entrance, faction, .5f); @@ -466,7 +165,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AFactionReachesItsOwnGearAndItsAllegianceNamesOnlyOthers() { - var galaxy = MainGalaxy(); + var galaxy = MainSectorGalaxy(); var products = _cache.GetAll().Where(p => p.Manufacturer.IsSet()).ToArray(); foreach (var faction in galaxy.Factions) { @@ -483,7 +182,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AStationWithoutZhestokostAccessIsPoweredAndStaysAboveFreezing() { - var galaxy = MainGalaxy(); + var galaxy = MainSectorGalaxy(); Faction Named(string shortName) => _cache.GetAll().Single(f => f.ShortName == shortName); var coop = new Faction { Name = "Test Cooperative", ShortName = "Coop" }; _cache.Upsert(coop); @@ -497,7 +196,7 @@ public sealed partial class RunStartTests : IDisposable var own = new FactionProductData { Name = "Cooperative Capacitor", Design = products.First().Design, Manufacturer = _cache.RefOf(coop) }; _cache.Upsert(own); Assert.True(generator.IsAvailable(own), "a faction reaches its own manufacturer's gear"); - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs Entity station = null; for (var i = 0; i < 8; i++) { @@ -525,7 +224,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AnIdleStationStaysAboveFreezing() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); var heater = station.Equipment.Single(IsHeater); var coldest = float.MaxValue; @@ -542,7 +241,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AStationHeaterHeatsBelowItsTargetAndNotAbove() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); var heater = station.Equipment.Single(IsHeater); var target = heater.Behaviors.OfType().Single().TargetTemperature; @@ -565,7 +264,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AHeaterAboveItsTargetDrawsNoPower() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); var heater = station.Equipment.Single(IsHeater); var target = heater.Behaviors.OfType().Single().TargetTemperature; @@ -616,7 +315,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void NoOrbitStandsStillAgainstItsParent() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs Assert.True(zone.AsteroidBelts.Count > 0, "the arena has an asteroid belt to race against"); float2 Relative(Orbit orbit, bool previous) => (previous ? orbit.PreviousPosition : orbit.Position) - @@ -638,7 +337,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void EveryAsteroidInABeltIsPlacedForTheSameTime() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var (key, belt) = zone.AsteroidBelts.First(); var asteroids = ((AsteroidBeltData) zone.Planets[key]).Asteroids; const float dt = .02f; @@ -668,7 +367,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void MiningWhileTheBeltUpdatesNeverRacesIt() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var (key, _) = zone.AsteroidBelts.First(); var data = (AsteroidBeltData) zone.Planets[key]; var count = data.Asteroids.Length; @@ -963,7 +662,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void TheBusBillsAHeaterExactlyWhenItsThermostatIsOpen() { - var zone = Arena(new Scenario { Ambient = false }); // no ships: nothing but the zone runs + var zone = Arena(false); // no ships: nothing but the zone runs var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); var heater = station.Equipment.Single(IsHeater); var thermostat = heater.Behaviors.OfType().Single(); @@ -1007,7 +706,7 @@ public sealed partial class RunStartTests : IDisposable { Name = "Two Group Gear", Design = new CultRecordRef(_cache.RefOf(gear).Key), Manufacturer = _cache.RefOf(_protagonist) }); - var zone = Arena(new Scenario { Ambient = false }); + var zone = Arena(false); var ship = BareHull(Hull("Djinni")); ship.Zone = zone; Assert.True(ship.TryEquip(Instance(gear))); @@ -1018,59 +717,6 @@ public sealed partial class RunStartTests : IDisposable Assert.True(abs(ship.PowerBus.TotalDemand - 7) < .01f, $"billed {ship.PowerBus.TotalDemand} for a closed 10 and an ungated 7"); } - // Q3, with the operator's 2026-09-30 ruling on the ten product-less designs: a design no product sells is a - // scenario test design and nothing else. A test design is one a scenario places: its preset's hull or a slot of - // it, or cargo it carries. These are the unsold designs no scenario places, by name. - private static List UnsoldAndUnplaced(CultCache cache) - { - var sold = cache.GetAll().Select(product => product.Design.Key).ToHashSet(); - var placed = new HashSet(); - foreach (var scenario in cache.GetAll()) - foreach (var ship in scenario.Entities.Prepend(scenario.Player).Where(ship => ship != null)) - { - foreach (var cargo in ship.Cargo) placed.Add(cargo.Key); - var preset = cache.Get(ship.Loadout); - if (preset == null) continue; - placed.Add(preset.Hull.Key); - foreach (var slot in preset.Slots) placed.Add(slot.Design.Key); - } - return cache.GetAll() - .Where(design => !sold.Contains(cache.RefOf(design).Key) && !placed.Contains(cache.RefOf(design).Key)) - .Select(design => design.Name).OrderBy(name => name, StringComparer.Ordinal).ToList(); - } - - // The shipped catalog holds no unsold design outside a scenario (today, none unsold at all). A new unsold design is - // flagged until a scenario places it, in a preset's slot or as an entity's cargo. - [Fact] - public void EveryUnsoldDesignIsAScenarioTestDesign() - { - Assert.Empty(UnsoldAndUnplaced(_cache)); - - GearData Probe(string name) - { - var design = new GearData { Name = name, Hardpoint = HardpointType.Tool, Shape = new Shape(), Durability = 1 }; - _cache.Upsert(design); - return design; - } - var fitted = Probe("Census Fitted"); - var carried = Probe("Census Carried"); - Assert.Equal(new[] { "Census Carried", "Census Fitted" }, UnsoldAndUnplaced(_cache)); - - var preset = Preset(new Loadout - { - Name = "census preset", Hull = _cache.RefOf(Hull("Djinni")), WeaponGroups = new int[0][], - Slots = { new LoadoutSlot { Design = new CultRecordRef(_cache.RefOf(fitted).Key) } } - }); - var scenario = Quiet(At(Bare(Hull("Djinni")), 0, 0), Placed(preset, 100, 0, ScenarioStance.Neutral, false)); - scenario.Name = "census"; - _cache.Upsert(scenario); - Assert.Equal(new[] { "Census Carried" }, UnsoldAndUnplaced(_cache)); - - scenario.Entities[0].Cargo.Add(new CultRecordRef(_cache.RefOf(carried).Key)); - _cache.Upsert(scenario); - Assert.Empty(UnsoldAndUnplaced(_cache)); - } - // The Q3 rule must not reach a real hull: the hulls scenarios rely on each have a product, and materialize bare, // branded, under the prelude's availability. [Fact] @@ -1080,7 +726,7 @@ public sealed partial class RunStartTests : IDisposable foreach (var hull in new[] { Hull("Djinni"), Hull("LonginusX"), TurretHull() }) { var failures = new List(); - var entity = Loadouts.Materialize(_items, null, _cache.Get(Bare(hull)), available.IsAvailable, failures); + var entity = Loadouts.Materialize(_items, null, BareLoadout(hull), available.IsAvailable, failures); Assert.Empty(failures); Assert.NotNull(_items.Brand(entity.Hull).Maker); } @@ -1091,7 +737,7 @@ public sealed partial class RunStartTests : IDisposable [Fact] public void AdmitIsTheOnlyAgentCreator() { - var pack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, null); + var pack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(true, _ => { })); var packedPlayer = pack.Entities.OfType().First(); packedPlayer.IsPlayerShip = true; var zone = new Zone(_items, _planetSettings, pack, _galaxy.Entrance, _galaxy); @@ -1099,44 +745,10 @@ public sealed partial class RunStartTests : IDisposable Assert.True(ships.Count > 1); foreach (var ship in ships) Assert.Equal(ship.IsPlayerShip ? 0 : 1, AgentsOf(zone, ship)); - var arriving = (Ship) Loadouts.Materialize(_items, zone, _cache.Get(Bare(Hull("Djinni"))), _ => true, new List()); + var arriving = (Ship) Loadouts.Materialize(_items, zone, BareLoadout(Hull("Djinni")), _ => true, new List()); var agents = zone.Agents.Count; zone.Admit(arriving, piloted: false); SafeAssert.In(zone, arriving, "the arrival is admitted"); Assert.Equal(agents, zone.Agents.Count); } - - // The scenario type is catalog data: it round-trips through the catalog file. - [Fact] - public void AScenarioRoundTripsThroughTheCatalog() - { - var djinni = Bare(Hull("Djinni")); - var cargo = new CultRecordRef(_cache.RefOf(Hull("LonginusX")).Key); - var scenario = new Scenario - { - Name = "Round trip", Brief = "one of everything", Seed = 42, Ambient = true, - Player = new ScenarioShip { Loadout = djinni, Position = float2(1, 2), Direction = float2(0, -1), Cargo = { cargo } }, - Entities = { Placed(djinni, 3, 4, ScenarioStance.Hostile, piloted: true) } - }; - // Written through its own cache over a second copy of the catalog, disposed, then read back fresh - var copy = Path.Combine(_root, "roundtrip.cc"); - File.Copy(Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc"), copy); - using (var writer = new CultCache(Registry())) - { - writer.AddBackingStore(new SingleFileMessagePackBackingStore(copy), AetheriaStores.CatalogTypes); - writer.Commit(batch => batch.Upsert(typeof(Loadout), _cache.Get(djinni), djinni.Key)); - writer.Upsert(scenario); - writer.FlushAsync().Wait(); - } - - using var reopened = new CultCache(Registry()); - reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy, true), AetheriaStores.CatalogTypes); - var loaded = Assert.Single(reopened.GetAll()); - Assert.Equal(("Round trip", "one of everything", 42u, true), (loaded.Name, loaded.Brief, loaded.Seed, loaded.Ambient)); - Assert.Equal(djinni.Key, loaded.Player.Loadout.Key); - Assert.Equal((float2(1, 2), float2(0, -1)), (loaded.Player.Position, loaded.Player.Direction)); - Assert.Equal(cargo.Key, Assert.Single(loaded.Player.Cargo).Key); - var entity = Assert.Single(loaded.Entities); - Assert.Equal((djinni.Key, float2(3, 4), ScenarioStance.Hostile, true), (entity.Loadout.Key, entity.Position, entity.Stance, entity.Piloted)); - } } diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index f35cded44..c2a4e36b3 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -2,29 +2,443 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +using System; using System.Buffers; using System.Collections.Generic; +using System.IO; +using System.Linq; +using CultMath; +using GameCult.Caching; using GameCult.Caching.MessagePack; using MessagePack; using Xunit; +using static CultMath.math; +using float2 = CultMath.float2; +using float3 = CultMath.float3; -// Scenarios S2 (docs/scenarios-cut.md, R): every new run is a scenario. The fixture is RunStartTests': the shipped -// catalog, the authored settings and a prelude galaxy at a fixed seed. +// Scenarios S2 (docs/scenarios-cut.md, R): every new run is a scenario. The fixture is RunStartTests': a scratch copy +// of the shipped catalog, the authored settings and a prelude galaxy at a fixed seed. public sealed partial class RunStartTests { + // A test's own scenario: a script given inline, staged into the fixture's arena or launched whole. + private sealed class Scripted : Scenario + { + private readonly bool _ambient; + private readonly Action _stage; + private readonly uint _seed; + private readonly Func _generate; + + public Scripted(bool ambient, Action stage, uint seed = GalaxySeed, Func generate = null) + { + _ambient = ambient; + _stage = stage; + _seed = seed; + _generate = generate ?? (galaxy => galaxy.Prelude()); + } + + public override string Name => "Scripted"; + public override string Brief => "A test's own scenario."; + public override uint Seed => _seed; + public override bool Ambient => _ambient; + public override Galaxy Generate(GalaxyStage stage) => _generate(stage); + public override void Stage(ScenarioStage stage) => _stage(stage); + } + + private static IEnumerable EveryScenario => Scenarios.Modes.Concat(Scenarios.Tests); + + // The galaxy inputs the menu hands to RunStart, read from the authored settings. + private GalaxyStage Inputs(Func clock = null) + { + var authored = AuthoredSettings.Load(FindRepoRoot()); + return new GalaxyStage( + authored.Read("SectorGenerationSettings"), + authored.Read("SectorBackgroundSettings"), + authored.Read("TutorialGenerationSettings"), + authored.Read("TutorialBackgroundSettings"), + authored.Read("NameGeneratorSettings"), + _cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, clock); + } + + // What MainMenu.Launch and StartGame do with a scenario: its galaxy, its arena, then staging. + private (Galaxy galaxy, Zone arena, RunStart.Staged staged, List failures) Launch(Scenario scenario, GalaxyStage inputs = null) + { + var galaxy = RunStart.Generate(scenario, inputs ?? Inputs()); + var arena = new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, galaxy, scenario), galaxy.Entrance, galaxy); + var failures = new List(); + var staged = RunStart.Stage(_items, arena, scenario, _startingHull, _tutorialSettings, failures); + return (galaxy, arena, staged, failures); + } + + private RunStart.Staged Stage(Zone zone, Scenario scenario) + { + var failures = new List(); + var staged = RunStart.Stage(_items, zone, scenario, _startingHull, _tutorialSettings, failures); + Assert.True(failures.Count == 0, string.Join("; ", failures)); + Assert.NotNull(staged); + return staged; + } + + // Staging that must fail: it returns null and admits nothing. Returns the failures. + private List Refused(Zone zone, Scenario scenario) + { + var entities = zone.Entities.ToList(); + var agents = zone.Agents.ToList(); + var failures = new List(); + Assert.Null(RunStart.Stage(_items, zone, scenario, _startingHull, _tutorialSettings, failures)); + SafeAssert.Unchanged(zone, entities, agents); + return failures; + } + + // The rot test: every scenario New Game lists generates its galaxy and stages against the live catalog and the + // authored settings with no failure. A script naming a design the catalog lost fails here, by name. + [Fact] + public void EveryScenarioStages() + { + Assert.Equal(EveryScenario.Count(), EveryScenario.Select(scenario => scenario.Name).Distinct().Count()); + foreach (var scenario in EveryScenario) + { + var (_, arena, staged, failures) = Launch(scenario, Inputs(() => GalaxySeed)); + Assert.True(failures.Count == 0, $"{scenario.Name}: {string.Join("; ", failures)}"); + Assert.True(staged.Player.IsPlayerShip, $"{scenario.Name}: the player is not flagged"); + SafeAssert.In(arena, staged.Player, $"{scenario.Name}: the player is admitted"); + foreach (var entity in staged.Entities) SafeAssert.In(arena, entity, $"{scenario.Name}: {entity.Name} is admitted"); + Assert.False(string.IsNullOrWhiteSpace(scenario.Brief), $"{scenario.Name} has no brief"); + } + } + + // The two game modes are what New Game did: the tutorial a prelude galaxy and a starting-hull ship of the + // protagonist faction at the entrance's origin; the main galaxy a main galaxy and a starting-hull ship there. Both + // arenas keep their generated ships. + [Fact] + public void ModesMatchOldNewGame() + { + Assert.Equal("LonginusX", _startingHull); + + var tutorial = Launch(new TutorialGalaxy(), Inputs(() => GalaxySeed)); + Assert.Empty(tutorial.failures); + Assert.True(tutorial.galaxy.IsPrelude, "the tutorial is a prelude galaxy"); + Assert.Equal(_startingHull, tutorial.staged.Player.HullData.Name); + Assert.Same(tutorial.galaxy.ResolveFaction(_tutorialSettings.ProtagonistFaction), tutorial.staged.Player.Faction); + Assert.Equal(float3.zero, tutorial.staged.Player.Position); + Assert.Empty(tutorial.staged.Entities); + Assert.Contains(tutorial.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false }); + + var main = Launch(new MainGalaxy(), Inputs(() => GalaxySeed)); + Assert.Empty(main.failures); + Assert.False(main.galaxy.IsPrelude, "the main galaxy is not a prelude"); + Assert.Equal(_startingHull, main.staged.Player.HullData.Name); + Assert.Equal(float3.zero, main.staged.Player.Position); + Assert.Empty(main.staged.Entities); + Assert.Contains(main.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false }); + } + + private static string Layout(Galaxy galaxy) => + string.Join(";", galaxy.Zones.Select(zone => $"{zone.Name}@{zone.Position}:{zone.Owner?.Name}")) + + $" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} noise {galaxy.Background.NoisePosition}"; + + private static string Layout(ZonePack arena) => + $"{arena.Radius} {arena.Mass} {arena.Planets.Count()} {arena.Orbits.Count()} {arena.Entities.Count()}"; + + // A fixed seed fixes the galaxy, its background and its arena, launch after launch in one session. Seed zero takes + // the clock, so two launches at two clock readings are two galaxies. + [Fact] + public void SeedRules() + { + var arena = new DjinniShakedown(); + Assert.NotEqual(0u, arena.Seed); + var first = Launch(arena); + var second = Launch(arena); + Assert.Equal(Layout(first.galaxy), Layout(second.galaxy)); + Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack)); + + var clock = new Queue(new[] { 1000u, 2000u }); + var inputs = Inputs(() => clock.Dequeue()); + var everyLaunchNew = new Scripted(true, _ => { }, seed: 0, generate: galaxy => galaxy.Main()); + var a = RunStart.Generate(everyLaunchNew, inputs); + Assert.Equal(1000u, inputs.Seed); + var b = RunStart.Generate(everyLaunchNew, inputs); + Assert.Equal(2000u, inputs.Seed); + Assert.NotEqual(Layout(a), Layout(b)); + } + + // A scenario naming one unknown design admits nothing, not even the parts that built, and names the design. + [Fact] + public void StagingIsAllOrNothing() + { + var failures = Refused(Arena(true), new Scripted(true, stage => + { + stage.Player(stage.Bare("Djinni"), float2(0, 0)); + stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); + stage.Place(stage.Fit("LonginusX", ("No Such Gun", int2(1, 8), ItemRotation.None)), float2(-300, 0)); + })); + Assert.Contains("No Such Gun", Assert.Single(failures)); + } + + // A stance holds both ways between an entity and the player. A piloted entity has exactly one agent; an unpiloted + // one has none, and nothing steers it. (It is not frozen: the zone's gravity still moves it, as it moves any ship, + // so the rule is observed on its steering inputs.) + [Fact] + public void StanceAndPilot() + { + var zone = Arena(true); + var staged = Stage(zone, new Scripted(true, stage => + { + stage.Player(stage.Bare("Djinni"), float2(0, 0)); + stage.Place(stage.Generated("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile, piloted: true); + stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Neutral, piloted: false); + })); + var player = staged.Player; + var hostile = staged.Entities[0]; + var neutral = staged.Entities[1]; + + Assert.True(player.IsHostileTo(hostile) && hostile.IsHostileTo(player), "hostile both ways"); + Assert.False(player.IsHostileTo(neutral) || neutral.IsHostileTo(player), "neutral both ways"); + // Admission activates: the live hostility each side tracks (what targeting and grudges read) agrees. + Assert.True(player.EntityHostility[hostile] && hostile.EntityHostility[player]); + Assert.False(player.EntityHostility[neutral] || neutral.EntityHostility[player]); + + Assert.Equal(1, AgentsOf(zone, hostile)); + Assert.Equal(0, AgentsOf(zone, neutral)); + Assert.Equal(0, AgentsOf(zone, player)); + var look = neutral.LookDirection; + for (var tick = 0; tick < 50; tick++) + { + zone.Update(.1f); + Assert.Equal(float2.zero, ((Ship) neutral).MovementDirection); + Assert.Equal(look, neutral.LookDirection); + } + Assert.Equal(0, AgentsOf(zone, neutral)); + } + + // A stance lasts the sitting: the player docking and undocking leaves it standing both ways. It goes with an entity + // gone for good: once an entity is destroyed, the player holds no stance toward it. + [Fact] + public void AStanceSurvivesDockingButNotDestruction() + { + var zone = Arena(true); + var staged = Stage(zone, new Scripted(true, stage => + { + stage.Player(stage.Bare("Djinni"), float2(0, 0)); + stage.Place(stage.Bare("LonginusX"), float2(300, 0), stance: ScenarioStance.Hostile); + stage.Place(stage.Bare("LonginusX"), float2(-300, 0), stance: ScenarioStance.Hostile); + })); + var player = staged.Player; + var hostile = staged.Entities[0]; + var doomed = staged.Entities[1]; + var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); + + Assert.NotNull(station.TryDock(player)); + SafeAssert.NotIn(zone, player, "a docked ship is out of the zone"); + Assert.True(station.TryUndock(player)); + Assert.True(player.IsHostileTo(hostile) && hostile.IsHostileTo(player), "the stance was lost over docking"); + Assert.True(player.EntityHostility[hostile] && hostile.EntityHostility[player], "the live hostility was lost over docking"); + + // Bare fits carry no faction, so without its stance the derived rule is not hostile + Assert.True(player.Faction == null && doomed.Faction == null, "the fixture's ships are factionless"); + Assert.True(player.IsHostileTo(doomed)); + doomed.Hull.Durability = 0; + doomed.HullDamage.OnNext(1); + SafeAssert.NotIn(zone, doomed, "a destroyed entity leaves the zone"); + Assert.False(player.IsHostileTo(doomed), "the stance toward a destroyed entity outlived it"); + } + + // Undocking admits the ship unpiloted: back in the zone, active (its live hostility is tracked), with no agent. + [Fact] + public void UndockingAdmitsWithNoAgent() + { + var zone = Arena(true); + var player = Stage(zone, new TutorialGalaxy()).Player; + var station = zone.Entities.First(entity => entity is OrbitalEntity && entity.DockingBays.Count > 0); + var agents = zone.Agents.Count; + + Assert.NotNull(station.TryDock(player)); + SafeAssert.NotIn(zone, player, "a docked ship is out of the zone"); + Assert.True(station.TryUndock(player)); + SafeAssert.In(zone, player, "an undocked ship is back in the zone"); + Assert.Equal(agents, zone.Agents.Count); + Assert.Equal(0, AgentsOf(zone, player)); + Assert.True(player.EntityHostility.ContainsKey(station), "an undocked ship is active"); + } + + // Ambient false keeps the stations and drops every generated ship and turret; ambient keeps them. The arena is the + // galaxy's entrance pack. The entrance holds a story station, placed as the narrative would place one, so a story + // station's own turrets are dropped too. + [Fact] + public void AQuietArenaHasNoGeneratedShipOrTurretButKeepsItsStations() + { + (int ships, int turrets, int stations) Census(ZonePack pack) => ( + pack.Entities.OfType().Count(), + pack.Entities.OfType().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Turret), + pack.Entities.OfType().Count(e => ((HullData) _cache.Get(e.Hull.Data)).HullType == HullType.Station)); + + _galaxy.Entrance.Locations.Add(new LocationStory + { + Zone = _galaxy.Entrance, Name = "story", Type = LocationType.Station, Faction = _protagonist, Turrets = 2 + }); + var ambient = Census(RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(true, _ => { }))); + var quietPack = RunStart.GenerateArena(_items, _zoneSettings, _galaxy, new Scripted(false, _ => { })); + var quiet = Census(quietPack); + + Assert.Same(quietPack, _galaxy.Entrance.PackedContents); + Assert.True(ambient.ships > 0 && ambient.turrets > 0, $"an ambient arena generated {ambient}"); + Assert.Equal(0, quiet.ships); + Assert.Equal(0, quiet.turrets); + Assert.True(quiet.stations > 0); + Assert.Equal(ambient.stations, quiet.stations); + Assert.Contains(quietPack.Entities.OfType(), station => station.Story == 0); + } + + // A scenario's player is its fit (here a catalog preset), at the scripted place and heading, carrying its cargo. + [Fact] + public void AScenarioPlayerIsItsFitAtItsPlaceWithItsCargo() + { + var generator = new LoadoutGenerator(ref _items.Random, _items, null, null, null, 2); + var preset = Loadouts.Capture(_items, (Ship) EntitySerializer.Unpack(_items, null, generator.GenerateShipLoadout(hull => hull.Name == "Djinni")), "test djinni"); + _cache.Upsert(preset); + // Two one-cell designs a manufacturer makes, by name, so the cargo is ordinary branded stock + var cargo = _cache.GetAll() + .Where(design => design.Shape.Coordinates.Length == 1) + .Where(design => _cache.GetAll().Any(p => p.Design.Key.Equals(_cache.RefOf(design).Key))) + .OrderBy(design => design.Name, StringComparer.Ordinal) + .Take(2) + .ToList(); + Assert.Equal(2, cargo.Count); + + var zone = Arena(true); + var player = Stage(zone, new Scripted(true, stage => + stage.Cargo(stage.Player(stage.Preset("test djinni"), float2(120, -40), float2(3, 4)), cargo.Select(design => design.Name).ToArray()))).Player; + + Assert.Equal("Djinni", player.HullData.Name); + SafeAssert.In(zone, player, "the player is admitted"); + Assert.Equal(float3(120, 0, -40), player.Position); + Assert.True(length(player.Direction - float2(.6f, .8f)) < 1e-5f, $"heading {player.Direction}"); + Assert.Equal( + preset.Slots.Select(slot => (slot.Position, slot.Design.Key)).OrderBy(s => s.ToString()), + player.Equipment.Concat(player.CargoBays).Concat(player.DockingBays) + .Where(item => item != player.EquippedHull) + .Select(item => (item.Position, _cache.RefOf(_items.GetData(item.EquippableItem)).Key)) + .OrderBy(s => s.ToString())); + Assert.Equal(cargo.Select(design => _cache.RefOf(design).Key.Value).OrderBy(k => k), + player.CargoBays.First().Cargo.Keys.Select(item => item.Data.Key.Value).OrderBy(k => k)); + } + + // A turret hull stages as an orbital entity with no orbit, which stays where it is put. + [Fact] + public void ATurretStagesAsAStationaryOrbitalEntity() + { + var zone = Arena(true); + var staged = Stage(zone, new Scripted(true, stage => + { + stage.Player(stage.Bare("Djinni"), float2(0, 0)); + stage.Place(stage.Bare(TurretHull().Name), float2(200, 50), stance: ScenarioStance.Hostile); + })); + var turret = Assert.IsType(staged.Entities[0]); + Assert.False(turret.OrbitData.IsSet()); + SafeAssert.In(zone, turret, "the turret is admitted"); + for (var tick = 0; tick < 20; tick++) zone.Update(.1f); + Assert.Equal(float2(200, 50), turret.Position.xz); + } + + // A run has exactly one player, and it is a ship; a station is never staged, and a turret is never piloted. Each + // is a staging failure, named, with nothing admitted. + [Fact] + public void AMisplacedHullOrPlayerIsAStagingFailure() + { + var station = _cache.GetAll().First(hull => hull.HullType == HullType.Station).Name; + var turret = TurretHull().Name; + foreach (var (script, expected) in new (Action, string)[] + { + (stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Place(stage.Bare(station), float2(200, 0)); }, $"{station} is a station hull"), + (stage => stage.Player(stage.Bare(turret), float2(0, 0)), $"player: {turret} is not a ship hull"), + (stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Place(stage.Bare(turret), float2(200, 0), piloted: true); }, "cannot be piloted"), + (stage => stage.Place(stage.Bare("Djinni"), float2(200, 0)), "places no player"), + (stage => { stage.Player(stage.Bare("Djinni"), float2(0, 0)); stage.Player(stage.Bare("Djinni"), float2(9, 0)); }, "a second player"), + }) + { + var failures = Refused(Arena(true), new Scripted(true, script)); + Assert.Contains(failures, failure => failure.Contains(expected)); + } + } + + // Arcs' geometry: the bow hull sits inside every one of the player's mount arcs, the stern hull outside all of them, + // and the turret is hostile. + [Fact] + public void ArcsGeometry() + { + var staged = Stage(Arena(false), new Arcs()); + var player = staged.Player; + var bow = staged.Entities[0]; + var stern = staged.Entities[1]; + var turret = Assert.IsType(staged.Entities[2]); + Assert.Equal((Arcs.Bow, Arcs.Stern, Arcs.Turret), (bow.Position.xz, stern.Position.xz, turret.Position.xz)); + + var mounts = player.Equipment.Where(item => item.Behaviors.OfType().Any()).ToList(); + Assert.NotEmpty(mounts); + foreach (var mount in mounts) + { + Assert.True(FireControl.InArc(mount, bow.Position - player.Position), $"{mount.Data.Name} cannot bear on the bow hull"); + Assert.False(FireControl.InArc(mount, stern.Position - player.Position), $"{mount.Data.Name} bears on the stern hull"); + } + Assert.True(player.IsHostileTo(turret) && turret.IsHostileTo(player), "the turret is hostile"); + Assert.False(player.IsHostileTo(bow) || player.IsHostileTo(stern), "the bare hulls are neutral"); + } + + // Q3 (operator, 2026-09-30): a design no product sells is a scenario test design and nothing else. A test design + // is one some scenario's staged entities carry, fitted or in cargo. These are the unsold designs none carries. + private List UnsoldAndUnplaced(IEnumerable runs) + { + var sold = _cache.GetAll().Select(product => product.Design.Key).ToHashSet(); + var placed = new HashSet(); + foreach (var entity in runs.SelectMany(run => run.Entities.Prepend(run.Player))) + { + foreach (var item in entity.Equipment.Concat(entity.CargoBays).Concat(entity.DockingBays)) + placed.Add(_cache.RefOf(_items.GetData(item.EquippableItem)).Key); + foreach (var item in entity.CargoBays.SelectMany(bay => bay.Cargo.Keys)) placed.Add(item.Data.Key); + } + return _cache.GetAll() + .Where(design => !sold.Contains(_cache.RefOf(design).Key) && !placed.Contains(_cache.RefOf(design).Key)) + .Select(design => design.Name).OrderBy(name => name, StringComparer.Ordinal).ToList(); + } + + // The shipped catalog holds no unsold design that no scenario carries (today, none unsold at all). A new unsold + // design is flagged until a scenario fits it or carries it as cargo. + [Fact] + public void EveryUnsoldDesignIsAScenarioTestDesign() + { + var runs = EveryScenario.Select(scenario => Launch(scenario, Inputs(() => GalaxySeed)).staged).ToList(); + Assert.Empty(UnsoldAndUnplaced(runs)); + + void Probe(string name) + { + var shape = new Shape(1, 1); + shape[int2(0, 0)] = true; + _cache.Upsert(new GearData { Name = name, Hardpoint = HardpointType.Tool, Shape = shape, Durability = 1 }); + } + Probe("Census Fitted"); + Probe("Census Carried"); + Assert.Equal(new[] { "Census Carried", "Census Fitted" }, UnsoldAndUnplaced(runs)); + + runs.Add(Stage(Arena(false), new Scripted(false, stage => + { + var ship = stage.Player(stage.Fit("Djinni", ("Census Fitted", int2(1, 6), ItemRotation.None), ("Store-All Plus", int2(3, 6), ItemRotation.None)), float2(0, 0)); + stage.Cargo(ship, "Census Carried"); + }))); + Assert.Empty(UnsoldAndUnplaced(runs)); + } + // Whether a run is the prelude is the galaxy's fact (Galaxy.IsPrelude); a save records it with no second owner. [Fact] public void SaveReadsPrelude() { - var arena = Arena(null); + var arena = Arena(true); var (prelude, _) = RunSave.Capture(_cache, _galaxy, arena, null, new SavedActionBarBinding[0]); Assert.True(prelude.IsTutorial); - var (main, _) = RunSave.Capture(_cache, MainGalaxy(), arena, null, new SavedActionBarBinding[0]); + var (main, _) = RunSave.Capture(_cache, MainSectorGalaxy(), arena, null, new SavedActionBarBinding[0]); Assert.False(main.IsTutorial); } - // PlayerSettings Key 2 (TutorialPassed) is retired: a player.cc written with it still loads, every other field intact. + // PlayerSettings Key 2, the never-written tutorial flag, is retired: a player.cc written with it still loads, every other field intact. [Fact] public void OldPlayerSettingsLoad() { @@ -34,7 +448,7 @@ public sealed partial class RunStartTests writer.WriteArrayHeader(7); writer.Write("Pilot"); writer.WriteNil(); // Key 1, retired before this cut - writer.Write(true); // Key 2, TutorialPassed + writer.Write(true); // Key 2, the retired tutorial flag MessagePackSerializer.Serialize(ref writer, new Dictionary { ["story"] = "hash" }, options); MessagePackSerializer.Serialize(ref writer, new PlayerGameplaySettings { TemperatureUnit = TemperatureUnit.Kelvin, SignificantDigits = 5 }, options); MessagePackSerializer.Serialize(ref writer, new PlayerInputSettings { ActionBarInputs = { "/1" } }, options); From c0be2843ea335b18dfc94332d3f38fe5ca2c8bf8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:45:26 +0200 Subject: [PATCH 04/12] A galaxy's zone names are unique within the galaxy, not across the process MarkovNameGenerator kept every name it ever gave in a static list, so each later galaxy in a session drew different zone names at the same seed, and ZoneGenerator seeds a zone from its name: a fixed-seed scenario relaunched in one session got a different arena. Galaxy now passes one used-name set to its factions' generators. The unused Reset goes. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Galaxy.cs | 3 ++- Assets/Scripts/ServerShared/MarkovNameGenerator.cs | 14 ++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index 89fa80794..7291a0c38 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -333,12 +333,13 @@ public class Galaxy ref Random random, Action progressCallback = null) { + var used = new HashSet(); for (var i = 0; i < Factions.Length; i++) { progressCallback?.Invoke($"Feeding Markov Chains: {i + 1} / {Factions.Length}"); //if(progressCallback!=null) Thread.Sleep(250); // Inserting Delay to make it seem like it's doing more work lmao var faction = Factions[i]; - _nameGenerators[faction] = new MarkovNameGenerator(ref random, cache.Get(faction.GeonameFile).Names, nameGeneratorSettings); + _nameGenerators[faction] = new MarkovNameGenerator(ref random, cache.Get(faction.GeonameFile).Names, nameGeneratorSettings, used); } // Generate zone name using the owner's name generator, otherwise assign catalogue ID diff --git a/Assets/Scripts/ServerShared/MarkovNameGenerator.cs b/Assets/Scripts/ServerShared/MarkovNameGenerator.cs index cf58a7b91..528684f40 100644 --- a/Assets/Scripts/ServerShared/MarkovNameGenerator.cs +++ b/Assets/Scripts/ServerShared/MarkovNameGenerator.cs @@ -14,14 +14,17 @@ public class MarkovNameGenerator { private Dictionary> _chains = new Dictionary>(); private List _samples = new List(); - private static List _used = new List(); + // The names already given, shared by every generator that must not repeat another's: a galaxy passes one set to + // all its factions' generators. Never process-wide, so one galaxy's names cannot change the next galaxy's. + private readonly HashSet _used; private int _order; private int _minLength; private int _maxLength; private Random _random; - public MarkovNameGenerator(ref Random random, IEnumerable sampleNames, NameGeneratorSettings settings) + public MarkovNameGenerator(ref Random random, IEnumerable sampleNames, NameGeneratorSettings settings, HashSet used) { + _used = used; var names = new HashSet(); foreach (var line in sampleNames) { @@ -41,6 +44,7 @@ public class MarkovNameGenerator public MarkovNameGenerator(ref Random random, IEnumerable sampleNames, int order, int minLength, int maxLength) { + _used = new HashSet(); _random = random; _order = max(1, order); @@ -113,12 +117,6 @@ public class MarkovNameGenerator } } - //Reset the used names - public void Reset() - { - _used.Clear(); - } - //Get a random letter from the chain private char GetLetter(string token) { From 0854d91d77b7c646b8030535bf92b0b1fb1b845a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:45:31 +0200 Subject: [PATCH 05/12] Arcs runs at seed 107: seed 103's prelude galaxy fails generation (a pre-existing KeyNotFound in Galaxy's home-zone placement) Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index d1ceb61e0..cd7eec885 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -6,7 +6,7 @@ public sealed class Arcs : Scenario public override string Name => "Arcs"; public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the bow " + "hull can be hit, nothing fires at the stern one through the ship, the turret tracks you all the way round."; - public override uint Seed => 103; + public override uint Seed => 107; public override bool Ambient => false; // Where the targets sit: the player is at the origin facing +z, and every mount fires forward. From 494d7955507aba8642c303988954832483d0d78c Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 20:55:02 +0200 Subject: [PATCH 06/12] Pin that generating a galaxy leaves the catalog's faction influence as authored Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index c2a4e36b3..cebf564d6 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -161,6 +161,16 @@ public sealed partial class RunStartTests Assert.NotEqual(Layout(a), Layout(b)); } + // Generating a galaxy writes nothing a later one reads: a prelude leaves the catalog's faction influence as authored. + [Fact] + public void AGalaxyLeavesTheCatalogsFactionsAsAuthored() + { + var authored = _cache.GetAll().ToDictionary(faction => faction, faction => faction.InfluenceDistance); + RunStart.Generate(new TutorialGalaxy(), Inputs()); + RunStart.Generate(new MainGalaxy(), Inputs()); + Assert.All(authored, pair => Assert.Equal(pair.Value, pair.Key.InfluenceDistance)); + } + // A scenario naming one unknown design admits nothing, not even the parts that built, and names the design. [Fact] public void StagingIsAllOrNothing() From e56af28892cd81b695c75eb3b005bed665126543 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 21:59:09 +0200 Subject: [PATCH 07/12] Galaxy links zones along a double-precision Delaunay triangulation: the vendored float MIConvexHull dropped zones, so ~2% of galaxies threw The vendored MIConvexHull (NIH, ported from double to float) returned triangulations that left whole sets of interior zones with no cell at all (prelude seed 103: 22 of 64 zones; main seed 4247651650: 182 of 196). Those zones had no link, Distance lacked them, and the constructor threw KeyNotFoundException placing the antagonist home (prelude) or the entrance (main). Galaxy was its only consumer; Delaunay.Edges (Bowyer-Watson, double) replaces it, and the 3,896 vendored lines go. The three RestoredHullsTests loops that skipped seeds whose galaxy threw now require every seed to build, and the stale comment that Galaxy mutates Faction.InfluenceDistance goes with them. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Delaunay.cs | 97 +++ Assets/Scripts/ServerShared/Delaunay.cs.meta | 2 + Assets/Scripts/ServerShared/Galaxy.cs | 21 +- .../ServerShared/NIH/MIConvexHull.meta | 8 - .../ServerShared/NIH/MIConvexHull/Cell2.cs | 114 --- .../NIH/MIConvexHull/Cell2.cs.meta | 11 - .../NIH/MIConvexHull/Cell2Object.cs | 114 --- .../NIH/MIConvexHull/Cell2Object.cs.meta | 3 - .../NIH/MIConvexHull/Constants.cs | 46 -- .../NIH/MIConvexHull/Constants.cs.meta | 11 - .../NIH/MIConvexHull/ConvexHull.cs | 135 ---- .../NIH/MIConvexHull/ConvexHull.cs.meta | 11 - .../NIH/MIConvexHull/ConvexHull.meta | 8 - .../MIConvexHull/ConvexHull/Collections.cs | 304 -------- .../ConvexHull/Collections.cs.meta | 11 - .../NIH/MIConvexHull/ConvexHull/ConvexFace.cs | 69 -- .../ConvexHull/ConvexFace.cs.meta | 11 - .../ConvexHullAlgorithm.Initialize.cs | 721 ------------------ .../ConvexHullAlgorithm.Initialize.cs.meta | 11 - .../ConvexHullAlgorithm.MainLoop.cs | 635 --------------- .../ConvexHullAlgorithm.MainLoop.cs.meta | 11 - .../NIH/MIConvexHull/ConvexHull/DataModel.cs | 237 ------ .../MIConvexHull/ConvexHull/DataModel.cs.meta | 11 - .../NIH/MIConvexHull/ConvexHull/MathHelper.cs | 480 ------------ .../ConvexHull/MathHelper.cs.meta | 11 - .../MIConvexHull/ConvexHull/ObjectManager.cs | 218 ------ .../ConvexHull/ObjectManager.cs.meta | 11 - .../ServerShared/NIH/MIConvexHull/Face3.cs | 8 - .../NIH/MIConvexHull/Face3.cs.meta | 11 - .../ServerShared/NIH/MIConvexHull/IVertex.cs | 53 -- .../NIH/MIConvexHull/IVertex.cs.meta | 11 - .../NIH/MIConvexHull/Triangulation.cs | 181 ----- .../NIH/MIConvexHull/Triangulation.cs.meta | 11 - .../NIH/MIConvexHull/Triangulation.meta | 8 - .../DelaunayTrianglationInternal.cs | 96 --- .../DelaunayTrianglationInternal.cs.meta | 11 - .../Triangulation/DelaunayTriangulation.cs | 75 -- .../DelaunayTriangulation.cs.meta | 11 - .../Triangulation/TriangulationCell.cs | 51 -- .../Triangulation/TriangulationCell.cs.meta | 11 - .../MIConvexHull/Triangulation/VoronoiEdge.cs | 93 --- .../Triangulation/VoronoiEdge.cs.meta | 11 - .../MIConvexHull/Triangulation/VoronoiMesh.cs | 203 ----- .../Triangulation/VoronoiMesh.cs.meta | 11 - .../ServerShared/NIH/MIConvexHull/Vertex2.cs | 34 - .../NIH/MIConvexHull/Vertex2.cs.meta | 11 - .../NIH/MIConvexHull/Vertex2Object.cs | 16 - .../NIH/MIConvexHull/Vertex2Object.cs.meta | 3 - .../ServerShared/NIH/MIConvexHull/Vertex3.cs | 30 - .../NIH/MIConvexHull/Vertex3.cs.meta | 11 - .../RestoredHullsTests.cs | 28 +- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 59 ++ 52 files changed, 165 insertions(+), 4205 deletions(-) create mode 100644 Assets/Scripts/ServerShared/Delaunay.cs create mode 100644 Assets/Scripts/ServerShared/Delaunay.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs.meta delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs delete mode 100644 Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs.meta diff --git a/Assets/Scripts/ServerShared/Delaunay.cs b/Assets/Scripts/ServerShared/Delaunay.cs new file mode 100644 index 000000000..b6a25bba6 --- /dev/null +++ b/Assets/Scripts/ServerShared/Delaunay.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using CultMath; + +// The Delaunay triangulation of a galaxy's zone positions, as the edges Galaxy links zones along. Bowyer-Watson +// (Bowyer 1981, Watson 1981) in double precision: each point is inserted by removing every triangle whose +// circumcircle holds it and fanning the cavity's boundary to it. Every point lands in the triangulation, so for two or +// more distinct points the edge graph is connected. +public static class Delaunay +{ + // Each edge once, as (lower index, higher index), in ascending order. + public static List<(int a, int b)> Edges(IReadOnlyList points) + { + var n = points.Count; + var x = new double[n + 3]; + var y = new double[n + 3]; + double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue; + for (var i = 0; i < n; i++) + { + x[i] = points[i].x; + y[i] = points[i].y; + minX = Math.Min(minX, x[i]); + minY = Math.Min(minY, y[i]); + maxX = Math.Max(maxX, x[i]); + maxY = Math.Max(maxY, y[i]); + } + + // A super-triangle far outside the points, so its vertices bend no edge between them; its triangles are + // dropped at the end. + var size = Math.Max(Math.Max(maxX - minX, maxY - minY), 1e-6) * 1000; + double midX = (minX + maxX) / 2, midY = (minY + maxY) / 2; + x[n] = midX - size; y[n] = midY - size; + x[n + 1] = midX + size; y[n + 1] = midY - size; + x[n + 2] = midX; y[n + 2] = midY + size; + + var triangles = new List { new Triangle(n, n + 1, n + 2, x, y) }; + var cavity = new Dictionary<(int, int), int>(); + for (var p = 0; p < n; p++) + { + cavity.Clear(); + for (var t = triangles.Count - 1; t >= 0; t--) + { + var triangle = triangles[t]; + var dx = x[p] - triangle.CenterX; + var dy = y[p] - triangle.CenterY; + if (dx * dx + dy * dy >= triangle.RadiusSquared) continue; + Count(cavity, triangle.A, triangle.B); + Count(cavity, triangle.B, triangle.C); + Count(cavity, triangle.C, triangle.A); + triangles[t] = triangles[triangles.Count - 1]; + triangles.RemoveAt(triangles.Count - 1); + } + + // The cavity's boundary is every edge only one removed triangle had. + foreach (var edge in cavity) + if (edge.Value == 1) + triangles.Add(new Triangle(edge.Key.Item1, edge.Key.Item2, p, x, y)); + } + + var edges = new SortedSet<(int a, int b)>(); + foreach (var triangle in triangles) + { + if (triangle.A >= n || triangle.B >= n || triangle.C >= n) continue; + edges.Add(Ordered(triangle.A, triangle.B)); + edges.Add(Ordered(triangle.B, triangle.C)); + edges.Add(Ordered(triangle.C, triangle.A)); + } + return new List<(int a, int b)>(edges); + } + + private static void Count(Dictionary<(int, int), int> cavity, int a, int b) + { + var key = Ordered(a, b); + cavity[key] = cavity.TryGetValue(key, out var seen) ? seen + 1 : 1; + } + + private static (int a, int b) Ordered(int a, int b) => a < b ? (a, b) : (b, a); + + private readonly struct Triangle + { + public readonly int A, B, C; + public readonly double CenterX, CenterY, RadiusSquared; + + public Triangle(int a, int b, int c, double[] x, double[] y) + { + A = a; + B = b; + C = c; + double ax = x[a], ay = y[a], bx = x[b], by = y[b], cx = x[c], cy = y[c]; + var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + double a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + CenterX = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d; + CenterY = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d; + RadiusSquared = (ax - CenterX) * (ax - CenterX) + (ay - CenterY) * (ay - CenterY); + } + } +} diff --git a/Assets/Scripts/ServerShared/Delaunay.cs.meta b/Assets/Scripts/ServerShared/Delaunay.cs.meta new file mode 100644 index 000000000..917f6e6b4 --- /dev/null +++ b/Assets/Scripts/ServerShared/Delaunay.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a05b77dab9114e19b222b79a8403fc21 diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index 7291a0c38..bcdf7a2f8 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -7,7 +7,6 @@ using System.Threading; using System.Threading.Tasks; using DataStructures.ViliWonka.Heap; using Ink.Runtime; -using MIConvexHull; using JM.LinqFaster; using UniRx; using CultMath; @@ -361,22 +360,10 @@ public class Galaxy progressCallback?.Invoke("Triangulating Zone Positions"); if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao - // Create a delaunay triangulation to connect adjacent sectors - var triangulation = DelaunayTriangulation, Cell2> - .Create(Zones.Select(z => new Vertex2(z.Position, z)).ToList(), 1e-7f); - var links = new HashSet<(GalaxyZone, GalaxyZone)>(); - foreach (var cell in triangulation.Cells) - { - if (!links.Contains((cell.Vertices[0].StoredObject, cell.Vertices[1].StoredObject)) && - !links.Contains((cell.Vertices[1].StoredObject, cell.Vertices[0].StoredObject))) - links.Add((cell.Vertices[0].StoredObject, cell.Vertices[1].StoredObject)); - if (!links.Contains((cell.Vertices[1].StoredObject, cell.Vertices[2].StoredObject)) && - !links.Contains((cell.Vertices[2].StoredObject, cell.Vertices[1].StoredObject))) - links.Add((cell.Vertices[1].StoredObject, cell.Vertices[2].StoredObject)); - if (!links.Contains((cell.Vertices[0].StoredObject, cell.Vertices[2].StoredObject)) && - !links.Contains((cell.Vertices[2].StoredObject, cell.Vertices[0].StoredObject))) - links.Add((cell.Vertices[0].StoredObject, cell.Vertices[2].StoredObject)); - } + // 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(); progressCallback?.Invoke("Eliminating Zone Links"); if (progressCallback != null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull.meta deleted file mode 100644 index 5f927b59d..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 4a109a0b0bb6e8f40935ca2ec55e5bd7 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs deleted file mode 100644 index 6e8a8d2f9..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections; -using System.Linq; -using MIConvexHull; -using CultMath; -using static CultMath.math; - -/// -/// A vertex is a simple class that stores the postion of a point, node or vertex. -/// -public class Cell2 : TriangulationCell -{ - float2? circumCenter, centroid; - - public float2 Circumcenter { - get { - circumCenter = circumCenter ?? GetCircumcenter(); - return circumCenter.Value; - } - } - - public float2 Centroid { - get { - centroid = centroid ?? GetCentroid(); - return centroid.Value; - } - } - - public Cell2 () - { - - } - - double LengthSquared (float[] v) - { - float norm = 0; - for (int i = 0; i < v.Length; i++) { - var t = v [i]; - norm += t * t; - } - return norm; - } - - float Determinant(float[,] m) - { - float fCofactor00 = m[1,1] * m[2,2] - m[1,2] * m[2,1]; - float fCofactor10 = m[1,2] * m[2,0] - m[1,0] * m[2,2]; - float fCofactor20 = m[1,0] * m[2,1] - m[1,1] * m[2,0]; - - float fDet = m[0,0] * fCofactor00 + m[0,1] * fCofactor10 + m[0,2] * fCofactor20; - - return fDet; - } - - float2 GetCircumcenter () - { - // From MathWorld: http://mathworld.wolfram.com/Circumcircle.html - - var points = Vertices; - - float[,] m = new float[3, 3]; - - // x, y, 1 - for (int i = 0; i < 3; i++) { - m[i, 0] = points[i].x; - m[i, 1] = points[i].y; - m[i, 2] = 1; - } - var a = Determinant(m); - - // size, y, 1 - for (int i = 0; i < 3; i++) { - m[i, 0] = lengthsq(points[i].StoredPosition); - } - var dx = -Determinant(m); - - // size, x, 1 - for (int i = 0; i < 3; i++) { - m[i, 1] = points[i].x; - } - var dy = Determinant(m); - - // size, x, y - //for (int i = 0; i < 3; i++) { - // m[i, 2] = points[i].y; - //} - //var c = -Det(m); - - var s = -1.0f / (2.0f * a); - //var r = System.Math.Abs(s) * System.Math.Sqrt(dx * dx + dy * dy - 4 * a * c); - - return new float2(s * dx, s * dy); - } - - float2 GetCentroid () - { - return new float2(Vertices.Select(v => v.Position[0]).Average(), Vertices.Select(v => v.Position[1]).Average()); - } - -} - - - - - - - - - - - - - - - diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs.meta deleted file mode 100644 index 52993549b..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: adf902e96e0c51049b65bc002d6ef4ca -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs deleted file mode 100644 index d23fbac51..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Collections; -using System.Linq; -using MIConvexHull; -using CultMath; -using static CultMath.math; - -/// -/// A vertex is a simple class that stores the postion of a point, node or vertex. -/// -public class Cell2 : TriangulationCell, Cell2> -{ - float2? circumCenter, centroid; - - public float2 Circumcenter { - get { - circumCenter = circumCenter ?? GetCircumcenter(); - return circumCenter.Value; - } - } - - public float2 Centroid { - get { - centroid = centroid ?? GetCentroid(); - return centroid.Value; - } - } - - public Cell2 () - { - - } - - double LengthSquared (float[] v) - { - float norm = 0; - for (int i = 0; i < v.Length; i++) { - var t = v [i]; - norm += t * t; - } - return norm; - } - - float Determinant(float[,] m) - { - float fCofactor00 = m[1,1] * m[2,2] - m[1,2] * m[2,1]; - float fCofactor10 = m[1,2] * m[2,0] - m[1,0] * m[2,2]; - float fCofactor20 = m[1,0] * m[2,1] - m[1,1] * m[2,0]; - - float fDet = m[0,0] * fCofactor00 + m[0,1] * fCofactor10 + m[0,2] * fCofactor20; - - return fDet; - } - - float2 GetCircumcenter () - { - // From MathWorld: http://mathworld.wolfram.com/Circumcircle.html - - var points = Vertices; - - float[,] m = new float[3, 3]; - - // x, y, 1 - for (int i = 0; i < 3; i++) { - m[i, 0] = points[i].x; - m[i, 1] = points[i].y; - m[i, 2] = 1; - } - var a = Determinant(m); - - // size, y, 1 - for (int i = 0; i < 3; i++) { - m[i, 0] = lengthsq(points[i].StoredPosition); - } - var dx = -Determinant(m); - - // size, x, 1 - for (int i = 0; i < 3; i++) { - m[i, 1] = points[i].x; - } - var dy = Determinant(m); - - // size, x, y - //for (int i = 0; i < 3; i++) { - // m[i, 2] = points[i].y; - //} - //var c = -Det(m); - - var s = -1.0f / (2.0f * a); - //var r = System.Math.Abs(s) * System.Math.Sqrt(dx * dx + dy * dy - 4 * a * c); - - return new float2(s * dx, s * dy); - } - - float2 GetCentroid () - { - return new float2(Vertices.Select(v => v.Position[0]).Average(), Vertices.Select(v => v.Position[1]).Average()); - } - -} - - - - - - - - - - - - - - - diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs.meta deleted file mode 100644 index d47180ee1..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Cell2Object.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: ea854ab9e5204f92828e92ffb1244964 -timeCreated: 1615420193 \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs deleted file mode 100644 index 20948bc1a..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs +++ /dev/null @@ -1,46 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; - -namespace MIConvexHull -{ - internal static class Constants - { - /// - /// The default plane distance tolerance - /// - internal const float DefaultPlaneDistanceTolerance = 1e-10f; - /// - /// The starting delta dot product in simplex - /// - internal const float StartingDeltaDotProductInSimplex = 0.5f; - /// - /// The connector table size - /// - internal const int ConnectorTableSize = 2017; - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs.meta deleted file mode 100644 index 3c5287e11..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Constants.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ee065f025d2f5374db2ceaa973b822d4 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs deleted file mode 100644 index 1ef83b8a9..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs +++ /dev/null @@ -1,135 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MIConvexHull -{ - /// - /// Factory class for computing convex hulls. - /// - public static class ConvexHull - { - /// - /// Creates a convex hull of the input data. - /// - /// The type of the t vertex. - /// The type of the t face. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// - /// ConvexHull<TVertex, TFace>. - /// - public static ConvexHull Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - where TFace : ConvexFace, new() - { - return ConvexHull.Create(data, PlaneDistanceTolerance); - } - - /// - /// Creates a convex hull of the input data. - /// - /// The type of the t vertex. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// - /// ConvexHull<TVertex, DefaultConvexFace<TVertex>>. - /// - public static ConvexHull> Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - { - return ConvexHull>.Create(data, PlaneDistanceTolerance); - } - - /// - /// Creates a convex hull of the input data. - /// - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// - /// ConvexHull<DefaultVertex, DefaultConvexFace<DefaultVertex>>. - /// - public static ConvexHull> Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - { - var points = data.Select(p => new DefaultVertex { Position = p }).ToList(); - return ConvexHull>.Create(points, PlaneDistanceTolerance); - } - } - - /// - /// Representation of a convex hull. - /// - /// The type of the t vertex. - /// The type of the t face. - public class ConvexHull - where TVertex : IVertex - where TFace : ConvexFace, new() - { - /// - /// Can only be created using a factory method. - /// - internal ConvexHull() - { - } - - /// - /// Points of the convex hull. - /// - /// The points. - public IEnumerable Points { get; internal set; } - - /// - /// Faces of the convex hull. - /// - /// The faces. - public IEnumerable Faces { get; internal set; } - - /// - /// Creates the convex hull. - /// - /// The data. - /// The plane distance tolerance. - /// - /// ConvexHull<TVertex, TFace>. - /// - /// The supplied data is null. - /// data - public static ConvexHull Create(IList data, float PlaneDistanceTolerance) - { - if (data == null) throw new ArgumentNullException("The supplied data is null."); - return ConvexHullAlgorithm.GetConvexHull(data, PlaneDistanceTolerance); - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs.meta deleted file mode 100644 index 0be10d4af..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 447289e858e19c14abbe91dfbd0e3089 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.meta deleted file mode 100644 index 6a291ff20..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: a6e88469e3b81a541a0bc8a66e60d0c7 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs deleted file mode 100644 index ace305648..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs +++ /dev/null @@ -1,304 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; - -namespace MIConvexHull -{ - /// - /// A more lightweight alternative to List of T. - /// On clear, only resets the count and does not clear the references - /// => this works because of the ObjectManager. - /// Includes a stack functionality. - /// - /// - internal class SimpleList - { - /// - /// The capacity - /// - private int capacity; - - /// - /// The count - /// - public int Count; - /// - /// The items - /// - private T[] items; - - /// - /// Get the i-th element. - /// - /// The i. - /// T. - public T this[int i] - { - get { return items[i]; } - set { items[i] = value; } - } - - /// - /// Size matters. - /// - private void EnsureCapacity() - { - if (capacity == 0) - { - capacity = 32; - items = new T[32]; - } - else - { - var newItems = new T[capacity*2]; - Array.Copy(items, newItems, capacity); - capacity = 2*capacity; - items = newItems; - } - } - - /// - /// Adds a vertex to the buffer. - /// - /// The item. - public void Add(T item) - { - if (Count + 1 > capacity) EnsureCapacity(); - items[Count++] = item; - } - - /// - /// Pushes the value to the back of the list. - /// - /// The item. - public void Push(T item) - { - if (Count + 1 > capacity) EnsureCapacity(); - items[Count++] = item; - } - - /// - /// Pops the last value from the list. - /// - /// T. - public T Pop() - { - return items[--Count]; - } - - /// - /// Sets the Count to 0, otherwise does nothing. - /// - public void Clear() - { - Count = 0; - } - } - - - /// - /// Class IndexBuffer. - /// A fancy name for a list of integers. - /// - internal class IndexBuffer : SimpleList - { - } - - /// - /// A priority based linked list. - /// - internal sealed class FaceList - { - /// - /// The last - /// - private ConvexFaceInternal last; - - /// - /// Get the first element. - /// - /// The first. - public ConvexFaceInternal First { get; private set; } - - /// - /// Adds the element to the beginning. - /// - /// The face. - private void AddFirst(ConvexFaceInternal face) - { - face.InList = true; - First.Previous = face; - face.Next = First; - First = face; - } - - /// - /// Adds a face to the list. - /// - /// The face. - public void Add(ConvexFaceInternal face) - { - if (face.InList) - { - if (First.VerticesBeyond.Count < face.VerticesBeyond.Count) - { - Remove(face); - AddFirst(face); - } - return; - } - - face.InList = true; - - if (First != null && First.VerticesBeyond.Count < face.VerticesBeyond.Count) - { - First.Previous = face; - face.Next = First; - First = face; - } - else - { - if (last != null) - { - last.Next = face; - } - face.Previous = last; - last = face; - if (First == null) - { - First = face; - } - } - } - - /// - /// Removes the element from the list. - /// - /// The face. - public void Remove(ConvexFaceInternal face) - { - if (!face.InList) return; - - face.InList = false; - - if (face.Previous != null) - { - face.Previous.Next = face.Next; - } - else if ( /*first == face*/ face.Previous == null) - { - First = face.Next; - } - - if (face.Next != null) - { - face.Next.Previous = face.Previous; - } - else if ( /*last == face*/ face.Next == null) - { - last = face.Previous; - } - - face.Next = null; - face.Previous = null; - } - } - - /// - /// Connector list. - /// - internal sealed class ConnectorList - { - /// - /// The last - /// - private FaceConnector last; - - /// - /// Get the first element. - /// - /// The first. - public FaceConnector First { get; private set; } - - /// - /// Adds the element to the beginning. - /// - /// The connector. - private void AddFirst(FaceConnector connector) - { - First.Previous = connector; - connector.Next = First; - First = connector; - } - - /// - /// Adds a face to the list. - /// - /// The element. - public void Add(FaceConnector element) - { - if (last != null) - { - last.Next = element; - } - element.Previous = last; - last = element; - if (First == null) - { - First = element; - } - } - - /// - /// Removes the element from the list. - /// - /// The connector. - public void Remove(FaceConnector connector) - { - if (connector.Previous != null) - { - connector.Previous.Next = connector.Next; - } - else if ( /*first == face*/ connector.Previous == null) - { - First = connector.Next; - } - - if (connector.Next != null) - { - connector.Next.Previous = connector.Previous; - } - else if ( /*last == face*/ connector.Next == null) - { - last = connector.Previous; - } - - connector.Next = null; - connector.Previous = null; - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs.meta deleted file mode 100644 index 323e260ef..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/Collections.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3969283304824fe4a8eca1eb522fd3a7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs deleted file mode 100644 index 34fab61b2..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs +++ /dev/null @@ -1,69 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -namespace MIConvexHull -{ - /// - /// A convex face representation containing adjacency information. - /// - /// The type of the t vertex. - /// The type of the t face. - public abstract class ConvexFace - where TVertex : IVertex - where TFace : ConvexFace - { - /// - /// Adjacency. Array of length "dimension". - /// If F = Adjacency[i] then the vertices shared with F are Vertices[j] where j != i. - /// In the context of triangulation, can be null (indicates the cell is at boundary). - /// - /// The adjacency. - public TFace[] Adjacency { get; set; } - - /// - /// The vertices stored in clockwise order for dimensions 2 - 4, in higher dimensions the order is arbitrary. - /// Unless I accidentally switch some index somewhere in which case the order is CCW. Either way, it is consistent. - /// 3D Normal = (V[1] - V[0]) x (V[2] - V[1]). - /// - /// The vertices. - public TVertex[] Vertices { get; set; } - - /// - /// The normal vector of the face. Null if used in triangulation. - /// - /// The normal. - public float[] Normal { get; set; } - } - - /// - /// A default convex face representation. - /// - /// The type of the t vertex. - public class DefaultConvexFace : ConvexFace> - where TVertex : IVertex - { - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs.meta deleted file mode 100644 index 40c26cd01..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexFace.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5db1a96860ec6d949930695ae9bedcc0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs deleted file mode 100644 index e4b8477ae..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs +++ /dev/null @@ -1,721 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MIConvexHull -{ - /* - * This part of the implementation handles initialization of the convex hull algorithm: - * - Constructor & Process initiation - * - Determine the dimension by looking at length of Position vector of 10 random data points from the input. - * - Identify bounding box points in each direction. - * - Pick (Dimension + 1) points from the extremes and construct the initial simplex. - */ - - /// - /// Class ConvexHullAlgorithm. - /// - internal partial class ConvexHullAlgorithm - { - #region Starting functions and constructor - /// - /// The main function for the Convex Hull algorithm. It is static, but it creates - /// an instantiation of this class in order to allow for parallel execution. - /// Following this simple function, the constructor and the main function "FindConvexHull" is listed. - /// - /// The type of the vertices in the data. - /// The desired type of the faces. - /// The data is the vertices as a collection of IVertices. - /// The plane distance tolerance. - /// - /// MIConvexHull.ConvexHull<TVertex, TFace>. - /// - internal static ConvexHull GetConvexHull(IList data, - float PlaneDistanceTolerance) - where TFace : ConvexFace, new() - where TVertex : IVertex - { - var ch = new ConvexHullAlgorithm(data.Cast().ToArray(), false, PlaneDistanceTolerance); - ch.GetConvexHull(); - - if (ch.NumOfDimensions == 2) return ch.Return2DResultInOrder(data); - return new ConvexHull - { - Points = ch.GetHullVertices(data), - Faces = ch.GetConvexFaces() - }; - } - - /// - /// Initializes a new instance of the class. - /// - /// The vertices. - /// if set to true [lift]. - /// The plane distance tolerance. - /// Dimension of the input must be 2 or greater. - /// There are too few vertices (m) for the n-dimensional space. (m must be greater + - /// than the n, but m is + NumberOfVertices + and n is + NumOfDimensions - /// PointTranslationGenerator cannot be null if PointTranslationType is enabled. - /// or - /// Dimension of the input must be 2 or greater. - /// There are too few vertices (m) for the n-dimensional space. (m must be greater " + - /// "than the n, but m is " + NumberOfVertices + " and n is " + Dimension - private ConvexHullAlgorithm(IVertex[] vertices, bool lift, float PlaneDistanceTolerance) - { - IsLifted = lift; - Vertices = vertices; - NumberOfVertices = vertices.Length; - - NumOfDimensions = DetermineDimension(); - if (IsLifted) NumOfDimensions++; - if (NumOfDimensions < 2) throw new InvalidOperationException("Dimension of the input must be 2 or greater."); - if (NumberOfVertices <= NumOfDimensions) - throw new ArgumentException( - "There are too few vertices (m) for the n-dimensional space. (m must be greater " + - "than the n, but m is " + NumberOfVertices + " and n is " + NumOfDimensions); - this.PlaneDistanceTolerance = PlaneDistanceTolerance; - UnprocessedFaces = new FaceList(); - ConvexFaces = new IndexBuffer(); - - FacePool = new ConvexFaceInternal[(NumOfDimensions + 1) * 10]; // must be initialized before object manager - AffectedFaceFlags = new bool[(NumOfDimensions + 1) * 10]; - ObjectManager = new ObjectManager(this); - - Center = new float[NumOfDimensions]; - TraverseStack = new IndexBuffer(); - UpdateBuffer = new int[NumOfDimensions]; - UpdateIndices = new int[NumOfDimensions]; - EmptyBuffer = new IndexBuffer(); - AffectedFaceBuffer = new IndexBuffer(); - ConeFaceBuffer = new SimpleList(); - SingularVertices = new HashSet(); - BeyondBuffer = new IndexBuffer(); - - ConnectorTable = new ConnectorList[Constants.ConnectorTableSize]; - for (var i = 0; i < Constants.ConnectorTableSize; i++) ConnectorTable[i] = new ConnectorList(); - - VertexVisited = new bool[NumberOfVertices]; - Positions = new float[NumberOfVertices * NumOfDimensions]; - boundingBoxPoints = new List[NumOfDimensions]; - minima = new float[NumOfDimensions]; - maxima = new float[NumOfDimensions]; - mathHelper = new MathHelper(NumOfDimensions, Positions); - } - /// - /// Check the dimensionality of the input data. - /// - /// System.Int32. - /// Invalid input data (non-uniform dimension). - private int DetermineDimension() - { - var r = new Random(); - var dimensions = new List(); - for (var i = 0; i < 10; i++) - dimensions.Add(Vertices[r.Next(NumberOfVertices)].Position.Length); - var dimension = dimensions.Min(); - if (dimension != dimensions.Max()) - throw new ArgumentException("Invalid input data (non-uniform dimension)."); - return dimension; - } - - - /// - /// Gets/calculates the convex hull. This is - /// - private void GetConvexHull() - { - // accessing a 1D array is quicker than a jagged array, so the first step is to make this array - SerializeVerticesToPositions(); - // next the bounding box extremes are found. This is used to shift, scale and find the starting simplex. - FindBoundingBoxPoints(); - // the positions are shifted to avoid divide by zero problems - // and if Delaunay or Voronoi, then the parabola terms are scaled back to match the size of the other coords - ShiftAndScalePositions(); - // Find the (dimension+1) initial points and create the simplexes. - CreateInitialSimplex(); - - // Now, the main loop. These initial faces of a simplex are replaced and expanded - // outwards to make the convex hull and faces. - while (UnprocessedFaces.First != null) - { - var currentFace = UnprocessedFaces.First; - CurrentVertex = currentFace.FurthestVertex; - - UpdateCenter(); - - // The affected faces get tagged - TagAffectedFaces(currentFace); - - // Create the cone from the currentVertex and the affected faces horizon. - if (!SingularVertices.Contains(CurrentVertex) && CreateCone()) CommitCone(); - else HandleSingular(); - - // Need to reset the tags - var count = AffectedFaceBuffer.Count; - for (var i = 0; i < count; i++) AffectedFaceFlags[AffectedFaceBuffer[i]] = false; - } - } - - #endregion - - /// - /// Serializes the vertices into the 1D array, Positions. The 1D array has much quicker access in C#. - /// - private void SerializeVerticesToPositions() - { - var index = 0; - if (IsLifted) // "Lifted" means that the last dimension is the sum of the squares of the others. - { - foreach (var v in Vertices) - { - var parabolaTerm = 0.0f; // the lifted term is a sum of squares. - var origNumDim = NumOfDimensions - 1; - for (var i = 0; i < origNumDim; i++) - { - var coordinate = v.Position[i]; - Positions[index++] = coordinate; - parabolaTerm += coordinate * coordinate; - } - Positions[index++] = parabolaTerm; - } - } - else - foreach (var v in Vertices) - { - for (var i = 0; i < NumOfDimensions; i++) - Positions[index++] = v.Position[i]; - } - } - - - - /// - /// Finds the bounding box points. - /// - private void FindBoundingBoxPoints() - { - indexOfDimensionWithLeastExtremes = -1; - var minNumExtremes = int.MaxValue; - for (var i = 0; i < NumOfDimensions; i++) - { - var minIndices = new List(); - var maxIndices = new List(); - float min = float.PositiveInfinity, max = float.NegativeInfinity; - for (var j = 0; j < NumberOfVertices; j++) - { - var v = GetCoordinate(j, i); - var difference = min - v; - if (difference >= PlaneDistanceTolerance) - { - // you found a better solution than before, clear out the list and store new value - min = v; - minIndices.Clear(); - minIndices.Add(j); - } - else if (difference > 0) - { - // you found a solution slightly better than before, clear out those that are no longer on the list and store new value - min = v; - minIndices.RemoveAll(index => min - GetCoordinate(index, i) > PlaneDistanceTolerance); - minIndices.Add(j); - } - else if (difference > -PlaneDistanceTolerance) - { - //same or almost as good as current limit, so store it - minIndices.Add(j); - } - difference = v - max; - if (difference >= PlaneDistanceTolerance) - { - // you found a better solution than before, clear out the list and store new value - max = v; - maxIndices.Clear(); - maxIndices.Add(j); - } - else if (difference > 0) - { - // you found a solution slightly better than before, clear out those that are no longer on the list and store new value - max = v; - maxIndices.RemoveAll(index => min - GetCoordinate(index, i) > PlaneDistanceTolerance); - maxIndices.Add(j); - } - else if (difference > -PlaneDistanceTolerance) - { - //same or almost as good as current limit, so store it - maxIndices.Add(j); - } - } - minima[i] = min; - maxima[i] = max; - minIndices.AddRange(maxIndices); - if (minIndices.Count < minNumExtremes) - { - minNumExtremes = minIndices.Count; - indexOfDimensionWithLeastExtremes = i; - } - boundingBoxPoints[i] = minIndices; - } - } - - /// - /// Shifts and scales the Positions to avoid future errors. This does not alter the original data. - /// - private void ShiftAndScalePositions() - { - var positionsLength = Positions.Length; - if (IsLifted) - { - var origNumDim = NumOfDimensions - 1; - var parabolaScale = 2 / (minima.Sum(x => Math.Abs(x)) + maxima.Sum(x => Math.Abs(x)) - - Math.Abs(maxima[origNumDim]) - Math.Abs(minima[origNumDim])); - // the parabola scale is 1 / average of the sum of the other dimensions. - // multiplying this by the parabola will scale it back to be on near similar size to the - // other dimensions. Without this, the term is much larger than the others, which causes - // problems for roundoff error and finding the normal of faces. - minima[origNumDim] *= parabolaScale; // change the extreme values as well - maxima[origNumDim] *= parabolaScale; - // it is done here because - for (int i = origNumDim; i < positionsLength; i += NumOfDimensions) - Positions[i] *= parabolaScale; - } - var shiftAmount = new float[NumOfDimensions]; - for (int i = 0; i < NumOfDimensions; i++) - // now the entire model is shifted to all positive numbers...plus some more. - // why? - // 1) to avoid dealing with a point at the origin {0,0,...,0} which causes problems - // for future normal finding - // 2) note that weird shift that is used (max - min - min). This is to avoid scaling - // issues. this shift means that the minima in a dimension will always be a positive - // number (no points at zero), and the minima [in a given dimension] will always be - // half of the maxima. 'Half' is much preferred to 'thousands of times' - // Think of the first term as the range (max - min), then the second term avoids cases - // where there are both positive and negative numbers. - if (maxima[i] == minima[i]) shiftAmount[i] = 0.0f; - else shiftAmount[i] = (maxima[i] - minima[i]) - minima[i]; - for (int i = 0; i < positionsLength; i++) - Positions[i] += shiftAmount[i % NumOfDimensions]; - } - - /// - /// Find the (dimension+1) initial points and create the simplexes. - /// Creates the initial simplex of n+1 vertices by using points from the bounding box. - /// Special care is taken to ensure that the vertices chosen do not result in a degenerate shape - /// where vertices are collinear (co-planar, etc). This would technically be resolved when additional - /// vertices are checked in the main loop, but: 1) a degenerate simplex would not eliminate any other - /// vertices (thus no savings there), 2) the creation of the face normal is prone to error. - /// - private void CreateInitialSimplex() - { - var initialPoints = FindInitialPoints(); - #region Create the first faces from (dimension + 1) vertices. - - var faces = new int[NumOfDimensions + 1]; - - for (var i = 0; i < NumOfDimensions + 1; i++) - { - var vertices = new int[NumOfDimensions]; - for (int j = 0, k = 0; j <= NumOfDimensions; j++) - { - if (i != j) vertices[k++] = initialPoints[j]; - } - var newFace = FacePool[ObjectManager.GetFace()]; - newFace.Vertices = vertices; - Array.Sort(vertices); - mathHelper.CalculateFacePlane(newFace, Center); - faces[i] = newFace.Index; - } - // update the adjacency (check all pairs of faces) - for (var i = 0; i < NumOfDimensions; i++) - for (var j = i + 1; j < NumOfDimensions + 1; j++) UpdateAdjacency(FacePool[faces[i]], FacePool[faces[j]]); - - #endregion - - #region Init the vertex beyond buffers. - - foreach (var faceIndex in faces) - { - var face = FacePool[faceIndex]; - FindBeyondVertices(face); - if (face.VerticesBeyond.Count == 0) ConvexFaces.Add(face.Index); // The face is on the hull - else UnprocessedFaces.Add(face); - } - - #endregion - - // Set all vertices to false (unvisited). - foreach (var vertex in initialPoints) VertexVisited[vertex] = false; - } - - /// - /// Finds (dimension + 1) initial points. - /// - /// List<System.Int32>. - /// The input data is degenerate. It appears to exist in " + NumOfDimensions + - /// " dimensions, but it is a " + (NumOfDimensions - 1) + " dimensional set (i.e. the point of collinear," - /// + " coplanar, or co-hyperplanar.) - private List FindInitialPoints() - { - var bigNumber = maxima.Sum() * NumOfDimensions * NumberOfVertices; - // the first two points are taken from the dimension that had the fewest extremes - // well, in most cases there will only be 2 in all dimensions: one min and one max - // but a lot of engineering part shapes are nice and square and can have hundreds of - // parallel vertices at the extremes - var vertex1 = boundingBoxPoints[indexOfDimensionWithLeastExtremes].First(); // these are min and max vertices along - var vertex2 = boundingBoxPoints[indexOfDimensionWithLeastExtremes].Last(); // the dimension that had the fewest points - boundingBoxPoints[indexOfDimensionWithLeastExtremes].RemoveAt(0); - boundingBoxPoints[indexOfDimensionWithLeastExtremes].RemoveAt(boundingBoxPoints[indexOfDimensionWithLeastExtremes].Count - 1); - var initialPoints = new List { vertex1, vertex2 }; - VertexVisited[vertex1] = VertexVisited[vertex2] = true; - CurrentVertex = vertex1; UpdateCenter(); - CurrentVertex = vertex2; UpdateCenter(); - var edgeVectors = new float[NumOfDimensions][]; - edgeVectors[0] = mathHelper.VectorBetweenVertices(vertex2, vertex1); - // now the remaining vertices are just combined in one big list - var extremes = boundingBoxPoints.SelectMany(x => x).ToList(); - // otherwise find the remaining points by maximizing the initial simplex volume - var index = 1; - while (index < NumOfDimensions && extremes.Any()) - { - var bestVertex = -1; - var bestEdgeVector = new float[] { }; - var maxVolume = Constants.DefaultPlaneDistanceTolerance; - for (var i = extremes.Count - 1; i >= 0; i--) - { - // count backwards in order to remove potential duplicates - var vIndex = extremes[i]; - if (initialPoints.Contains(vIndex)) extremes.RemoveAt(i); - else - { - edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1); - var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber); - if (maxVolume < volume) - { - maxVolume = volume; - bestVertex = vIndex; - bestEdgeVector = edgeVectors[index]; - } - } - } - extremes.Remove(bestVertex); - if (bestVertex == -1) break; - initialPoints.Add(bestVertex); - edgeVectors[index++] = bestEdgeVector; - CurrentVertex = bestVertex; UpdateCenter(); - } - // hmm, there are not enough points on the bounding box to make a simplex. It is rare but entirely possibly. - // As an extreme, the bounding box can be made in n dimensions from only 2 unique points. When we can't find - // enough unique points, we start again with ALL the vertices. The following is a near replica of the code - // above, but instead of extremes, we consider "allVertices". - if (initialPoints.Count <= NumOfDimensions && !IsLifted) - { - var allVertices = Enumerable.Range(0, NumberOfVertices).ToList(); - while (index < NumOfDimensions && allVertices.Any()) - { - var bestVertex = -1; - var bestEdgeVector = new float[] { }; - var maxVolume = 0.0; - for (var i = allVertices.Count - 1; i >= 0; i--) - { - // count backwards in order to remove potential duplicates - var vIndex = allVertices[i]; - if (initialPoints.Contains(vIndex)) allVertices.RemoveAt(i); - else - { - edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1); - var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber); - if (maxVolume < volume) - { - maxVolume = volume; - bestVertex = vIndex; - bestEdgeVector = edgeVectors[index]; - } - } - } - allVertices.Remove(bestVertex); - if (bestVertex == -1) break; - initialPoints.Add(bestVertex); - edgeVectors[index++] = bestEdgeVector; - CurrentVertex = bestVertex; UpdateCenter(); - } - } - if (initialPoints.Count <= NumOfDimensions && IsLifted) - { - var allVertices = Enumerable.Range(0, NumberOfVertices).ToList(); - while (index < NumOfDimensions && allVertices.Any()) - { - var bestVertex = -1; - var bestEdgeVector = new float[] { }; - var maxVolume = 0.0; - for (var i = allVertices.Count - 1; i >= 0; i--) - { - // count backwards in order to remove potential duplicates - var vIndex = allVertices[i]; - if (initialPoints.Contains(vIndex)) allVertices.RemoveAt(i); - else - { - mathHelper.RandomOffsetToLift(vIndex, maxima.Last()-minima.Last()); - edgeVectors[index] = mathHelper.VectorBetweenVertices(vIndex, vertex1); - var volume = mathHelper.GetSimplexVolume(edgeVectors, index, bigNumber); - if (maxVolume < volume) - { - maxVolume = volume; - bestVertex = vIndex; - bestEdgeVector = edgeVectors[index]; - } - } - } - allVertices.Remove(bestVertex); - if (bestVertex == -1) break; - initialPoints.Add(bestVertex); - edgeVectors[index++] = bestEdgeVector; - CurrentVertex = bestVertex; UpdateCenter(); - } - } - if (initialPoints.Count <= NumOfDimensions && IsLifted) - throw new ArgumentException("The input data is degenerate. It appears to exist in " + NumOfDimensions + - " dimensions, but it is a " + (NumOfDimensions - 1) + " dimensional set (i.e. the point of collinear," - + " coplanar, or co-hyperplanar.)"); - return initialPoints; - } - - /// - /// Check if 2 faces are adjacent and if so, update their AdjacentFaces array. - /// - /// The l. - /// The r. - private void UpdateAdjacency(ConvexFaceInternal l, ConvexFaceInternal r) - { - var lv = l.Vertices; - var rv = r.Vertices; - int i; - - // reset marks on the 1st face - for (i = 0; i < lv.Length; i++) VertexVisited[lv[i]] = false; - - // mark all vertices on the 2nd face - for (i = 0; i < rv.Length; i++) VertexVisited[rv[i]] = true; - - // find the 1st false index - for (i = 0; i < lv.Length; i++) if (!VertexVisited[lv[i]]) break; - - // no vertex was marked - if (i == NumOfDimensions) return; - - // check if only 1 vertex wasn't marked - for (var j = i + 1; j < lv.Length; j++) if (!VertexVisited[lv[j]]) return; - - // if we are here, the two faces share an edge - l.AdjacentFaces[i] = r.Index; - - // update the adj. face on the other face - find the vertex that remains marked - for (i = 0; i < lv.Length; i++) VertexVisited[lv[i]] = false; - for (i = 0; i < rv.Length; i++) - { - if (VertexVisited[rv[i]]) break; - } - r.AdjacentFaces[i] = l.Index; - } - - /// - /// Used in the "initialization" code. - /// - /// The face. - private void FindBeyondVertices(ConvexFaceInternal face) - { - var beyondVertices = face.VerticesBeyond; - MaxDistance = float.NegativeInfinity; - FurthestVertex = 0; - for (var i = 0; i < NumberOfVertices; i++) - { - if (VertexVisited[i]) continue; - IsBeyond(face, beyondVertices, i); - } - - face.FurthestVertex = FurthestVertex; - } - #region Fields - - /// - /// Corresponds to the dimension of the data. - /// When the "lifted" hull is computed, Dimension is automatically incremented by one. - /// - internal readonly int NumOfDimensions; - - /// - /// Are we on a paraboloid? - /// - private readonly bool IsLifted; - - /// - /// Explained in ConvexHullComputationConfig. - /// - private readonly float PlaneDistanceTolerance; - - /* - * Representation of the input vertices. - * - * - In the algorithm, a vertex is represented by its index in the Vertices array. - * This makes the algorithm a lot faster (up to 30%) than using object reference everywhere. - * - Positions are stored as a single array of values. Coordinates for vertex with index i - * are stored at indices - /// The vertices - /// - private readonly IVertex[] Vertices; - /// - /// The positions - /// - private float[] Positions; - /// - /// The vertex marks - /// - private readonly bool[] VertexVisited; - - private readonly int NumberOfVertices; - - /* - * The triangulation faces are represented in a single pool for objects that are being reused. - * This allows for represent the faces as integers and significantly speeds up many computations. - * - AffectedFaceFlags are used to mark affected faces/ - */ - /// - /// The face pool - /// - internal ConvexFaceInternal[] FacePool; - /// - /// The affected face flags - /// - internal bool[] AffectedFaceFlags; - - /// - /// Used to track the size of the current hull in the Update/RollbackCenter functions. - /// - private int ConvexHullSize; - - /// - /// A list of faces that that are not a part of the final convex hull and still need to be processed. - /// - private readonly FaceList UnprocessedFaces; - - /// - /// A list of faces that form the convex hull. - /// - private readonly IndexBuffer ConvexFaces; - - /// - /// The vertex that is currently being processed. - /// - private int CurrentVertex; - - /// - /// A helper variable to determine the furthest vertex for a particular convex face. - /// - private double MaxDistance; - - /// - /// A helper variable to help determine the index of the vertex that is furthest from the face that is currently being - /// processed. - /// - private int FurthestVertex; - - /// - /// The centroid of the currently computed hull. - /// - private readonly float[] Center; - - /* - * Helper arrays to store faces for adjacency update. - * This is just to prevent unnecessary allocations. - */ - /// - /// The update buffer - /// - private readonly int[] UpdateBuffer; - /// - /// The update indices - /// - private readonly int[] UpdateIndices; - - /// - /// Used to determine which faces need to be updated at each step of the algorithm. - /// - private readonly IndexBuffer TraverseStack; - - /// - /// Used for VerticesBeyond for faces that are on the convex hull. - /// - private readonly IndexBuffer EmptyBuffer; - - /// - /// Used to determine which vertices are "above" (or "beyond") a face - /// - private IndexBuffer BeyondBuffer; - - /// - /// Stores faces that are visible from the current vertex. - /// - private readonly IndexBuffer AffectedFaceBuffer; - - /// - /// Stores faces that form a "cone" created by adding new vertex. - /// - private readonly SimpleList ConeFaceBuffer; - - /// - /// Stores a list of "singular" (or "generate", "planar", etc.) vertices that cannot be part of the hull. - /// - private readonly HashSet SingularVertices; - - /// - /// The connector table helps to determine the adjacency of convex faces. - /// Hashing is used instead of pairwise comparison. This significantly speeds up the computations, - /// especially for higher dimensions. - /// - private readonly ConnectorList[] ConnectorTable; - - /// - /// Manages the memory allocations and storage of unused objects. - /// Saves the garbage collector a lot of work. - /// - private readonly ObjectManager ObjectManager; - - /// - /// Helper class for handling math related stuff. - /// - private readonly MathHelper mathHelper; - private readonly List[] boundingBoxPoints; - private int indexOfDimensionWithLeastExtremes; - private readonly float[] minima; - private readonly float[] maxima; - #endregion - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs.meta deleted file mode 100644 index 35319f3c2..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.Initialize.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 48bd00cb6ec8d244f90e1c1608f39074 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs deleted file mode 100644 index 058b82640..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs +++ /dev/null @@ -1,635 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System.Collections.Generic; - -namespace MIConvexHull -{ - /* - * Main part of the algorithm - * Basic idea: - * - Create the initial hull (done in Initialize.cs) - * - * For each face there are "vertices beyond" which are "visible" from it. - * If there are no such vertices, the face is on the hull. - * - * - While there is at least one face with at least one "vertex beyond": - * * Pick the furthest beyond vertex - * * For this vertex: - * > find all faces that are visible from it (TagAffectedFaces) - * > remove them and replace them with a "cone" created by the vertex and the boundary - * of the affected faces, and for each new face, compute "beyond vertices" - * (CreateCone + CommitCone) - * - * + Implement it in way that is fast, but hard to understand and maintain. - */ - - /// - /// Class ConvexHullAlgorithm. - /// - internal partial class ConvexHullAlgorithm - { - /// - /// Tags all faces seen from the current vertex with 1. - /// - /// The current face. - private void TagAffectedFaces(ConvexFaceInternal currentFace) - { - AffectedFaceBuffer.Clear(); - AffectedFaceBuffer.Add(currentFace.Index); - TraverseAffectedFaces(currentFace.Index); - } - - /// - /// Recursively traverse all the relevant faces. - /// - /// The current face. - private void TraverseAffectedFaces(int currentFace) - { - TraverseStack.Clear(); - TraverseStack.Push(currentFace); - AffectedFaceFlags[currentFace] = true; - - while (TraverseStack.Count > 0) - { - var top = FacePool[TraverseStack.Pop()]; - for (var i = 0; i < NumOfDimensions; i++) - { - var adjFace = top.AdjacentFaces[i]; - - if (!AffectedFaceFlags[adjFace] && - mathHelper.GetVertexDistance(CurrentVertex, FacePool[adjFace]) >= PlaneDistanceTolerance) - { - AffectedFaceBuffer.Add(adjFace); - AffectedFaceFlags[adjFace] = true; - TraverseStack.Push(adjFace); - } - } - } - } - - /// - /// Creates a new deferred face. - /// - /// The face. - /// Index of the face. - /// The pivot. - /// Index of the pivot. - /// The old face. - /// DeferredFace. - private DeferredFace MakeDeferredFace(ConvexFaceInternal face, int faceIndex, ConvexFaceInternal pivot, - int pivotIndex, ConvexFaceInternal oldFace) - { - var ret = ObjectManager.GetDeferredFace(); - - ret.Face = face; - ret.FaceIndex = faceIndex; - ret.Pivot = pivot; - ret.PivotIndex = pivotIndex; - ret.OldFace = oldFace; - - return ret; - } - - /// - /// Connect faces using a connector. - /// - /// The connector. - private void ConnectFace(FaceConnector connector) - { - var index = connector.HashCode % Constants.ConnectorTableSize; - var list = ConnectorTable[index]; - - for (var current = list.First; current != null; current = current.Next) - { - if (FaceConnector.AreConnectable(connector, current, NumOfDimensions)) - { - list.Remove(current); - FaceConnector.Connect(current, connector); - current.Face = null; - connector.Face = null; - ObjectManager.DepositConnector(current); - ObjectManager.DepositConnector(connector); - return; - } - } - - list.Add(connector); - } - - /// - /// Removes the faces "covered" by the current vertex and adds the newly created ones. - /// - /// true if XXXX, false otherwise. - private bool CreateCone() - { - var currentVertexIndex = CurrentVertex; - ConeFaceBuffer.Clear(); - - for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) - { - var oldFaceIndex = AffectedFaceBuffer[fIndex]; - var oldFace = FacePool[oldFaceIndex]; - - // Find the faces that need to be updated - var updateCount = 0; - for (var i = 0; i < NumOfDimensions; i++) - { - var af = oldFace.AdjacentFaces[i]; - if (!AffectedFaceFlags[af]) // Tag == false when oldFaces does not contain af - { - UpdateBuffer[updateCount] = af; - UpdateIndices[updateCount] = i; - ++updateCount; - } - } - - for (var i = 0; i < updateCount; i++) - { - var adjacentFace = FacePool[UpdateBuffer[i]]; - - var oldFaceAdjacentIndex = 0; - var adjFaceAdjacency = adjacentFace.AdjacentFaces; - for (var j = 0; j < adjFaceAdjacency.Length; j++) - { - if (oldFaceIndex == adjFaceAdjacency[j]) - { - oldFaceAdjacentIndex = j; - break; - } - } - - var forbidden = UpdateIndices[i]; // Index of the face that corresponds to this adjacent face - - int oldVertexIndex; - int[] vertices; - - var newFaceIndex = ObjectManager.GetFace(); - var newFace = FacePool[newFaceIndex]; - vertices = newFace.Vertices; - for (var j = 0; j < NumOfDimensions; j++) vertices[j] = oldFace.Vertices[j]; - oldVertexIndex = vertices[forbidden]; - - int orderedPivotIndex; - - // correct the ordering - if (currentVertexIndex < oldVertexIndex) - { - orderedPivotIndex = 0; - for (var j = forbidden - 1; j >= 0; j--) - { - if (vertices[j] > currentVertexIndex) vertices[j + 1] = vertices[j]; - else - { - orderedPivotIndex = j + 1; - break; - } - } - } - else - { - orderedPivotIndex = NumOfDimensions - 1; - for (var j = forbidden + 1; j < NumOfDimensions; j++) - { - if (vertices[j] < currentVertexIndex) vertices[j - 1] = vertices[j]; - else - { - orderedPivotIndex = j - 1; - break; - } - } - } - - vertices[orderedPivotIndex] = CurrentVertex; - - if (!mathHelper.CalculateFacePlane(newFace, Center)) - { - return false; - } - - ConeFaceBuffer.Add(MakeDeferredFace(newFace, orderedPivotIndex, adjacentFace, oldFaceAdjacentIndex, - oldFace)); - } - } - - return true; - } - - /// - /// Commits a cone and adds a vertex to the convex hull. - /// - private void CommitCone() - { - // Fill the adjacency. - for (var i = 0; i < ConeFaceBuffer.Count; i++) - { - var face = ConeFaceBuffer[i]; - - var newFace = face.Face; - var adjacentFace = face.Pivot; - var oldFace = face.OldFace; - var orderedPivotIndex = face.FaceIndex; - - newFace.AdjacentFaces[orderedPivotIndex] = adjacentFace.Index; - adjacentFace.AdjacentFaces[face.PivotIndex] = newFace.Index; - - // let there be a connection. - for (var j = 0; j < NumOfDimensions; j++) - { - if (j == orderedPivotIndex) continue; - var connector = ObjectManager.GetConnector(); - connector.Update(newFace, j, NumOfDimensions); - ConnectFace(connector); - } - - // the id adjacent face on the hull? If so, we can use simple method to find beyond vertices. - if (adjacentFace.VerticesBeyond.Count == 0) - FindBeyondVertices(newFace, oldFace.VerticesBeyond); - // it is slightly more effective if the face with the lower number of beyond vertices comes first. - else if (adjacentFace.VerticesBeyond.Count < oldFace.VerticesBeyond.Count) - FindBeyondVertices(newFace, adjacentFace.VerticesBeyond, oldFace.VerticesBeyond); - else - FindBeyondVertices(newFace, oldFace.VerticesBeyond, adjacentFace.VerticesBeyond); - - // This face will definitely lie on the hull - if (newFace.VerticesBeyond.Count == 0) - { - ConvexFaces.Add(newFace.Index); - UnprocessedFaces.Remove(newFace); - ObjectManager.DepositVertexBuffer(newFace.VerticesBeyond); - newFace.VerticesBeyond = EmptyBuffer; - } - else // Add the face to the list - { - UnprocessedFaces.Add(newFace); - } - - // recycle the object. - ObjectManager.DepositDeferredFace(face); - } - - // Recycle the affected faces. - for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) - { - var face = AffectedFaceBuffer[fIndex]; - UnprocessedFaces.Remove(FacePool[face]); - ObjectManager.DepositFace(face); - } - } - - /// - /// Check whether the vertex v is beyond the given face. If so, add it to beyondVertices. - /// - /// The face. - /// The beyond vertices. - /// The v. - private void IsBeyond(ConvexFaceInternal face, IndexBuffer beyondVertices, int v) - { - var distance = mathHelper.GetVertexDistance(v, face); - if (distance >= PlaneDistanceTolerance) - { - if (distance > MaxDistance) - { - // If it's within the tolerance distance, use the lex. larger point - if (distance - MaxDistance < PlaneDistanceTolerance) - { // todo: why is this LexCompare necessary. Would seem to favor x over y over z (etc.)? - if (LexCompare(v, FurthestVertex) > 0) - { - MaxDistance = distance; - FurthestVertex = v; - } - } - else - { - MaxDistance = distance; - FurthestVertex = v; - } - } - beyondVertices.Add(v); - } - } - - - /// - /// Compares the values of two vertices. The return value (-1, 0 or +1) are found - /// by first checking the first coordinate and then progressing through the rest. - /// In this way {2, 8} will be a "-1" (less than) {3, 1}. - /// - /// The base vertex index, u. - /// The compared vertex index, v. - /// System.Int32. - private int LexCompare(int u, int v) - { - int uOffset = u * NumOfDimensions, vOffset = v * NumOfDimensions; - for (var i = 0; i < NumOfDimensions; i++) - { - double x = Positions[uOffset + i], y = Positions[vOffset + i]; - var comp = x.CompareTo(y); - if (comp != 0) return comp; - } - return 0; - } - - - /// - /// Used by update faces. - /// - /// The face. - /// The beyond. - /// The beyond1. - private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond, IndexBuffer beyond1) - { - var beyondVertices = BeyondBuffer; - - MaxDistance = double.NegativeInfinity; - FurthestVertex = 0; - int v; - - for (var i = 0; i < beyond1.Count; i++) VertexVisited[beyond1[i]] = true; - VertexVisited[CurrentVertex] = false; - for (var i = 0; i < beyond.Count; i++) - { - v = beyond[i]; - if (v == CurrentVertex) continue; - VertexVisited[v] = false; - IsBeyond(face, beyondVertices, v); - } - - for (var i = 0; i < beyond1.Count; i++) - { - v = beyond1[i]; - if (VertexVisited[v]) IsBeyond(face, beyondVertices, v); - } - - face.FurthestVertex = FurthestVertex; - - // Pull the old switch a roo (switch the face beyond buffers) - var temp = face.VerticesBeyond; - face.VerticesBeyond = beyondVertices; - if (temp.Count > 0) temp.Clear(); - BeyondBuffer = temp; - } - - /// - /// Finds the beyond vertices. - /// - /// The face. - /// The beyond. - private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond) - { - var beyondVertices = BeyondBuffer; - - MaxDistance = double.NegativeInfinity; - FurthestVertex = 0; - int v; - - for (var i = 0; i < beyond.Count; i++) - { - v = beyond[i]; - if (v == CurrentVertex) continue; - IsBeyond(face, beyondVertices, v); - } - - face.FurthestVertex = FurthestVertex; - - // Pull the old switch a roo (switch the face beyond buffers) - var temp = face.VerticesBeyond; - face.VerticesBeyond = beyondVertices; - if (temp.Count > 0) temp.Clear(); - BeyondBuffer = temp; - } - - /// - /// Recalculates the centroid of the current hull. - /// - private void UpdateCenter() - { - for (var i = 0; i < NumOfDimensions; i++) Center[i] *= ConvexHullSize; - ConvexHullSize += 1; - var f = 1.0f / ConvexHullSize; - var co = CurrentVertex * NumOfDimensions; - for (var i = 0; i < NumOfDimensions; i++) Center[i] = f * (Center[i] + Positions[co + i]); - } - - /// - /// Removes the last vertex from the center. - /// - private void RollbackCenter() - { - for (var i = 0; i < NumOfDimensions; i++) Center[i] *= ConvexHullSize; - ConvexHullSize -= 1; - var f = ConvexHullSize > 0 ? 1.0f / ConvexHullSize : 0.0f; - var co = CurrentVertex * NumOfDimensions; - for (var i = 0; i < NumOfDimensions; i++) Center[i] = f * (Center[i] - Positions[co + i]); - } - - /// - /// Handles singular vertex. - /// - private void HandleSingular() - { - RollbackCenter(); - SingularVertices.Add(CurrentVertex); - - // This means that all the affected faces must be on the hull and that all their "vertices beyond" are singular. - for (var fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) - { - var face = FacePool[AffectedFaceBuffer[fIndex]]; - var vb = face.VerticesBeyond; - for (var i = 0; i < vb.Count; i++) - { - SingularVertices.Add(vb[i]); - } - - ConvexFaces.Add(face.Index); - UnprocessedFaces.Remove(face); - ObjectManager.DepositVertexBuffer(face.VerticesBeyond); - face.VerticesBeyond = EmptyBuffer; - } - } - - /// - /// Get a vertex coordinate. In order to reduce speed, all vertex coordinates - /// have been placed in a single array. - /// - /// The vertex index. - /// The index of the dimension. - /// System.Double. - private float GetCoordinate(int vIndex, int dimension) - { - return Positions[vIndex * NumOfDimensions + dimension]; - } - - #region Returning the Results in the proper format - - /// - /// Gets the hull vertices. - /// - /// The type of the t vertex. - /// The data. - /// TVertex[]. - private TVertex[] GetHullVertices(IList data) - { - var cellCount = ConvexFaces.Count; - var hullVertexCount = 0; - - for (var i = 0; i < NumberOfVertices; i++) VertexVisited[i] = false; - - for (var i = 0; i < cellCount; i++) - { - var vs = FacePool[ConvexFaces[i]].Vertices; - for (var j = 0; j < vs.Length; j++) - { - var v = vs[j]; - if (!VertexVisited[v]) - { - VertexVisited[v] = true; - hullVertexCount++; - } - } - } - - var result = new TVertex[hullVertexCount]; - for (var i = 0; i < NumberOfVertices; i++) - { - if (VertexVisited[i]) result[--hullVertexCount] = data[i]; - } - - return result; - } - - /// - /// Finds the convex hull and creates the TFace objects. - /// - /// The type of the t face. - /// The type of the t vertex. - /// TFace[]. - private TFace[] GetConvexFaces() - where TFace : ConvexFace, new() - where TVertex : IVertex - { - var faces = ConvexFaces; - var cellCount = faces.Count; - var cells = new TFace[cellCount]; - - for (var i = 0; i < cellCount; i++) - { - var face = FacePool[faces[i]]; - var vertices = new TVertex[NumOfDimensions]; - for (var j = 0; j < NumOfDimensions; j++) - { - vertices[j] = (TVertex)Vertices[face.Vertices[j]]; - } - - cells[i] = new TFace - { - Vertices = vertices, - Adjacency = new TFace[NumOfDimensions], - Normal = IsLifted ? null : face.Normal - }; - face.Tag = i; - } - - for (var i = 0; i < cellCount; i++) - { - var face = FacePool[faces[i]]; - var cell = cells[i]; - for (var j = 0; j < NumOfDimensions; j++) - { - if (face.AdjacentFaces[j] < 0) continue; - cell.Adjacency[j] = cells[FacePool[face.AdjacentFaces[j]].Tag]; - } - - // Fix the vertex orientation. - if (face.IsNormalFlipped) - { - var tempVert = cell.Vertices[0]; - cell.Vertices[0] = cell.Vertices[NumOfDimensions - 1]; - cell.Vertices[NumOfDimensions - 1] = tempVert; - - var tempAdj = cell.Adjacency[0]; - cell.Adjacency[0] = cell.Adjacency[NumOfDimensions - 1]; - cell.Adjacency[NumOfDimensions - 1] = tempAdj; - } - } - - return cells; - } - - /// - /// For 2D only: Returns the result in counter-clockwise order starting with the element with the lowest X value. - /// If there are multiple vertices with the same minimum X, then the one with the lowest Y is chosen. - /// - /// The type of the vertex. - /// The type of the face. - /// The data. - /// - private ConvexHull Return2DResultInOrder(IList data) - where TVertex : IVertex - where TFace : ConvexFace, new() - { - TFace[] faces = GetConvexFaces(); - var numPoints = faces.Length; - var orderDictionary = new Dictionary(); - foreach (var face in faces) - orderDictionary.Add(face.Vertices[1], face); - var firstPoint = faces[0].Vertices[1]; - var nextPoint = faces[0].Vertices[0]; - var orderedPointList = new List(); - orderedPointList.Add(firstPoint); - var orderedFaceList = new List(); - orderedFaceList.Add(faces[1]); - var lowestXMinIndex = 0; - var k = 0; - while (!nextPoint.Equals(firstPoint)) - { - orderedPointList.Add(nextPoint); - var nextFace = orderDictionary[nextPoint]; - orderedFaceList.Add(nextFace); - if (nextPoint.Position[0] < orderedPointList[lowestXMinIndex].Position[0] - || (nextPoint.Position[0] == orderedPointList[lowestXMinIndex].Position[0] - && nextPoint.Position[1] <= orderedPointList[lowestXMinIndex].Position[1])) - lowestXMinIndex = k; - k++; - nextPoint = nextFace.Vertices[0]; - } - TVertex[] points = new TVertex[numPoints]; - for (int i = 0; i < numPoints; i++) - { - var j = (i + lowestXMinIndex) % numPoints; - points[i] = orderedPointList[j]; - faces[i] = orderedFaceList[j]; - } - return new ConvexHull - { - Points = points, - Faces = faces - }; - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs.meta deleted file mode 100644 index 8eb122b5b..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ConvexHullAlgorithm.MainLoop.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f7f265217d00ea340b87645242a45181 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs deleted file mode 100644 index 761dd1ccf..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs +++ /dev/null @@ -1,237 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -namespace MIConvexHull -{ - /// - /// For deferred face addition. - /// - internal sealed class DeferredFace - { - /// - /// The faces. - /// - public ConvexFaceInternal Face, Pivot, OldFace; - - /// - /// The indices. - /// - public int FaceIndex, PivotIndex; - } - - /// - /// A helper class used to connect faces. - /// - internal sealed class FaceConnector - { - /// - /// The edge to be connected. - /// - public int EdgeIndex; - - /// - /// The face. - /// - public ConvexFaceInternal Face; - - /// - /// The hash code computed from indices. - /// - public uint HashCode; - - /// - /// Next node in the list. - /// - public FaceConnector Next; - - /// - /// Prev node in the list. - /// - public FaceConnector Previous; - - /// - /// The vertex indices. - /// - public int[] Vertices; - - /// - /// Ctor. - /// - /// The dimension. - public FaceConnector(int dimension) - { - Vertices = new int[dimension - 1]; - } - - /// - /// Updates the connector. - /// - /// The face. - /// Index of the edge. - /// The dim. - public void Update(ConvexFaceInternal face, int edgeIndex, int dim) - { - Face = face; - EdgeIndex = edgeIndex; - - uint hashCode = 23; - - unchecked - { - var vs = face.Vertices; - int i, c = 0; - for (i = 0; i < edgeIndex; i++) - { - Vertices[c++] = vs[i]; - hashCode += 31*hashCode + (uint) vs[i]; - } - for (i = edgeIndex + 1; i < vs.Length; i++) - { - Vertices[c++] = vs[i]; - hashCode += 31*hashCode + (uint) vs[i]; - } - } - - HashCode = hashCode; - } - - /// - /// Can two faces be connected. - /// - /// a. - /// The b. - /// The dim. - /// true if XXXX, false otherwise. - public static bool AreConnectable(FaceConnector a, FaceConnector b, int dim) - { - if (a.HashCode != b.HashCode) return false; - - var av = a.Vertices; - var bv = b.Vertices; - for (var i = 0; i < av.Length; i++) - { - if (av[i] != bv[i]) return false; - } - - return true; - } - - /// - /// Connect two faces. - /// - /// a. - /// The b. - public static void Connect(FaceConnector a, FaceConnector b) - { - a.Face.AdjacentFaces[a.EdgeIndex] = b.Face.Index; - b.Face.AdjacentFaces[b.EdgeIndex] = a.Face.Index; - } - } - - /// - /// This internal class manages the faces of the convex hull. It is a - /// separate class from the desired user class. - /// - internal sealed class ConvexFaceInternal - { - /// - /// Gets or sets the adjacent face data. - /// - public int[] AdjacentFaces; - - /// - /// The furthest vertex. - /// - public int FurthestVertex; - - /// - /// Index of the face inside the pool. - /// - public int Index; - - /// - /// Is it present in the list. - /// - public bool InList; - - /// - /// Is the normal flipped? - /// - public bool IsNormalFlipped; - - /// - /// Next node in the list. - /// - public ConvexFaceInternal Next; - - /// - /// Gets or sets the normal vector. - /// - public float[] Normal; - - /// - /// Face plane constant element. - /// - public float Offset; - - //public int UnprocessedIndex; - - /// - /// Prev node in the list. - /// - public ConvexFaceInternal Previous; - - /// - /// Used to traverse affected faces and create the Delaunay representation. - /// - public int Tag; - - /// - /// Gets or sets the vertices. - /// - public int[] Vertices; - - /// - /// Gets or sets the vertices beyond. - /// - public IndexBuffer VerticesBeyond; - - /// - /// Initializes a new instance of the class. - /// - /// The dimension. - /// The index. - /// The beyond list. - public ConvexFaceInternal(int dimension, int index, IndexBuffer beyondList) - { - Index = index; - AdjacentFaces = new int[dimension]; - VerticesBeyond = beyondList; - Normal = new float[dimension]; - Vertices = new int[dimension]; - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs.meta deleted file mode 100644 index 411bcb0d5..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/DataModel.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 58c58e163f4c86e48a6df3ee6c5056c0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs deleted file mode 100644 index 0f030c59d..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs +++ /dev/null @@ -1,480 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; -using System.Collections.Generic; -using CultMath; -using static CultMath.math; -using Random = CultMath.Random; - -namespace MIConvexHull -{ - /// - /// A helper class mostly for normal computation. If convex hulls are computed - /// in higher dimensions, it might be a good idea to add a specific - /// FindNormalVectorND function. - /// - internal class MathHelper - { - /// - /// The dimension - /// - private readonly int Dimension; - /// - /// The matrix pivots - /// - private readonly int[] matrixPivots; - /// - /// The n d matrix - /// - private readonly float[] nDMatrix; - /// - /// The n d normal helper vector - /// - private readonly float[] nDNormalHelperVector; - - /// - /// The nt x - /// - private readonly float[] ntX; - /// - /// The nt y - /// - private readonly float[] ntY; - /// - /// The nt z - /// - private readonly float[] ntZ; - - /// - /// The position data - /// - private readonly float[] PositionData; - - private static Random _random = new Random((uint) (DateTime.Now.Ticks % uint.MaxValue)); - - /// - /// Initializes a new instance of the class. - /// - /// The dimension. - /// The positions. - internal MathHelper(int dimension, float[] positions) - { - PositionData = positions; - Dimension = dimension; - - ntX = new float[Dimension]; - ntY = new float[Dimension]; - ntZ = new float[Dimension]; - - nDNormalHelperVector = new float[Dimension]; - nDMatrix = new float[Dimension * Dimension]; - matrixPivots = new int[Dimension]; - } - - /// - /// Calculates the normal and offset of the hyper-plane given by the face's vertices. - /// - /// The face. - /// The center. - /// true if XXXX, false otherwise. - internal bool CalculateFacePlane(ConvexFaceInternal face, float[] center) - { - var vertices = face.Vertices; - var normal = face.Normal; - FindNormalVector(vertices, normal); - - if (float.IsNaN(normal[0])) - { - return false; - } - - var offset = 0.0f; - var centerDistance = 0.0f; - var fi = vertices[0] * Dimension; - for (var i = 0; i < Dimension; i++) - { - var n = normal[i]; - offset += n * PositionData[fi + i]; - centerDistance += n * center[i]; - } - face.Offset = -offset; - centerDistance -= offset; - - if (centerDistance > 0) - { - for (var i = 0; i < Dimension; i++) normal[i] = -normal[i]; - face.Offset = offset; - face.IsNormalFlipped = true; - } - else face.IsNormalFlipped = false; - - return true; - } - - /// - /// Check if the vertex is "visible" from the face. - /// The vertex is "over face" if the return value is > Constants.PlaneDistanceTolerance. - /// - /// The v. - /// The f. - /// The vertex is "over face" if the result is positive. - internal double GetVertexDistance(int v, ConvexFaceInternal f) - { - var normal = f.Normal; - var x = v * Dimension; - var distance = f.Offset; - for (var i = 0; i < normal.Length; i++) distance += normal[i] * PositionData[x + i]; - return distance; - } - - /// - /// Returns the vector the between vertices. - /// - /// To index. - /// From index. - /// System.Double[]. - internal float[] VectorBetweenVertices(int toIndex, int fromIndex) - { - var target = new float[Dimension]; - VectorBetweenVertices(toIndex, fromIndex, target); - return target; - } - /// - /// Returns the vector the between vertices. - /// - /// From index. - /// To index. - /// The target. - /// - private void VectorBetweenVertices(int toIndex, int fromIndex, float[] target) - { - int u = toIndex * Dimension, v = fromIndex * Dimension; - for (var i = 0; i < Dimension; i++) - { - target[i] = PositionData[u + i] - PositionData[v + i]; - } - } - - internal void RandomOffsetToLift(int index, float maxHeight) - { - var liftIndex = (index * Dimension) + Dimension - 1; - PositionData[liftIndex] += 0.0001f * maxHeight * (_random.NextFloat() - 0.5f); - } - #region Find the normal vector of the face - /// - /// Finds normal vector of a hyper-plane given by vertices. - /// Stores the results to normalData. - /// - /// The vertices. - /// The normal data. - private void FindNormalVector(int[] vertices, float[] normalData) - { - switch (Dimension) - { - case 2: - FindNormalVector2D(vertices, normalData); - break; - case 3: - FindNormalVector3D(vertices, normalData); - break; - case 4: - FindNormalVector4D(vertices, normalData); - break; - default: - FindNormalVectorND(vertices, normalData); - break; - } - } - /// - /// Finds 2D normal vector. - /// - /// The vertices. - /// The normal. - private void FindNormalVector2D(int[] vertices, float[] normal) - { - VectorBetweenVertices(vertices[1], vertices[0], ntX); - - var nx = -ntX[1]; - var ny = ntX[0]; - - var norm = sqrt(nx * nx + ny * ny); - - var f = 1.0f / norm; - normal[0] = f * nx; - normal[1] = f * ny; - } - /// - /// Finds 3D normal vector. - /// - /// The vertices. - /// The normal. - private void FindNormalVector3D(int[] vertices, float[] normal) - { - VectorBetweenVertices(vertices[1], vertices[0], ntX); - VectorBetweenVertices(vertices[2], vertices[1], ntY); - - var nx = ntX[1] * ntY[2] - ntX[2] * ntY[1]; - var ny = ntX[2] * ntY[0] - ntX[0] * ntY[2]; - var nz = ntX[0] * ntY[1] - ntX[1] * ntY[0]; - - var norm = sqrt(nx * nx + ny * ny + nz * nz); - - var f = 1.0f / norm; - normal[0] = f * nx; - normal[1] = f * ny; - normal[2] = f * nz; - } - /// - /// Finds 4D normal vector. - /// - /// The vertices. - /// The normal. - private void FindNormalVector4D(int[] vertices, float[] normal) - { - VectorBetweenVertices(vertices[1], vertices[0], ntX); - VectorBetweenVertices(vertices[2], vertices[1], ntY); - VectorBetweenVertices(vertices[3], vertices[2], ntZ); - - var x = ntX; - var y = ntY; - var z = ntZ; - - // This was generated using Mathematica - var nx = x[3] * (y[2] * z[1] - y[1] * z[2]) - + x[2] * (y[1] * z[3] - y[3] * z[1]) - + x[1] * (y[3] * z[2] - y[2] * z[3]); - var ny = x[3] * (y[0] * z[2] - y[2] * z[0]) - + x[2] * (y[3] * z[0] - y[0] * z[3]) - + x[0] * (y[2] * z[3] - y[3] * z[2]); - var nz = x[3] * (y[1] * z[0] - y[0] * z[1]) - + x[1] * (y[0] * z[3] - y[3] * z[0]) - + x[0] * (y[3] * z[1] - y[1] * z[3]); - var nw = x[2] * (y[0] * z[1] - y[1] * z[0]) - + x[1] * (y[2] * z[0] - y[0] * z[2]) - + x[0] * (y[1] * z[2] - y[2] * z[1]); - - var norm = sqrt(nx * nx + ny * ny + nz * nz + nw * nw); - - var f = 1.0f / norm; - normal[0] = f * nx; - normal[1] = f * ny; - normal[2] = f * nz; - normal[3] = f * nw; - } - - /// - /// Finds the normal vector nd. - /// - /// The vertices. - /// The normal. - private void FindNormalVectorND(int[] vertices, float[] normal) - { - /* We need to solve the matrix A n = B where - * - A contains coordinates of vertices as columns - * - B is vector with all 1's. Really, it should be the distance of - * the plane from the origin, but - since we're not worried about that - * here and we will normalize the normal anyway - all 1's suffices. - */ - var iPiv = matrixPivots; - var data = nDMatrix; - var norm = 0.0f; - - // Solve determinants by replacing x-th column by all 1. - for (var x = 0; x < Dimension; x++) - { - for (var i = 0; i < Dimension; i++) - { - var offset = vertices[i] * Dimension; - for (var j = 0; j < Dimension; j++) - { - // maybe I got the i/j mixed up here regarding the representation Math.net uses... - // ...but it does not matter since Det(A) = Det(Transpose(A)). - data[Dimension * i + j] = j == x ? 1.0f : PositionData[offset + j]; - } - } - LUFactor(data, Dimension, iPiv, nDNormalHelperVector); - var coord = 1.0f; - for (var i = 0; i < Dimension; i++) - { - if (iPiv[i] != i) coord *= -data[Dimension * i + i]; // the determinant sign changes on row swap. - else coord *= data[Dimension * i + i]; - } - normal[x] = coord; - norm += coord * coord; - } - - // Normalize the result - var f = 1.0f / sqrt(norm); - for (var i = 0; i < normal.Length; i++) normal[i] *= f; - } - #endregion - - #region Simplex Volume - /// - /// Gets the simplex volume. Prior to having enough edge vectors, the method pads the remaining with all - /// "other numbers". So, yes, this method is not really finding the volume. But a relative volume-like measure. It - /// uses the magnitude of the determinant as the volume stand-in following the Cayley-Menger theorem. - /// - /// The edge vectors. - /// The last index. - /// The big number. - /// System.Double. - internal float GetSimplexVolume(float[][] edgeVectors, int lastIndex, float bigNumber) - { - var A = new float[Dimension * Dimension]; - var index = 0; - for (int i = 0; i < Dimension; i++) - for (int j = 0; j < Dimension; j++) - if (i <= lastIndex) - A[index++] = edgeVectors[i][j]; - else A[index] = (pow(-1, index) * index++) / bigNumber; - // this last term is used for all the vertices in the comparison for the yet determined vertices - // the idea is to come up with sets of numbers that are orthogonal so that an non-zero value will result - // and to choose smallish numbers since the choice of vectors will affect what the end volume is. - // A better way (todo?) is to solve a smaller matrix. However, cases were found in which the obvious smaller vector - // (the upper left) had too many zeros. So, one would need to find the right subset. Indeed choosing a subset - // biases the first dimensions of the others. Perhaps a larger volume would be created from a different vertex - // if another subset of dimensions were used. - return abs(DeterminantDestructive(A)); - } - - /// - /// Determinants the destructive. - /// - /// a. - /// System.Double. - private float DeterminantDestructive(float[] A) - { - switch (Dimension) - { - case 0: - return 0.0f; - case 1: - return A[0]; - case 2: - return A[0] * A[3] - A[1] * A[2]; - case 3: - return A[0] * A[4] * A[8] + A[1] * A[5] * A[6] + A[2] * A[3] * A[7] - - A[0] * A[5] * A[7] - A[1] * A[3] * A[8] - A[2] * A[4] * A[6]; - default: - { - var iPiv = new int[Dimension]; - var helper = new float[Dimension]; - LUFactor(A, Dimension, iPiv, helper); - var det = 1.0f; - for (var i = 0; i < iPiv.Length; i++) - { - det *= A[Dimension * i + i]; - if (iPiv[i] != i) det *= -1; // the determinant sign changes on row swap. - } - return det; - } - } - } - #endregion - - - // Modified from Math.NET - // Copyright (c) 2009-2013 Math.NET - /// - /// Lus the factor. - /// - /// The data. - /// The order. - /// The ipiv. - /// The vec l ucolj. - private static void LUFactor(float[] data, int order, int[] ipiv, float[] vecLUcolj) - { - // Initialize the pivot matrix to the identity permutation. - for (var i = 0; i < order; i++) - { - ipiv[i] = i; - } - - // Outer loop. - for (var j = 0; j < order; j++) - { - var indexj = j * order; - var indexjj = indexj + j; - - // Make a copy of the j-th column to localize references. - for (var i = 0; i < order; i++) - { - vecLUcolj[i] = data[indexj + i]; - } - - // Apply previous transformations. - for (var i = 0; i < order; i++) - { - // Most of the time is spent in the following dot product. - var kmax = Math.Min(i, j); - var s = 0.0f; - for (var k = 0; k < kmax; k++) - { - s += data[k * order + i] * vecLUcolj[k]; - } - - data[indexj + i] = vecLUcolj[i] -= s; - } - - // Find pivot and exchange if necessary. - var p = j; - for (var i = j + 1; i < order; i++) - { - if (abs(vecLUcolj[i]) > abs(vecLUcolj[p])) - { - p = i; - } - } - - if (p != j) - { - for (var k = 0; k < order; k++) - { - var indexk = k * order; - var indexkp = indexk + p; - var indexkj = indexk + j; - var temp = data[indexkp]; - data[indexkp] = data[indexkj]; - data[indexkj] = temp; - } - - ipiv[j] = p; - } - - // Compute multipliers. - if (j < order & data[indexjj] != 0.0f) - { - for (var i = j + 1; i < order; i++) - { - data[indexj + i] /= data[indexjj]; - } - } - } - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs.meta deleted file mode 100644 index 920a8673d..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/MathHelper.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 8bb3327041efa18479319ec117493f8c -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs deleted file mode 100644 index 66333b74f..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs +++ /dev/null @@ -1,218 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; - -namespace MIConvexHull -{ - /// - /// A helper class for object allocation/storage. - /// This helps the GC a lot as it prevents the creation of about 75% of - /// new face objects (in the case of ConvexFaceInternal). In the case of - /// FaceConnectors and DefferedFaces, the difference is even higher (in most - /// cases O(1) vs O(number of created faces)). - /// - internal class ObjectManager - { - /// - /// The dimension - /// - private readonly int Dimension; - /// - /// The connector stack - /// - private FaceConnector ConnectorStack; - /// - /// The deferred face stack - /// - private readonly SimpleList DeferredFaceStack; - /// - /// The empty buffer stack - /// - private readonly SimpleList EmptyBufferStack; - /// - /// The face pool - /// - private ConvexFaceInternal[] FacePool; - /// - /// The face pool size - /// - private int FacePoolSize; - /// - /// The face pool capacity - /// - private int FacePoolCapacity; - /// - /// The free face indices - /// - private readonly IndexBuffer FreeFaceIndices; - - /// - /// The hull - /// - private readonly ConvexHullAlgorithm Hull; - - /// - /// Create the manager. - /// - /// The hull. - public ObjectManager(ConvexHullAlgorithm hull) - { - Dimension = hull.NumOfDimensions; - Hull = hull; - FacePool = hull.FacePool; - FacePoolSize = 0; - FacePoolCapacity = hull.FacePool.Length; - FreeFaceIndices = new IndexBuffer(); - - EmptyBufferStack = new SimpleList(); - DeferredFaceStack = new SimpleList(); - } - - /// - /// Return the face to the pool for later use. - /// - /// Index of the face. - public void DepositFace(int faceIndex) - { - var face = FacePool[faceIndex]; - var af = face.AdjacentFaces; - for (var i = 0; i < af.Length; i++) - { - af[i] = -1; - } - FreeFaceIndices.Push(faceIndex); - } - - /// - /// Reallocate the face pool, including the AffectedFaceFlags - /// - private void ReallocateFacePool() - { - var newPool = new ConvexFaceInternal[2*FacePoolCapacity]; - var newTags = new bool[2*FacePoolCapacity]; - Array.Copy(FacePool, newPool, FacePoolCapacity); - Buffer.BlockCopy(Hull.AffectedFaceFlags, 0, newTags, 0, FacePoolCapacity*sizeof (bool)); - FacePoolCapacity = 2*FacePoolCapacity; - Hull.FacePool = newPool; - FacePool = newPool; - Hull.AffectedFaceFlags = newTags; - } - - /// - /// Create a new face and put it in the pool. - /// - /// System.Int32. - private int CreateFace() - { - var index = FacePoolSize; - var face = new ConvexFaceInternal(Dimension, index, GetVertexBuffer()); - FacePoolSize++; - if (FacePoolSize > FacePoolCapacity) ReallocateFacePool(); - FacePool[index] = face; - return index; - } - - /// - /// Return index of an unused face or creates a new one. - /// - /// System.Int32. - public int GetFace() - { - if (FreeFaceIndices.Count > 0) return FreeFaceIndices.Pop(); - return CreateFace(); - } - - /// - /// Store a face connector in the "embedded" linked list. - /// - /// The connector. - public void DepositConnector(FaceConnector connector) - { - if (ConnectorStack == null) - { - connector.Next = null; - ConnectorStack = connector; - } - else - { - connector.Next = ConnectorStack; - ConnectorStack = connector; - } - } - - /// - /// Get an unused face connector. If none is available, create it. - /// - /// FaceConnector. - public FaceConnector GetConnector() - { - if (ConnectorStack == null) return new FaceConnector(Dimension); - - var ret = ConnectorStack; - ConnectorStack = ConnectorStack.Next; - ret.Next = null; - return ret; - } - - /// - /// Deposit the index buffer. - /// - /// The buffer. - public void DepositVertexBuffer(IndexBuffer buffer) - { - buffer.Clear(); - EmptyBufferStack.Push(buffer); - } - - /// - /// Get a store index buffer or create a new instance. - /// - /// IndexBuffer. - public IndexBuffer GetVertexBuffer() - { - return EmptyBufferStack.Count != 0 ? EmptyBufferStack.Pop() : new IndexBuffer(); - } - - /// - /// Deposit the deferred face. - /// - /// The face. - public void DepositDeferredFace(DeferredFace face) - { - DeferredFaceStack.Push(face); - } - - /// - /// Get the deferred face. - /// - /// DeferredFace. - public DeferredFace GetDeferredFace() - { - return DeferredFaceStack.Count != 0 ? DeferredFaceStack.Pop() : new DeferredFace(); - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs.meta deleted file mode 100644 index cb1cf125b..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/ConvexHull/ObjectManager.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e8d582e97c6824740ba9e1a41641b372 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs deleted file mode 100644 index 980e442d0..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Collections; -using MIConvexHull; - -public class Face3 : ConvexFace -{ - - -} diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs.meta deleted file mode 100644 index ab550e9ac..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Face3.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 30a2635ffe51545499499ce392b234c8 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs deleted file mode 100644 index b7217c074..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs +++ /dev/null @@ -1,53 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -namespace MIConvexHull -{ - /// - /// An interface for a structure with nD position. - /// - public interface IVertex - { - /// - /// Position of the vertex. - /// - /// The position. - float[] Position { get; } - } - - /// - /// "Default" vertex. - /// - /// - public class DefaultVertex : IVertex - { - /// - /// Position of the vertex. - /// - /// The position. - public float[] Position { get; set; } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs.meta deleted file mode 100644 index 06195b473..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/IVertex.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a55f713690988bb48aef7cbd4f01ebc7 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs deleted file mode 100644 index 690071bec..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs +++ /dev/null @@ -1,181 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System.Collections.Generic; -using System.Linq; - -namespace MIConvexHull -{ - /// - /// Simple interface to unify different types of triangulations in the future. - /// - /// The type of the t vertex. - /// The type of the t cell. - public interface ITriangulation - where TCell : TriangulationCell, new() - where TVertex : IVertex - { - /// - /// Triangulation simplexes. For 2D - triangles, 3D - tetrahedrons, etc ... - /// - /// The cells. - IEnumerable Cells { get; } - } - - /// - /// Factory class for creating triangulations. - /// - public static class Triangulation - { - /// - /// Creates the Delaunay triangulation of the input data. - /// - /// The type of the t vertex. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// ITriangulation<TVertex, DefaultTriangulationCell<TVertex>>. - public static ITriangulation> CreateDelaunay( - IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - { - return DelaunayTriangulation>.Create(data, PlaneDistanceTolerance); - } - - /// - /// Creates the Delaunay triangulation of the input data. - /// - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// ITriangulation<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>. - public static ITriangulation> CreateDelaunay( - IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - { - var points = data.Select(p => new DefaultVertex { Position = p }).ToList(); - return DelaunayTriangulation>.Create(points, PlaneDistanceTolerance); - } - - /// - /// Creates the Delaunay triangulation of the input data. - /// - /// The type of the t vertex. - /// The type of the t face. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// ITriangulation<TVertex, TFace>. - public static ITriangulation CreateDelaunay(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - where TFace : TriangulationCell, new() - { - return DelaunayTriangulation.Create(data, PlaneDistanceTolerance); - } - - - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// The type of the t edge. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// VoronoiMesh<TVertex, TCell, TEdge>. - public static VoronoiMesh CreateVoronoi(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TCell : TriangulationCell, new() - where TVertex : IVertex - where TEdge : VoronoiEdge, new() - { - return VoronoiMesh.Create(data, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// VoronoiMesh<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>. - public static - VoronoiMesh - , VoronoiEdge>> - CreateVoronoi(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - { - return - VoronoiMesh - , VoronoiEdge> - >.Create(data, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// VoronoiMesh<DefaultVertex, DefaultTriangulationCell<DefaultVertex>, VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>. - public static - VoronoiMesh - , - VoronoiEdge>> - CreateVoronoi(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - { - var points = data.Select(p => new DefaultVertex { Position = p.ToArray() }).ToList(); - return - VoronoiMesh - , - VoronoiEdge>>.Create(points, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>. - public static VoronoiMesh> CreateVoronoi( - IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - where TCell : TriangulationCell, new() - { - return VoronoiMesh>.Create(data, PlaneDistanceTolerance); - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs.meta deleted file mode 100644 index 2f8f75a85..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0762bfe861ed97749a222fb1c217788a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.meta deleted file mode 100644 index 346f5fafe..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 5bf99a94223c6e1458c243cb68091852 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs deleted file mode 100644 index 7209c0ccc..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs +++ /dev/null @@ -1,96 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System.Collections.Generic; -using System.Linq; - -namespace MIConvexHull -{ - /* - * Code here handles triangulation related stuff. - */ - - /// - /// Class ConvexHullAlgorithm. - /// - internal partial class ConvexHullAlgorithm - { - /// - /// Computes the Delaunay triangulation. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// The data. - /// The plane distance tolerance. - /// TCell[]. - internal static TCell[] GetDelaunayTriangulation(IList data, - float PlaneDistanceTolerance ) - where TCell : TriangulationCell, new() - where TVertex : IVertex - { - var ch = new ConvexHullAlgorithm(data.Cast().ToArray(), true, PlaneDistanceTolerance); - ch.GetConvexHull(); - ch.RemoveUpperFaces(); - return ch.GetConvexFaces(); - } - - /// - /// Removes up facing Tetrahedrons from the triangulation. - /// - private void RemoveUpperFaces() - { - var delaunayFaces = ConvexFaces; - var dimension = NumOfDimensions - 1; - - // Remove the "upper" faces - for (var i = delaunayFaces.Count - 1; i >= 0; i--) - { - var candidateIndex = delaunayFaces[i]; - var candidate = FacePool[candidateIndex]; - if (candidate.Normal[dimension] >= 0.0) - { - for (var fi = 0; fi < candidate.AdjacentFaces.Length; fi++) - { - var af = candidate.AdjacentFaces[fi]; - if (af >= 0) - { - var face = FacePool[af]; - for (var j = 0; j < face.AdjacentFaces.Length; j++) - { - if (face.AdjacentFaces[j] == candidateIndex) - { - face.AdjacentFaces[j] = -1; - } - } - } - } - delaunayFaces[i] = delaunayFaces[delaunayFaces.Count - 1]; - delaunayFaces.Pop(); - } - } - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs.meta deleted file mode 100644 index d78b006e2..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTrianglationInternal.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: de8661d86f2667647b9dca59c1c97e9a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs deleted file mode 100644 index a22962b23..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs +++ /dev/null @@ -1,75 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; -using System.Collections.Generic; - -namespace MIConvexHull -{ - /// - /// Calculation and representation of Delaunay triangulation. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// - public class DelaunayTriangulation : ITriangulation - where TCell : TriangulationCell, new() - where TVertex : IVertex - { - /// - /// Can only be created using a factory method. - /// - private DelaunayTriangulation() - { - } - - /// - /// Cells of the triangulation. - /// - /// The cells. - public IEnumerable Cells { get; private set; } - - /// - /// Creates the Delaunay triangulation of the input data. - /// - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// DelaunayTriangulation<TVertex, TCell>. - /// data - /// data - public static DelaunayTriangulation Create(IList data, - float PlaneDistanceTolerance)// = Constants.DefaultPlaneDistanceTolerance) - { - if (data == null) throw new ArgumentNullException("data"); - if (data.Count == 0) return new DelaunayTriangulation { Cells = new TCell[0] }; - - var cells = ConvexHullAlgorithm.GetDelaunayTriangulation(data, PlaneDistanceTolerance); - - return new DelaunayTriangulation { Cells = cells }; - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs.meta deleted file mode 100644 index fc2ba273c..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/DelaunayTriangulation.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 4b0e872e78d2ab8428b446cfa5c06580 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs deleted file mode 100644 index 63e9d1f5a..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs +++ /dev/null @@ -1,51 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -namespace MIConvexHull -{ - /// - /// Representation of the triangulation cell. Pretty much the same as ConvexFace, - /// just wanted to distinguish the two. - /// To declare your own face type, use class Face : DelaunayFace(of Vertex, of Face) - /// - /// The type of the t vertex. - /// The type of the t cell. - /// - public abstract class TriangulationCell : ConvexFace - where TVertex : IVertex - where TCell : ConvexFace - { - } - - /// - /// Default triangulation cell. - /// - /// The type of the t vertex. - public class DefaultTriangulationCell : TriangulationCell> - where TVertex : IVertex - { - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs.meta deleted file mode 100644 index 29dcef792..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/TriangulationCell.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 808182249e0e66143b65945d9aeb37fe -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs deleted file mode 100644 index 1557352d6..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs +++ /dev/null @@ -1,93 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -namespace MIConvexHull -{ - /// - /// A class representing an (undirected) edge of the Voronoi graph. - /// - /// The type of the t vertex. - /// The type of the t cell. - public class VoronoiEdge - where TVertex : IVertex - where TCell : TriangulationCell - { - /// - /// Create an instance of the edge. - /// - public VoronoiEdge() - { - } - - /// - /// Create an instance of the edge. - /// - /// The source. - /// The target. - public VoronoiEdge(TCell source, TCell target) - { - Source = source; - Target = target; - } - - /// - /// Source of the edge. - /// - /// The source. - public TCell Source { get; internal set; } - - /// - /// Target of the edge. - /// - /// The target. - public TCell Target { get; internal set; } - - /// - /// ... - /// - /// The object to compare with the current object. - /// true if the specified is equal to this instance; otherwise, false. - public override bool Equals(object obj) - { - var other = obj as VoronoiEdge; - if (other == null) return false; - if (ReferenceEquals(this, other)) return true; - return (Source == other.Source && Target == other.Target) - || (Source == other.Target && Target == other.Source); - } - - /// - /// ... - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - public override int GetHashCode() - { - var hash = 23; - hash = hash*31 + Source.GetHashCode(); - return hash*31 + Target.GetHashCode(); - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs.meta deleted file mode 100644 index 71c57ddb7..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiEdge.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 2f5c388c78c121a47b14c2f4ed26af38 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs deleted file mode 100644 index 3142af854..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs +++ /dev/null @@ -1,203 +0,0 @@ -/****************************************************************************** - * - * The MIT License (MIT) - * - * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - *****************************************************************************/ - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace MIConvexHull -{ - /// - /// A factory class for creating a Voronoi mesh. - /// - public static class VoronoiMesh - { - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// The type of the t edge. - /// The data. - /// VoronoiMesh<TVertex, TCell, TEdge>. - public static VoronoiMesh Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TCell : TriangulationCell, new() - where TVertex : IVertex - where TEdge : VoronoiEdge, new() - { - return VoronoiMesh.Create(data, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The data. - /// VoronoiMesh<TVertex, DefaultTriangulationCell<TVertex>, VoronoiEdge<TVertex, DefaultTriangulationCell<TVertex>>>. - public static - VoronoiMesh - , VoronoiEdge>> - Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - { - return - VoronoiMesh - , VoronoiEdge> - >.Create(data, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The data. - /// VoronoiMesh<DefaultVertex, DefaultTriangulationCell<DefaultVertex>, VoronoiEdge<DefaultVertex, DefaultTriangulationCell<DefaultVertex>>>. - public static - VoronoiMesh - , - VoronoiEdge>> - Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - { - var points = data.Select(p => new DefaultVertex { Position = p.ToArray() }).ToList(); - return - VoronoiMesh - , - VoronoiEdge>>.Create(points, PlaneDistanceTolerance); - } - - /// - /// Create the voronoi mesh. - /// - /// The type of the t vertex. - /// The type of the t cell. - /// The data. - /// VoronoiMesh<TVertex, TCell, VoronoiEdge<TVertex, TCell>>. - public static VoronoiMesh> Create( - IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - where TVertex : IVertex - where TCell : TriangulationCell, new() - { - return VoronoiMesh>.Create(data, PlaneDistanceTolerance); - } - } - - /// - /// A representation of a voronoi mesh. - /// - /// The type of the t edge. - /// The type of the t vertex. - /// The type of the t cell. - public class VoronoiMesh - where TCell : TriangulationCell, new() - where TVertex : IVertex - where TEdge : VoronoiEdge, new() - { - /// - /// Can only be created using a factory method. - /// - private VoronoiMesh() - { - } - - /// - /// Vertices of the diagram. - /// - /// The vertices. - public IEnumerable Vertices { get; private set; } - - /// - /// Edges connecting the cells. - /// The same information can be retrieved Cells' Adjacency. - /// - /// The edges. - public IEnumerable Edges { get; private set; } - - /// - /// Create a Voronoi diagram of the input data. - /// - /// The data. - /// The plane distance tolerance (default is 1e-10). If too high, points - /// will be missed. If too low, the algorithm may break. Only adjust if you notice problems. - /// VoronoiMesh<TVertex, TCell, TEdge>. - /// data - /// data - public static VoronoiMesh Create(IList data, - float PlaneDistanceTolerance = Constants.DefaultPlaneDistanceTolerance) - { - if (data == null) throw new ArgumentNullException("data"); - - var t = DelaunayTriangulation.Create(data, PlaneDistanceTolerance); - var vertices = t.Cells.ToList(); - var edges = new HashSet(new EdgeComparer()); - - foreach (var f in vertices) - { - for (var i = 0; i < f.Adjacency.Length; i++) - { - var af = f.Adjacency[i]; - if (af != null) edges.Add(new TEdge { Source = f, Target = af }); - } - } - - return new VoronoiMesh - { - Vertices = vertices, - Edges = edges.ToList() - }; - } - - /// - /// This is probably not needed, but might make things a tiny bit faster. - /// - /// - private class EdgeComparer : IEqualityComparer - { - /// - /// Equals the specified x. - /// - /// The x. - /// The y. - /// System.Boolean. - public bool Equals(TEdge x, TEdge y) - { - return (x.Source == y.Source && x.Target == y.Target) || (x.Source == y.Target && x.Target == y.Source); - } - - /// - /// Returns a hash code for this instance. - /// - /// The for which a hash code is to be returned. - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - public int GetHashCode(TEdge obj) - { - return obj.Source.GetHashCode() ^ obj.Target.GetHashCode(); - } - } - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs.meta deleted file mode 100644 index c87b2415b..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Triangulation/VoronoiMesh.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: fbb5750f20479844eb4e504302755db9 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs deleted file mode 100644 index e00c7deef..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections; -using MIConvexHull; -using CultMath; -using static CultMath.math; - -public class Vertex2 : IVertex -{ - public float[] Position => new[] {StoredPosition.x, StoredPosition.y}; - - public float2 StoredPosition; - - public float x { get { return StoredPosition.x; } } - public float y { get { return StoredPosition.y; } } - - public Vertex2(float x, float y) - { - StoredPosition = float2(x, y); - } - - public Vertex2(float2 pos) - { - StoredPosition = pos; - } - - public float2 ToFloat2() { - return StoredPosition; - } - - public float Distance(Vertex2 v) - { - return length(StoredPosition-v.StoredPosition); - } -} diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs.meta deleted file mode 100644 index a314ce4ec..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 67d3ec22ea52a2642a14ff864e3017b0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs deleted file mode 100644 index a266ddaec..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs +++ /dev/null @@ -1,16 +0,0 @@ -using CultMath; - -public class Vertex2 : Vertex2 -{ - public T StoredObject { get; } - - public Vertex2(float x, float y, T obj) : base(x, y) - { - StoredObject = obj; - } - - public Vertex2(float2 pos, T obj) : base(pos) - { - StoredObject = obj; - } -} \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs.meta deleted file mode 100644 index 24639c89b..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex2Object.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: e42612a273ec490380eaa67bed216a6d -timeCreated: 1615418804 \ No newline at end of file diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs deleted file mode 100644 index a14aea5b1..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections; -using MIConvexHull; -using CultMath; -using static CultMath.math; - -public class Vertex3 : IVertex -{ - public float[] Position => new[] {StoredPosition.x, StoredPosition.y}; - - public float3 StoredPosition; - - public Vertex3(float x, float y, float z) - { - StoredPosition = float3(x, y, z); - } - - public Vertex3(float3 pos) - { - StoredPosition = pos; - } - - public float3 ToFloat3() { - return StoredPosition; - } - - public float Distance(Vertex3 v) - { - return length(StoredPosition-v.StoredPosition); - } -} diff --git a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs.meta b/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs.meta deleted file mode 100644 index fc1115c36..000000000 --- a/Assets/Scripts/ServerShared/NIH/MIConvexHull/Vertex3.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 3553f231414f28347996a803a79425fa -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index 6a70f9a43..ed7756a5a 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -282,25 +282,14 @@ public sealed class RestoredHullsTests var background = TutorialBackgroundSettings(); var names = TutorialNameSettings(); var gameData = Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc"); - - // Galaxy zone-graph generation has a pre-existing, seed-dependent flakiness of its own (a disconnected - // zone graph throws inside the Galaxy constructor on roughly 1 seed in 15, confirmed unrelated to this - // cut: it reproduces identically against the pre-fix-batch catalog). That failure belongs to Galaxy.cs, - // not to ZoneGenerator's entrance-station override this test is pinning, so it is skipped rather than - // asserted on; every seed whose Galaxy actually builds must still get a docked entrance station. - var succeeded = 0; var sawGenuineLagrangeOrbit = false; for (uint seed = 1; seed <= 25; seed++) { var scratchRun = Path.Combine(Path.GetTempPath(), $"aetheria-entrance-station-{Guid.NewGuid():N}.cc"); try { - // Galaxy mutates Faction.InfluenceDistance in place; a fresh catalog open per seed keeps runs independent. using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - Galaxy galaxy; - try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); } - catch (Exception) { continue; } - succeeded++; + var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true); @@ -317,8 +306,6 @@ public sealed class RestoredHullsTests if (File.Exists(scratchRun)) File.Delete(scratchRun); } } - - Assert.True(succeeded >= 15, $"only {succeeded} of 25 seeds produced a Galaxy at all; too few runs to trust this result."); Assert.True(sawGenuineLagrangeOrbit, "none of the 25 seeds seated the entrance station on a genuine (non-rosette) Lagrange orbit."); } @@ -336,8 +323,6 @@ public sealed class RestoredHullsTests var background = TutorialBackgroundSettings(); var names = TutorialNameSettings(); var gameData = Path.Combine(FindRepoRoot(), "GameData", "Aetheria.cc"); - - var succeeded = 0; var sawGenuineCandidate = false; for (uint seed = 1; seed <= 25; seed++) { @@ -345,10 +330,7 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - Galaxy galaxy; - try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); } - catch (Exception) { continue; } - succeeded++; + var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true); @@ -369,8 +351,6 @@ public sealed class RestoredHullsTests if (File.Exists(scratchRun)) File.Delete(scratchRun); } } - - Assert.True(succeeded >= 15, $"only {succeeded} of 25 seeds produced a Galaxy at all; too few runs to trust this result."); Assert.True(sawGenuineCandidate, "none of the 25 seeds had a genuine Lagrange candidate at all; too few runs to trust this result."); } @@ -477,9 +457,7 @@ public sealed class RestoredHullsTests try { using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun); - Galaxy galaxy; - try { galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); } - catch (Exception) { continue; } + var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed); var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { }); var pack = ZoneGenerator.GenerateZone(items, zoneSettings, galaxy, galaxy.Entrance, isTutorial: true); diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index cebf564d6..ac57abce9 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -171,6 +171,65 @@ public sealed partial class RunStartTests Assert.All(authored, pair => Assert.Equal(pair.Value, pair.Key.InfluenceDistance)); } + // Every zone is linked into one graph. These seeds left zones with no link at all (the vendored float-precision + // convex-hull triangulation dropped them), so the constructor threw KeyNotFoundException placing homes (prelude) + // or the entrance (main). Each galaxy's links are a subset of its zone positions' Delaunay edges, and the + // triangulation agrees with a brute-force empty-circumcircle oracle. + [Theory] + [InlineData(103u, true)] + [InlineData(134u, true)] + [InlineData(137u, true)] + [InlineData(173u, true)] + [InlineData(227u, true)] + [InlineData(285u, true)] + [InlineData(34u, false)] + public void EveryZoneIsLinked(uint seed, bool prelude) + { + var galaxy = RunStart.Generate(new Scripted(true, _ => { }, seed, stage => prelude ? stage.Prelude() : stage.Main()), Inputs()); + Assert.All(galaxy.Zones, zone => Assert.Equal(galaxy.Zones.Length, zone.Distance.Count)); + + var positions = galaxy.Zones.Select(zone => zone.Position).ToArray(); + var edges = Delaunay.Edges(positions); + Assert.Equal(BruteForceDelaunay(positions), edges); + var delaunay = new HashSet<(int, int)>(edges); + for (var i = 0; i < galaxy.Zones.Length; i++) + foreach (var adjacent in galaxy.Zones[i].AdjacentZones) + { + var j = Array.IndexOf(galaxy.Zones, adjacent); + Assert.Contains((Math.Min(i, j), Math.Max(i, j)), delaunay); + } + } + + // Every triangle whose circumcircle holds no other point, as sorted edges. + private static List<(int a, int b)> BruteForceDelaunay(float2[] points) + { + var edges = new SortedSet<(int a, int b)>(); + for (var i = 0; i < points.Length; i++) + for (var j = i + 1; j < points.Length; j++) + for (var k = j + 1; k < points.Length; k++) + { + double ax = points[i].x, ay = points[i].y, bx = points[j].x, by = points[j].y, cx = points[k].x, cy = points[k].y; + var d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); + if (d == 0) continue; + double a2 = ax * ax + ay * ay, b2 = bx * bx + by * by, c2 = cx * cx + cy * cy; + var ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d; + var uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d; + var r2 = (ax - ux) * (ax - ux) + (ay - uy) * (ay - uy); + var empty = true; + for (var m = 0; m < points.Length && empty; m++) + if (m != i && m != j && m != k) + { + double dx = points[m].x - ux, dy = points[m].y - uy; + empty = dx * dx + dy * dy >= r2; + } + if (!empty) continue; + edges.Add((i, j)); + edges.Add((j, k)); + edges.Add((i, k)); + } + return edges.ToList(); + } + // A scenario naming one unknown design admits nothing, not even the parts that built, and names the design. [Fact] public void StagingIsAllOrNothing() From 4d1334e044a20a6c6bee7fc9b0e5294d5c3e8e41 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 21:59:17 +0200 Subject: [PATCH 08/12] Arcs runs at seed 103 again, as the spec sets it: its prelude galaxy generates now that every zone is linked Reverts 0854d91d. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index cd7eec885..d1ceb61e0 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -6,7 +6,7 @@ public sealed class Arcs : Scenario public override string Name => "Arcs"; public override string Brief => "Forward mounts, a bare hull off the bow, one off the stern, a hostile turret. Verify: the bow " + "hull can be hit, nothing fires at the stern one through the ship, the turret tracks you all the way round."; - public override uint Seed => 107; + public override uint Seed => 103; public override bool Ambient => false; // Where the targets sit: the player is at the origin facing +z, and every mount fires forward. From 02e7fd4b7e308d1ac05fc436618d074886a04f28 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 22:01:17 +0200 Subject: [PATCH 09/12] 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 --- Assets/Scripts/ServerShared/Galaxy.cs | 10 +++- Assets/Scripts/ServerShared/RunStart.cs | 15 +++++- Assets/Scripts/ServerShared/Scenario.cs | 3 +- Assets/Scripts/ServerShared/ScenarioStage.cs | 4 +- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 55 +++++++++++++++----- 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/Assets/Scripts/ServerShared/Galaxy.cs b/Assets/Scripts/ServerShared/Galaxy.cs index bcdf7a2f8..a5660f52e 100644 --- a/Assets/Scripts/ServerShared/Galaxy.cs +++ b/Assets/Scripts/ServerShared/Galaxy.cs @@ -28,6 +28,10 @@ public class Galaxy public Dictionary FactionRelationships { get; } = new Dictionary(); private Action Log { get; } public bool IsPrelude { get; } + + // 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; } private HashSet _containedFactions; private GalaxyZone[] _exitPath; @@ -104,7 +108,8 @@ public class Galaxy Background = background; Log = log; var factions = cache.GetAll(); - var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed); + Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed; + var random = new Random(Seed); Factions = factions.OrderBy(x => random.NextFloat()).Take(settings.MegaCount).ToArray(); foreach (var f in Factions) FactionRelationships[f] = FactionRelationship.Neutral; @@ -159,7 +164,8 @@ public class Galaxy Background = background; Log = log; - var random = new Random(seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed); + Seed = seed == 0 ? (uint) (DateTime.Now.Ticks % uint.MaxValue) : seed; + var random = new Random(Seed); var factions = new List(); diff --git a/Assets/Scripts/ServerShared/RunStart.cs b/Assets/Scripts/ServerShared/RunStart.cs index 1eab125bb..4397f4575 100644 --- a/Assets/Scripts/ServerShared/RunStart.cs +++ b/Assets/Scripts/ServerShared/RunStart.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Random = CultMath.Random; // The owner of a new run (docs/scenarios-cut.md, R.4). Every new run is a scenario: RunStart generates its galaxy, // generates its arena (the galaxy's entrance zone) under the scenario's Ambient rule, and admits what the scenario @@ -21,9 +22,12 @@ public static class RunStart return scenario.Generate(stage) ?? throw new InvalidOperationException($"Scenario {scenario.Name} generated no galaxy."); } - public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario) => - galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone( + public static ZonePack GenerateArena(ItemManager itemManager, ZoneGenerationSettings zoneSettings, Galaxy galaxy, Scenario scenario) + { + SeedItems(itemManager, galaxy, 1); + return galaxy.Entrance.PackedContents = ZoneGenerator.GenerateZone( itemManager, zoneSettings, galaxy, galaxy.Entrance, galaxy.IsPrelude, scenario.Ambient); + } // Runs the scenario's Stage, then admits its player and everything it placed: each entity's stance set both ways // with the player, a pilot for each piloted one. Returns null, with every failure listed and the arena unchanged, @@ -31,6 +35,7 @@ public static class RunStart public static Staged Stage(ItemManager itemManager, Zone arena, Scenario scenario, string startingHull, TutorialGenerationSettings tutorialGenerationSettings, List failures) { + SeedItems(itemManager, arena.Galaxy, 2); var stage = new ScenarioStage(itemManager, arena, startingHull, tutorialGenerationSettings); scenario.Stage(stage); var reported = failures.Count; @@ -51,4 +56,10 @@ public static class RunStart } return new Staged { Player = player, Entities = stage.Placed.Select(placed => placed.Entity).ToList() }; } + + // A new run's item draws (each lot's quality, each generated fit) come from its galaxy's seed, not the process clock, + // so a fixed seed fixes the arena's gear and the player's fit. Each step draws its own stream, so whatever runs + // between them (populating the level) cannot shift it. + private static void SeedItems(ItemManager itemManager, Galaxy galaxy, uint step) => + itemManager.Random = new Random(galaxy.Seed * 0x9E3779B1u + step); } diff --git a/Assets/Scripts/ServerShared/Scenario.cs b/Assets/Scripts/ServerShared/Scenario.cs index ba7165a19..acd2f10b2 100644 --- a/Assets/Scripts/ServerShared/Scenario.cs +++ b/Assets/Scripts/ServerShared/Scenario.cs @@ -10,7 +10,8 @@ public abstract class Scenario public abstract string Brief { get; } // Zero draws the seed from the clock, so each launch is a new galaxy. Anything else fixes the galaxy, its - // background and its arena, so every launch has the same layout. + // background, its arena and every fit, each item's maker and workmanship included, so every launch has the same + // layout. public virtual uint Seed => 0; // False: the arena keeps its planets, orbits and stations but generates no ship or turret, so nothing wanders into diff --git a/Assets/Scripts/ServerShared/ScenarioStage.cs b/Assets/Scripts/ServerShared/ScenarioStage.cs index 241684a35..feac93e66 100644 --- a/Assets/Scripts/ServerShared/ScenarioStage.cs +++ b/Assets/Scripts/ServerShared/ScenarioStage.cs @@ -64,8 +64,8 @@ public sealed class ScenarioStage // A hull with nothing fitted. public ScenarioFit Bare(string hull) => Fit(hull); - // A fit generated as the game generates its ships, for the faction when one is given. An empty hull means any - // ship hull. + // A fit generated as the game generates its ships, for the faction when one is given, from the run's item draws, + // which RunStart seeds from the galaxy's seed. An empty hull means any ship hull. public ScenarioFit Generated(string hull, Faction faction = null) { var generator = new LoadoutGenerator(ref _items.Random, _items, Galaxy, Arena.GalaxyZone, faction, 2); diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index ac57abce9..9fbd9e7e6 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -132,24 +132,55 @@ public sealed partial class RunStartTests Assert.Contains(main.arena.Pack.Entities, entity => entity is ShipPack { IsPlayerShip: false }); } + // Everything a galaxy's layout fixes: each zone's name, position, owner, factions and links, the homes, the + // entrance, the exit and the background. private static string Layout(Galaxy galaxy) => - string.Join(";", galaxy.Zones.Select(zone => $"{zone.Name}@{zone.Position}:{zone.Owner?.Name}")) + - $" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} noise {galaxy.Background.NoisePosition}"; + string.Join(";", galaxy.Zones.Select(zone => + $"{zone.Name}@{zone.Position}:{zone.Owner?.Name}:[{string.Join(",", zone.Factions.Select(faction => faction.Name))}]" + + $":[{string.Join(",", zone.AdjacentZones.Select(adjacent => Array.IndexOf(galaxy.Zones, adjacent)).OrderBy(i => i))}]")) + + $" homes {string.Join(",", galaxy.HomeZones.Select(home => $"{home.Key.Name}={Array.IndexOf(galaxy.Zones, home.Value)}").OrderBy(home => home))}" + + $" entrance {Array.IndexOf(galaxy.Zones, galaxy.Entrance)} exit {Array.IndexOf(galaxy.Zones, galaxy.Exit)} noise {galaxy.Background.NoisePosition}"; - private static string Layout(ZonePack arena) => - $"{arena.Radius} {arena.Mass} {arena.Planets.Count()} {arena.Orbits.Count()} {arena.Entities.Count()}"; + // An arena's bodies and every entity in it, down to each item's design, maker, workmanship and role fills. + private string Layout(ZonePack arena) => + $"{arena.Radius} {arena.Mass} planets [{string.Join(",", arena.Planets.Select(planet => _cache.Get(planet)).Select(body => $"{body.GetType().Name}:{body.Mass}"))}]" + + $" orbits [{string.Join(",", arena.Orbits.Select(orbit => _cache.Get(orbit).Distance))}]" + + $" entities [{string.Join(",", arena.Entities.Select(Gear))}]"; - // A fixed seed fixes the galaxy, its background and its arena, launch after launch in one session. Seed zero takes - // the clock, so two launches at two clock readings are two galaxies. + // What a launch staged: the player and everything placed, each where it is and with exactly what it carries. + private string Fits(RunStart.Staged staged) => + string.Join(" | ", staged.Entities.Prepend(staged.Player).Select(entity => $"{entity.Name}@{entity.Position}:{Gear(EntitySerializer.Pack(entity))}")); + + private string Gear(EntityPack pack) => + $"{pack.GetType().Name}{(pack is ShipPack ship ? "@" + ship.Position : "")}:{string.Join("/", EntitySerializer.Items(pack).Select(Item))}"; + + private string Item(ItemInstance item) + { + var name = _items.GetData(item)?.Name; + if (!(item is CraftedItemInstance crafted)) return name; + var lot = _items.GetLot(crafted); + return $"{name}<{(lot.Origin as Attributed)?.Faction.Key.Value}>~{lot.Quality}" + + string.Concat((lot.Roles ?? new List()).Select(fill => $"/{fill.Role}={fill.Quality}")); + } + + // A fixed seed fixes the galaxy, its background, its arena and every fit, each item's maker and workmanship + // included, launch after launch in one session, whatever ran in between. Seed zero takes the clock, so two launches + // at two clock readings are two galaxies. [Fact] public void SeedRules() { - var arena = new DjinniShakedown(); - Assert.NotEqual(0u, arena.Seed); - var first = Launch(arena); - var second = Launch(arena); - Assert.Equal(Layout(first.galaxy), Layout(second.galaxy)); - Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack)); + foreach (var scenario in Scenarios.Tests) + { + Assert.NotEqual(0u, scenario.Seed); + var first = Launch(scenario); + Launch(new MainGalaxy(), Inputs(() => 4242)); + _items.Random = new CultMath.Random(0xC0FFEE); // play draws from the run's item random + var second = Launch(scenario); + Assert.True(first.failures.Count == 0, $"{scenario.Name}: {string.Join("; ", first.failures)}"); + Assert.Equal(Layout(first.galaxy), Layout(second.galaxy)); + Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack)); + Assert.Equal(Fits(first.staged), Fits(second.staged)); + } var clock = new Queue(new[] { 1000u, 2000u }); var inputs = Inputs(() => clock.Dequeue()); From 8a1e205cf069e86c1bc570fa4c52208a104914ea Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 22:03:50 +0200 Subject: [PATCH 10/12] A new run replaces the saved run only once it has staged; a failed start says why and leaves the saved run Launch cleared the run store before generating, and a generation or staging failure only reached the log: the Generating Galaxy dialog stayed up, StartGame threw, and the saved run was already gone. RunSave.Replace runs a start with the saved run still stored: when it returns a staged run the saved run's records go, and when it returns null or throws, only the records it wrote (the arena's orbits and bodies) go. StartGame's new-run branch is StartScenario, chosen by PendingScenario rather than by the absence of a SavedGame; it mints into an empty ledger, and a failed start clears CurrentGalaxy (so quitting saves nothing) and refuses on the main menu with every failure. Launch no longer clears; a galaxy that throws is refused on the title menu. The menu offers Continue whenever no run is live, which replaces MainMenu.InGame (cut, with its prefab value and scene override), so a failed start can resume the saved run in place. Co-Authored-By: Claude Opus 5.5 --- Assets/Prefabs/UI/Main Menu Canvas.prefab | 1 - Assets/Scenes/ARPG.unity | 5 -- Assets/Scripts/Gameplay/ActionGameManager.cs | 55 ++++++++++++++------ Assets/Scripts/ServerShared/SavedGame.cs | 28 ++++++++-- Assets/Scripts/UI/MainMenu.cs | 31 ++++++----- tests/Aetheria.Shared.Tests/RunSaveTests.cs | 50 ++++++++++++++++++ 6 files changed, 132 insertions(+), 38 deletions(-) diff --git a/Assets/Prefabs/UI/Main Menu Canvas.prefab b/Assets/Prefabs/UI/Main Menu Canvas.prefab index a03628ba0..866d5fb33 100644 --- a/Assets/Prefabs/UI/Main Menu Canvas.prefab +++ b/Assets/Prefabs/UI/Main Menu Canvas.prefab @@ -915,7 +915,6 @@ MonoBehaviour: m_EditorClassIdentifier: Settings: {fileID: 11400000, guid: cd2344e2001a37544aab0960c164d4ff, type: 2} Dialog: {fileID: 4533226043449861657} - InGame: 0 PanelPrototype: {fileID: 7329274282942065594} FadeTime: 0.5 FadeDistance: 1024 diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index 1ef50e0ea..ad5c3d536 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -9636,11 +9636,6 @@ PrefabInstance: propertyPath: m_SizeDelta.y value: 90.91 objectReference: {fileID: 0} - - target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99, - type: 3} - propertyPath: InGame - value: 1 - objectReference: {fileID: 0} - target: {fileID: 4533226043457584178, guid: 3d8d9cbf10b12214e98891c1f03c1e99, type: 3} propertyPath: CloudRenderer diff --git a/Assets/Scripts/Gameplay/ActionGameManager.cs b/Assets/Scripts/Gameplay/ActionGameManager.cs index 8ebcf413a..422ad2327 100644 --- a/Assets/Scripts/Gameplay/ActionGameManager.cs +++ b/Assets/Scripts/Gameplay/ActionGameManager.cs @@ -270,7 +270,9 @@ public class ActionGameManager : MonoBehaviour EntityInstance.EffectManagerParent = EffectManagerParent; ConsoleController.MessageReceiver = this; - ItemManager = new ItemManager(CultCache, RunSave.Lots(CultCache), Settings.GameplaySettings, Debug.Log); + // A new run mints into an empty ledger; the saved run's stays in the store until the new run has started. + ItemManager = new ItemManager(CultCache, PendingScenario != null ? new ProvenanceLedger() : RunSave.Lots(CultCache), + Settings.GameplaySettings, Debug.Log); ZoneRenderer.ItemManager = ItemManager; // If hiding minimap asteroids, turn them off to start with @@ -795,22 +797,10 @@ public class ActionGameManager : MonoBehaviour { if (CurrentGalaxy != null) { - var saved = CultCache.GetGlobal(); - if (saved == null) - { - var scenario = PendingScenario ?? throw new InvalidOperationException("A new run needs a scenario; MainMenu.Launch sets one."); - PendingScenario = null; - SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance)); - // RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this only enters and binds. - RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, scenario); - PopulateLevel(CurrentGalaxy.Entrance); - var failures = new List(); - var staged = RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures) - ?? throw new InvalidOperationException($"Scenario {scenario.Name} could not stage: {string.Join("; ", failures)}"); - BindToEntity(staged.Player); - } + if (PendingScenario != null) StartScenario(); else { + var saved = CultCache.GetGlobal(); foreach(var group in CurrentGalaxy.DiscoveredZones .GroupBy(dz=>dz.Distance[CurrentGalaxy.Entrance])) SectorMap.QueueZoneReveal(group); @@ -835,6 +825,41 @@ public class ActionGameManager : MonoBehaviour } } + // RunStart owns what a new run starts with (docs/scenarios-cut.md, R.4); this enters and binds. The saved run is + // replaced only once the scenario has staged. A start that fails leaves the saved run as it was and says why on + // the main menu, where Continue still resumes the saved run. + private void StartScenario() + { + var scenario = PendingScenario; + PendingScenario = null; + var failures = new List(); + RunStart.Staged staged = null; + try + { + staged = RunSave.Replace(CultCache, () => + { + RunStart.GenerateArena(ItemManager, Settings.ZoneSettings, CurrentGalaxy, scenario); + PopulateLevel(CurrentGalaxy.Entrance); + return RunStart.Stage(ItemManager, Zone, scenario, Settings.StartingHullName, Settings.TutorialGenerationSettings, failures); + }); + } + catch (Exception exception) + { + Debug.LogException(exception); + failures.Add(exception.Message); + } + + if (staged == null) + { + CurrentGalaxy = null; + MainMenu.gameObject.SetActive(true); + MainMenu.Refuse($"{scenario.Name} could not start", string.Join("\n", failures)); + return; + } + SectorMap.QueueZoneReveal(CurrentGalaxy.Entrance.AdjacentZones.Prepend(CurrentGalaxy.Entrance)); + BindToEntity(staged.Player); + } + private IEnumerator IntroCutscene(Ship ship) { ZoneRenderer.PerspectiveEntity = ship; diff --git a/Assets/Scripts/ServerShared/SavedGame.cs b/Assets/Scripts/ServerShared/SavedGame.cs index a674df81d..87bbeda60 100644 --- a/Assets/Scripts/ServerShared/SavedGame.cs +++ b/Assets/Scripts/ServerShared/SavedGame.cs @@ -155,17 +155,37 @@ public static class RunSave } // Removes every run record (SavedGame, SavedZone, OrbitData, BodyData, ProvenanceLedger) in one Commit. - public static void Clear(CultCache cache) + public static void Clear(CultCache cache) => Remove(cache, Records(cache)); + + // A new run replaces the saved one only once it has started: start runs with the saved run still in the store. + // When it returns a value, the saved run's records go. When it returns null or throws, the records it wrote go and + // the saved run stays as it was. + public static T Replace(CultCache cache, Func start) where T : class { - var run = cache.AllStoredDocuments + var saved = Records(cache); + T started = null; + try + { + started = start(); + } + finally + { + Remove(cache, started != null ? saved : Records(cache).Except(saved).ToArray()); + } + return started; + } + + private static CultRecordKey[] Records(CultCache cache) => + cache.AllStoredDocuments .Where(stored => IsRunRecord(stored.Descriptor.DocumentType)) .Select(stored => stored.Key) .ToArray(); + + private static void Remove(CultCache cache, CultRecordKey[] records) => cache.Commit(batch => { - foreach (var key in run) batch.Remove(key); + foreach (var key in records) batch.Remove(key); }); - } public static bool IsRunRecord(Type documentType) => AetheriaStores.RunTypes.Any(home => home.IsAssignableFrom(documentType)); diff --git a/Assets/Scripts/UI/MainMenu.cs b/Assets/Scripts/UI/MainMenu.cs index b5bdab2ee..f20208ff7 100644 --- a/Assets/Scripts/UI/MainMenu.cs +++ b/Assets/Scripts/UI/MainMenu.cs @@ -17,7 +17,6 @@ public class MainMenu : MonoBehaviour public VolumeCloudRenderer CloudRenderer; public GameSettings Settings; public ConfirmationDialog Dialog; - public bool InGame; public Prototype PanelPrototype; public float FadeTime = .5f; public float FadeDistance = 512; @@ -92,9 +91,10 @@ public class MainMenu : MonoBehaviour { _nextMenu.panel.Clear(); _nextMenu.panel.Title.text = TitleSubtitle("aetheria", "terminus"); - if (!InGame) + if (ActionGameManager.CurrentGalaxy == null) { - // A run exists only as the run store's SavedGame global. + // No run is live: at the title, after death, or after a new run failed to start. A saved run exists only as + // the run store's SavedGame global. var cache = ActionGameManager.CultCache; if (ActionGameManager.ModExclusions.Length > 0 && !_exclusionsShown) { @@ -156,11 +156,11 @@ public class MainMenu : MonoBehaviour } } - // The only way into a new run: clears the saved run, generates the scenario's galaxy off the main thread, and - // enters the game, where StartGame has RunStart stage it. + // The only way into a new run: generates the scenario's galaxy off the main thread and enters the game, where + // StartGame has RunStart stage it and only then replaces the saved run. A galaxy that cannot generate is refused + // here, with the saved run untouched. private void Launch(Scenario scenario) { - RunSave.Clear(ActionGameManager.CultCache); var generatorState = "Loading Database Contents"; Fade(true); @@ -181,16 +181,21 @@ public class MainMenu : MonoBehaviour ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"), Debug.Log, state => generatorState = state); - Task.Run(() => - { - var galaxy = RunStart.Generate(scenario, stage); + Task.Run(() => RunStart.Generate(scenario, stage)).ContinueWith(generation => Observable.NextFrame().Subscribe(_ => { - ActionGameManager.CurrentGalaxy = galaxy; + if (generation.IsFaulted) + { + Debug.LogException(generation.Exception); + ShowMain(); + Fade(false); + Refuse($"{scenario.Name} could not generate its galaxy", generation.Exception.GetBaseException().Message); + return; + } + ActionGameManager.CurrentGalaxy = generation.Result; ActionGameManager.PendingScenario = scenario; EnterGame(); - }); - }).ContinueWith(t => Debug.LogException(t.Exception), TaskContinuationOptions.OnlyOnFaulted); + })); } // The one way into the game scene. Mod ship prototypes import asynchronously at boot, so the scene waits for them, @@ -209,7 +214,7 @@ public class MainMenu : MonoBehaviour SceneManager.LoadScene("ARPG"); } - private void Refuse(string title, string reason) + public void Refuse(string title, string reason) { Debug.LogError($"{title}: {reason}"); Dialog.Clear(); diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 6ce426995..51db6b137 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -126,6 +126,56 @@ public sealed class RunSaveTests : IDisposable Assert.Equal(before, new[] { Catalog, Player }.Select(Hash).ToArray()); } + // A new run's start writes its arena's orbits and bodies while the saved run is still stored. A start that fails, + // by returning nothing or by throwing, takes away only what it wrote; one that starts takes away the saved run. + [Fact] + public void ANewRunReplacesTheSavedRunOnlyOnceItHasStarted() + { + using (var cache = Open()) + RunSave.Commit(cache, Game(cache), Zones(3, StageZoneContents(cache), 0), new ProvenanceLedger()); + string[] saved; + using (var cache = Open()) + saved = RunKeys(cache); + Assert.Equal(8, saved.Length); + + using (var cache = Open()) + Assert.Null(RunSave.Replace(cache, () => + { + StageZoneContents(cache); + return null; + })); + using (var cache = Open()) + { + Assert.Equal(saved, RunKeys(cache)); + Assert.NotNull(cache.GetGlobal()); + } + + using (var cache = Open()) + Assert.Throws(() => RunSave.Replace(cache, () => + { + StageZoneContents(cache); + throw new InvalidOperationException("the arena would not generate"); + })); + using (var cache = Open()) + Assert.Equal(saved, RunKeys(cache)); + + ZonePack started; + using (var cache = Open()) + started = RunSave.Replace(cache, () => StageZoneContents(cache)); + using (var cache = Open()) + { + Assert.Null(cache.GetGlobal()); + Assert.Equal(started.Orbits.Select(orbit => orbit.Key.Value).Concat(started.Planets.Select(body => body.Key.Value)) + .OrderBy(key => key, StringComparer.Ordinal), RunKeys(cache)); + } + } + + private static string[] RunKeys(CultCache cache) => cache.AllStoredDocuments + .Where(stored => RunSave.IsRunRecord(stored.Descriptor.DocumentType)) + .Select(stored => stored.Key.Value) + .OrderBy(key => key, StringComparer.Ordinal) + .ToArray(); + // Lot 1 sits on the root ship's hull; lot 3 (Produced from facility 4, input 5) sits in a docked child's // docking-bay contents; lots 2 and 6 are minted but referenced by nothing packed. Only the reachable closure // {1, 3, 4, 5} survives into the written copy; the live ledger keeps every lot. From d44df96fb0b5e83ab9ca802c3e540b3272282d7b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 22:04:17 +0200 Subject: [PATCH 11/12] SeedRules scrambles the item random between the arena and staging, so the staging step's own seed is pinned Co-Authored-By: Claude Opus 5.5 --- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index 9fbd9e7e6..ddb75aa8e 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -59,11 +59,13 @@ public sealed partial class RunStartTests _cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, clock); } - // What MainMenu.Launch and StartGame do with a scenario: its galaxy, its arena, then staging. - private (Galaxy galaxy, Zone arena, RunStart.Staged staged, List failures) Launch(Scenario scenario, GalaxyStage inputs = null) + // What MainMenu.Launch and StartGame do with a scenario: its galaxy, its arena, then staging. beforeStaging stands in + // for whatever the game runs between the arena and staging. + private (Galaxy galaxy, Zone arena, RunStart.Staged staged, List failures) Launch(Scenario scenario, GalaxyStage inputs = null, Action beforeStaging = null) { var galaxy = RunStart.Generate(scenario, inputs ?? Inputs()); var arena = new Zone(_items, _planetSettings, RunStart.GenerateArena(_items, _zoneSettings, galaxy, scenario), galaxy.Entrance, galaxy); + beforeStaging?.Invoke(); var failures = new List(); var staged = RunStart.Stage(_items, arena, scenario, _startingHull, _tutorialSettings, failures); return (galaxy, arena, staged, failures); @@ -175,7 +177,7 @@ public sealed partial class RunStartTests var first = Launch(scenario); Launch(new MainGalaxy(), Inputs(() => 4242)); _items.Random = new CultMath.Random(0xC0FFEE); // play draws from the run's item random - var second = Launch(scenario); + var second = Launch(scenario, beforeStaging: () => _items.Random = new CultMath.Random(0xBADC0DE)); Assert.True(first.failures.Count == 0, $"{scenario.Name}: {string.Join("; ", first.failures)}"); Assert.Equal(Layout(first.galaxy), Layout(second.galaxy)); Assert.Equal(Layout(first.arena.Pack), Layout(second.arena.Pack)); From 8cd68f4e65cdb5318d49338403866fec9773f4db Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 3 Oct 2026 22:36:04 +0200 Subject: [PATCH 12/12] Pin that a failed start's own writes are removed: the failed starts write a record the saved run does not hold, and the open store must not keep it Co-Authored-By: Claude Sonnet 5.5 --- tests/Aetheria.Shared.Tests/RunSaveTests.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/Aetheria.Shared.Tests/RunSaveTests.cs b/tests/Aetheria.Shared.Tests/RunSaveTests.cs index 51db6b137..c8630cdfd 100644 --- a/tests/Aetheria.Shared.Tests/RunSaveTests.cs +++ b/tests/Aetheria.Shared.Tests/RunSaveTests.cs @@ -139,11 +139,14 @@ public sealed class RunSaveTests : IDisposable Assert.Equal(8, saved.Length); using (var cache = Open()) + { Assert.Null(RunSave.Replace(cache, () => { - StageZoneContents(cache); + WriteAnOrbitTheSavedRunLacks(cache); return null; })); + Assert.Equal(saved, RunKeys(cache)); + } using (var cache = Open()) { Assert.Equal(saved, RunKeys(cache)); @@ -151,11 +154,14 @@ public sealed class RunSaveTests : IDisposable } using (var cache = Open()) + { Assert.Throws(() => RunSave.Replace(cache, () => { - StageZoneContents(cache); + WriteAnOrbitTheSavedRunLacks(cache); throw new InvalidOperationException("the arena would not generate"); })); + Assert.Equal(saved, RunKeys(cache)); + } using (var cache = Open()) Assert.Equal(saved, RunKeys(cache)); @@ -582,6 +588,9 @@ public sealed class RunSaveTests : IDisposable QualityPriceModifier = new ExponentialLerp() }; + // What a start that fails leaves behind: a record of its own, which the saved run does not hold. + private static void WriteAnOrbitTheSavedRunLacks(CultCache cache) => cache.Upsert(new OrbitData { Distance = 99 }); + // Two orbits and a body upserted into the run store and left staged, as zone generation leaves them. private static ZonePack StageZoneContents(CultCache cache) {