Merge faction-curated-place-names (7da0d414), --no-ff

Code from eureka/aetheria-release-faction-curated-place-names. Conflicts kept both sides: Faction holds Assembly (key 19) and Places (key 21); AetherDb dispatches assembly-from-maker, place-names and doctrine-catalog, and its usage names both; FlightTests counts the Faction wire at 22 slots (19 is Assembly, 21 is Places).

The two LFS files are the merge's resolution, replayed on the post-assembly bytes, never merged as text or by side.

GameData/Aetheria.cc, per h1's two-step replay: (1) AetherDb place-names apply built at the branch's commit 1 (740e1ee7, merged onto the assembly merge so key 19 survives), run on the post-assembly catalog: purges the Markov NameFile records while their type exists and authors 15 place lists; (2) place-names apply built at this merged tree, run on step 1's output: authors Miss Terri's list (1 list). Each step's second apply said nothing to do and left the bytes identical; the whole chain run twice from the assembly catalog gave bytes equal except the record write timestamps (masked, both hash alike). Decoded independently with cultcache-py: 16 factions, key 19 Assembly filled on the 15 makers (Ewan Hart has none), key 21 Places 20 names on all 16, key 9 nil on all, no namefile type among the 321 records.

Assets/Resources/Settings.asset: the post-assembly file (Rarity values removed) with the four NameGeneratorSettings lines removed (NameGeneratorSettings, MinLength 5, MaxLength 10, Order 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-18 02:16:57 +02:00
commit 55bda75ac3
57 changed files with 584 additions and 1891 deletions

View file

@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 0f3e518a4c9bcc2469abe6cebdcb3691
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Australia.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:01bb478f4433dc42d5533e5030ef103ad163deca6bc40fb412025acd12e35c73
3 size 442795

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 8ba25d5d0ecee31408c7ce7460b4e015
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Congo.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:c84ff44083e4f0d9d9314e7b7095ed4ce2955905f26e7926faa09ed222655729
3 size 506722

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 1e43b383bf860524986a6947496776d0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Germany.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:73e4e7231d448879d0a6d8524b3200be75fc8f5011a70cfd670b0897d07eca10
3 size 1284208

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ed470b8eb6cc68c4485ddc05d5a17ca1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/India.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:96c66414822c43a5451ce1910c1e545dedb224b0f3e79bc54f945dd484afc6a2
3 size 3229324

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: ca62f6d649360184aaa975c2716dfb27
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Iran.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:b70fbbe6b4832eb1a3f35844bd87c0eb479fc77b6e59d6600b64cbbe7cc1bf16
3 size 1073119

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: e2d60c7158f6a01418d78e46c5845729
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Japan.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:d58dc0227cc6e3b7622726c155031467b6d7314623c7e3a33aad8ce1462b2c5b
3 size 1031132

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5dd450a6f5ec92d49871674b33509135
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Mexico.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:9a654b8451a08a0c769712f4fb1d96c3c661457a88bb57ec4288eab36eab7ec1
3 size 633432

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: b75e17ea31f6a62488628bf9605bd48c
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Russia.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:f4ed581eef681e5357a14efe4e6a1fc52370def7ada6565f4b953e410fa7f39e
3 size 2375319

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5c1aee30ccf3a1e44bddeb4d550edbf0
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Swahili.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:fb0f44e7948c637a2b0f9e0ca098b2f4689d065fe6484723acf0bfa34a26d0a5
3 size 501002

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 63e21276398c8b0449e01d97a088add1
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/Thailand.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:888c929f0e29728369448e6981bb8a043c49610f4c4390a2cb549bf6b494d3dc
3 size 143084

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 2923f8bd3a2f0fb4084f64138e6d793d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/USA.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:314a30df2033795f04738589c660991e4310f1bfe0578c71bda2a9a72d8a617f
3 size 954928

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: a06c0e26f73d51f489303deadad77030
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load diff

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 790ef350f8658b04480e0645919a21fa
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Geonames/pleiades.csv (Stored with Git LFS)

Binary file not shown.
1 version https://git-lfs.github.com/spec/v1
2 oid sha256:45b0dbc07d492958b26de3695427076d270bdd0525ab45c415be72560ebccb5f
3 size 400944

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: b532694a5ec3cc6458d42aa2835537b5
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

BIN
Assets/Resources/Settings.asset (Stored with Git LFS)

Binary file not shown.

View file

@ -1,93 +0,0 @@
using System.Buffers.Text;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using MessagePack;
using CultMath;
using UnityEngine;
using UnityEditor;
using Random = CultMath.Random;
public class NameTools : EditorWindow
{
public int NameGeneratorMinLength = 5;
public int NameGeneratorMaxLength = 10;
public int NameGeneratorOrder = 4;
private int minWordLength = 4;
private TextAsset nameFile;
private MarkovNameGenerator _nameGenerator;
private bool _stripNumberTokens;
// Add menu named "My Window" to the Window menu
[MenuItem("Window/Aetheria/Name Tools")]
static void Init()
{
// Get existing open window or if none, make a new one:
NameTools window = (NameTools)EditorWindow.GetWindow(typeof(NameTools));
window.Show();
}
bool HasNonASCIIChars(string str)
{
return (System.Text.Encoding.UTF8.GetByteCount(str) != str.Length);
}
void OnGUI()
{
nameFile = (TextAsset) EditorGUILayout.ObjectField("Name File", nameFile, typeof(TextAsset), false);
minWordLength = EditorGUILayout.IntField("Minimum File Word Length", minWordLength);
NameGeneratorMinLength = EditorGUILayout.IntField("Generated Minimum Word Length", NameGeneratorMinLength);
NameGeneratorMaxLength = EditorGUILayout.IntField("Generated Maximum Word Length", NameGeneratorMaxLength);
NameGeneratorOrder = EditorGUILayout.IntField("Generator Order", NameGeneratorOrder);
_stripNumberTokens = GUILayout.Toggle(_stripNumberTokens, "Strip Number Tokens");
if (GUILayout.Button("Clean Name File"))
{
var lines = nameFile.text.Split('\n');
using StreamWriter outputFile = new StreamWriter(Path.Combine(Application.dataPath, nameFile.name + ".csv"));
var names = new HashSet<string>();
foreach (var line in lines)
{
var tokens = line.Split(',', ' ');
foreach (var t in tokens)
{
if (!HasNonASCIIChars(t))
{
var s = new string(t.Where(c => char.IsLetter(c) || c == '-' || c == '`' || c == '\'').ToArray()).Trim().Trim('`','-');
if(s.Length >= minWordLength && !names.Contains(s))
{
names.Add(s);
outputFile.WriteLine(s);
}
}
}
}
}
if (GUILayout.Button("Process Name File") && nameFile != null)
{
var names = new HashSet<string>();
var lines = nameFile.text.Split('\n');
foreach (var line in lines)
{
foreach(var word in line.ToUpperInvariant().Split(' ', ',', '.', '"'))
if (word.Length >= minWordLength && !names.Contains(word))
names.Add(word);
}
Debug.Log($"Found {lines.Length} lines, with {names.Count} unique names!");
var random = new Random(1337);
_nameGenerator = new MarkovNameGenerator(ref random, names, NameGeneratorOrder, NameGeneratorMinLength, NameGeneratorMaxLength);
}
if (_nameGenerator != null)
{
if (GUILayout.Button("Generate Name"))
{
Debug.Log(_nameGenerator.NextName);
}
}
}
}

View file

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 88b5f578fe63a904b997331630e61416
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -22,7 +22,6 @@ public class GameSettings : ScriptableObject
public float MinimapAsteroidSize = 3;
public float MinimapIconSize = 1f / 32;
public float PlanetRotationSpeed = 1;
public NameGeneratorSettings NameGeneratorSettings;
public TutorialGenerationSettings TutorialGenerationSettings;
public SectorBackgroundSettings TutorialBackgroundSettings;
public SectorBackgroundSettings SectorBackgroundSettings;

View file

@ -7,7 +7,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(FieldKindData), typeof(VerseGrammar), typeof(VerseVerb) };
public static readonly Type[] CatalogTypes = { typeof(ItemData), typeof(ShipAuthoring), typeof(Faction), typeof(FactionProductData), typeof(PersonalityAttribute), typeof(InputLayout), typeof(Loadout), typeof(FieldKindData), typeof(VerseGrammar), typeof(VerseVerb) };
public static readonly Type[] RunTypes = { typeof(OrbitData), typeof(BodyData), typeof(SavedZone), typeof(SavedGame), typeof(ProvenanceLedger) };
public static readonly Type[] PlayerTypes = { typeof(PlayerSettings) };

View file

@ -37,8 +37,7 @@ public class Faction
[InspectableColor, JsonProperty("secondaryColor"), Key(8)]
public float3 SecondaryColor;
[Inspectable, JsonProperty("nameFile"), Key(9)]
public CultRecordRef<NameFile> GeonameFile;
// Key 9 was the faction's name-file link, retired by ruling faction-curated-place-names; never reuse it.
[Inspectable, JsonProperty("bossHull"), Key(10)]
public CultRecordRef<HullData> BossHull;
@ -70,11 +69,23 @@ public class Faction
// tune it in Studio after that. Key 16 is reserved for Relations and 18 for Livery.
[Inspectable, JsonProperty("assembly"), Key(19)]
public AssemblyDistribution Assembly;
// Curated place names in authored order; the home zone takes the first (ruling faction-curated-place-names).
// Authored by AetherDb place-names, which restores the table on every apply.
[Inspectable, JsonProperty("places"), Key(21)]
public FactionPlace[] Places = Array.Empty<FactionPlace>();
}
[CultDocument("aetheria.namefile", "1"), Inspectable, MessagePackObject]
public class NameFile
// Historical, narrative and strategic intent join as keys 2+ when economics-driven placement lands (ruling faction-curated-place-names).
[MessagePackObject, JsonObject(MemberSerialization.OptIn)]
public class FactionPlace
{
[CultName, Key(1)] public string Name;
[Key(2)] public string[] Names;
[Inspectable, JsonProperty("name"), Key(1)]
public string Name;
// The one rule for a valid place name: trimmed, non-empty and not a catalogue designation (EAC-, any case). Galaxy.NamePlaces
// treats an invalid entry as absent; AetherDb place-names refuses a table that holds one.
public static bool IsValidName(string name) =>
!string.IsNullOrEmpty(name) && name == name.Trim() && !name.StartsWith("EAC-", StringComparison.OrdinalIgnoreCase);
}

View file

@ -20,7 +20,6 @@ public class Galaxy
public HashSet<GalaxyZone> DiscoveredZones = new HashSet<GalaxyZone>();
public SectorBackgroundSettings Background { get; }
public NameGeneratorSettings NameGeneratorSettings { get; }
public Faction[] Factions { get; }
public GalaxyZone[] Zones { get; }
public GalaxyZone Entrance { get; }
@ -34,7 +33,6 @@ public class Galaxy
public uint Seed { get; }
private GalaxyZone[] _exitPath;
private Dictionary<Faction, MarkovNameGenerator> _nameGenerators = new Dictionary<Faction, MarkovNameGenerator>();
private readonly CultCache _cache;
public GalaxyZone[] ExitPath
@ -96,7 +94,6 @@ public class Galaxy
public Galaxy(
SectorGenerationSettings settings,
SectorBackgroundSettings background,
NameGeneratorSettings nameGeneratorSettings,
CultCache cache,
Action<string> log,
Action<string> progressCallback = null,
@ -131,7 +128,7 @@ public class Galaxy
CalculateFactionInfluence(progressCallback);
GenerateNames(cache, nameGeneratorSettings, ref random, progressCallback);
NamePlaces(ref random, progressCallback);
progressCallback?.Invoke("Done!");
if(progressCallback!=null) Thread.Sleep(500); // Inserting Delay to make it seem like it's doing more work lmao
@ -162,7 +159,6 @@ public class Galaxy
public Galaxy(
TutorialGenerationSettings settings,
SectorBackgroundSettings background,
NameGeneratorSettings nameGeneratorSettings,
CultCache cache,
PlayerSettings playerSettings,
DirectoryInfo narrativeDirectory,
@ -259,7 +255,7 @@ public class Galaxy
CalculateFactionInfluence(progressCallback);
GenerateNames(cache, nameGeneratorSettings, ref random, progressCallback);
NamePlaces(ref random, progressCallback);
// progressCallback?.Invoke("Weaving Narrative");
// var processor = new StoryProcessor(playerSettings, narrativeDirectory, this, ref random, Log);
@ -350,31 +346,37 @@ public class Galaxy
}
}
private void GenerateNames(CultCache cache,
NameGeneratorSettings nameGeneratorSettings,
ref Random random,
Action<string> progressCallback = null)
// Each faction names the zones it owns from its own curated list (ruling faction-curated-place-names): nearest its
// home first, so the home zone takes the capital, which is Places[0]. A zone left over when a list is spent, and every
// zone nobody owns, takes a catalogue designation. A name is never used twice, ignoring case.
internal void NamePlaces(ref Random random, Action<string> progressCallback = null)
{
var used = new HashSet<string>();
for (var i = 0; i < Factions.Length; i++)
progressCallback?.Invoke("Naming places");
var used = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var zone in Zones) zone.Name = null;
foreach (var faction in Factions)
{
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, used);
if (!HomeZones.TryGetValue(faction, out var home)) continue;
var places = (faction.Places ?? Array.Empty<FactionPlace>()).Select(p => p?.Name).Where(FactionPlace.IsValidName).ToArray();
var next = 0;
var owned = Zones.Select((zone, index) => (zone, index)).Where(z => z.zone.Owner == faction)
.OrderBy(z => z.zone.Distance[home]).ThenBy(z => z.index);
foreach (var (zone, _) in owned)
{
while (next < places.Length && used.Contains(places[next])) next++;
if (next == places.Length) break;
zone.Name = places[next];
used.Add(places[next++]);
}
}
// Generate zone name using the owner's name generator, otherwise assign catalogue ID
foreach (var zone in Zones)
foreach (var zone in Zones.Where(z => z.Name == null))
{
if (zone.Owner != null)
{
zone.Name = _nameGenerators[zone.Owner].NextName.Trim();
}
else
{
zone.Name = $"EAC-{random.NextInt(9999).ToString()}";
}
string designation;
do designation = $"EAC-{random.NextInt(9999)}";
while (!used.Add(designation));
zone.Name = designation;
}
}

View file

@ -15,7 +15,6 @@ public sealed class GalaxyStage
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;
@ -28,7 +27,6 @@ public sealed class GalaxyStage
SectorBackgroundSettings sectorBackground,
TutorialGenerationSettings prelude,
SectorBackgroundSettings preludeBackground,
NameGeneratorSettings names,
CultCache cache,
PlayerSettings playerSettings,
DirectoryInfo narrativeDirectory,
@ -40,7 +38,6 @@ public sealed class GalaxyStage
_sectorBackground = sectorBackground;
_prelude = prelude;
_preludeBackground = preludeBackground;
_names = names;
_cache = cache;
_playerSettings = playerSettings;
_narrativeDirectory = narrativeDirectory;
@ -64,7 +61,7 @@ public sealed class GalaxyStage
var background = Copy(_sectorBackground);
var random = new Random(Seed);
background.NoisePosition = random.NextFloat() * 1000;
return new Galaxy(_sector, background, _names, _cache, _log, _progress, Seed);
return new Galaxy(_sector, background, _cache, _log, _progress, Seed);
}
// A prelude galaxy: the tutorial settings and their fixed factions, centred in dense cloud.
@ -81,7 +78,7 @@ public sealed class GalaxyStage
background.NoisePosition = random.NextFloat() * 1000;
_progress?.Invoke($"Finding Galaxy Position: iteration {iteration++}");
} while (background.CloudDensity(float2(.5f)) < .5f);
return new Galaxy(cast, background, _names, _cache, _playerSettings, _narrativeDirectory, _log, _progress, Seed);
return new Galaxy(cast, background, _cache, _playerSettings, _narrativeDirectory, _log, _progress, Seed);
}
private static SectorBackgroundSettings Copy(SectorBackgroundSettings settings) =>

View file

@ -1,129 +0,0 @@
/* 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;
using System.Collections.Generic;
using System.Linq;
using static CultMath.math;
using Random = CultMath.Random;
//Generates random names based on the statistical weight of letter sequences
//in a collection of sample names
public class MarkovNameGenerator
{
private Dictionary<string, List<char>> _chains = new Dictionary<string, List<char>>();
private List<string> _samples = new List<string>();
// 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<string> _used;
private int _order;
private int _minLength;
private int _maxLength;
private Random _random;
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, NameGeneratorSettings settings, HashSet<string> used)
{
_used = used;
var names = new HashSet<string>();
foreach (var line in sampleNames)
{
foreach(var word in line.ToUpperInvariant().Split(' ', ',', '.', '"'))
if (word.Length >= settings.NameGeneratorMinLength && !names.Contains(word))
names.Add(word);
}
_random = random;
_order = max(1, settings.NameGeneratorOrder);
_minLength = max(1, settings.NameGeneratorMinLength);
_maxLength = settings.NameGeneratorMaxLength;
Initialize(names);
}
public MarkovNameGenerator(ref Random random, IEnumerable<string> sampleNames, int order, int minLength, int maxLength)
{
_used = new HashSet<string>();
_random = random;
_order = max(1, order);
_minLength = max(1, minLength);
_maxLength = maxLength;
Initialize(sampleNames);
}
private void Initialize(IEnumerable<string> sampleNames)
{
//split comma delimited lines
foreach (string line in sampleNames)
{
string[] tokens = line.Split(' ','\'','(',')');
foreach (string word in tokens)
{
string upper = word.Trim().ToLowerInvariant();
if (upper.Length < _order + 1)
continue;
_samples.Add(upper + '|'); // Add end character
}
}
//Build chains
foreach (string word in _samples)
{
for (int letter = 0; letter < word.Length - _order; letter++)
{
string token = word.Substring(letter, _order);
List<char> entry = null;
if (_chains.ContainsKey(token))
entry = _chains[token];
else
{
entry = new List<char>();
_chains[token] = entry;
}
entry.Add(word[letter + _order]);
}
}
}
//Get the next random name
public string NextName
{
get
{
string s = "";
do
{
int n = _random.NextInt(0,_samples.Count);
//int nameLength = _samples[n].Length;
s = _samples[n].Substring(0, _order);//get a random token at the start of a sample word
while (true)//(s.Length < nameLength)
{
string token = s.Substring(s.Length - _order, _order);
char c = GetLetter(token);
if (c != '?' && c != '|')
s += c;//GetLetter(token);
else
break;
}
s = s.Substring(0, 1).ToUpper() + s.Substring(1);
}
while (_used.Contains(s) || s.Length < _minLength || s.Length > _maxLength);
_used.Add(s);
return s;
}
}
//Get a random letter from the chain
private char GetLetter(string token)
{
if (!_chains.ContainsKey(token))
return '?';
List<char> letters = _chains[token];
int n = _random.NextInt(0,letters.Count);
return letters[n];
}
}

View file

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 33c3b119ccc9de447b8c8318b6f0d1af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -44,14 +44,6 @@ public class GalaxyShapeSettings
public float TwistExponent = 2;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class NameGeneratorSettings
{
public int NameGeneratorMinLength = 5;
public int NameGeneratorMaxLength = 10;
public int NameGeneratorOrder = 4;
}
[Serializable, MessagePackObject(keyAsPropertyName:true), JsonObject]
public class SectorBackgroundSettings
{

View file

@ -177,7 +177,6 @@ public class MainMenu : MonoBehaviour
Settings.SectorBackgroundSettings,
Settings.TutorialGenerationSettings,
Settings.TutorialBackgroundSettings,
Settings.NameGeneratorSettings,
ActionGameManager.CultCache,
ActionGameManager.PlayerSettings,
ActionGameManager.GameDataDirectory.CreateSubdirectory("Narrative"),

BIN
GameData/Aetheria.cc (Stored with Git LFS)

Binary file not shown.

View file

@ -40,7 +40,7 @@ public sealed class ChildrenWithinReachTests : IClassFixture<ChildrenWithinReach
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
var galaxy = new Galaxy(authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"),
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), cache, new PlayerSettings(),
cache, new PlayerSettings(),
Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
var planetSettings = authored.Read<PlanetSettings>("PlanetSettings");

View file

@ -280,8 +280,7 @@ public sealed class ConsumableSupplyTests : IDisposable
var authored = AuthoredSettings.Load(repo);
var galaxy = new Galaxy(authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"),
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"), authored.Read<NameGeneratorSettings>("NameGeneratorSettings"),
cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, 1);
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"), cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, 1);
var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
int stations = 0, ships = 0;

View file

@ -910,7 +910,8 @@ public sealed class FlightTests : IDisposable
Assert.Equal(turns, turn != 0f);
}
// Faction.Doctrine rides at wire slot 17; slot 16 is reserved for Relations. Kills: the key moved.
// Faction.Doctrine rides at wire slot 17; slot 16 is reserved for Relations; Places rides at 21, which sets the array's length.
// Kills: the key moved.
[Fact]
public void TheFactionDoctrineRidesAtWireSlot17()
{
@ -919,7 +920,7 @@ public sealed class FlightTests : IDisposable
var slots = MessagePack.MessagePackSerializer.Deserialize<object[]>(MessagePack.MessagePackSerializer.Serialize(faction,
CultDocumentMessagePackSerialization.OptionsFor(typeof(Faction).Assembly)));
Assert.Equal(20, slots.Length); // 19 is Assembly
Assert.Equal(22, slots.Length); // 19 is Assembly, 21 is Places
Assert.Null(slots[16]);
var doctrine = Assert.IsType<object[]>(slots[17]);
Assert.Equal(6f, Convert.ToSingle(doctrine[1]));

View file

@ -814,7 +814,7 @@ public sealed partial class MiningCut3Tests : IDisposable
try
{
galaxy = new Galaxy(RestoredHullsTests.TutorialGalaxySettings(), RestoredHullsTests.TutorialBackgroundSettings(),
RestoredHullsTests.TutorialNameSettings(), cache, new PlayerSettings(), new DirectoryInfo(dir), _ => { }, null, seed);
cache, new PlayerSettings(), new DirectoryInfo(dir), _ => { }, null, seed);
}
catch (Exception) { continue; } // Galaxy's own seed-dependent flakiness (RestoredHullsTests)
var items = new ItemManager(cache, new ProvenanceLedger(), GameSettings(), _ => { });

View file

@ -57,7 +57,7 @@ public sealed class MinorPowersTests : IDisposable
foreach (var shortName in Minor)
{
var faction = Assert.Single(factions, f => f.ShortName == shortName);
Assert.NotNull(cache.Get(faction.GeonameFile));
Assert.NotEmpty(faction.Places);
Assert.True(faction.InfluenceDistance > 0, shortName);
var named = faction.Allegiance.Keys.Select(k => k.Key).ToArray();
var expected = factions.Where(f => f != faction && f.ShortName != "Pirates").Select(f => cache.RefOf(f).Key).ToArray();

View file

@ -253,7 +253,7 @@ public sealed class OrbitsFromFieldTests : IDisposable
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
var tutorial = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
var galaxy = new Galaxy(tutorial, authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), cache, new PlayerSettings(),
cache, new PlayerSettings(),
Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
var pack = ZoneGenerator.GenerateZone(items, authored.Read<ZoneGenerationSettings>("ZoneSettings"), authored.Read<PlanetSettings>("PlanetSettings"), galaxy, galaxy.Entrance,

View file

@ -82,7 +82,6 @@ public sealed class PiratesFactionCommandTests : IDisposable
var db = AetherDb.Open(root: _root);
try
{
Assert.Equal("pleiades", db.Cache.Get(faction.GeonameFile).Name);
var expected = new (string Design, string Name, string Description)[]
{
("Autocannon", "🫳🔫", "Serial number filed off. Still shoots."),
@ -172,17 +171,4 @@ public sealed class PiratesFactionCommandTests : IDisposable
Assert.Throws<InvalidOperationException>(() => Program.PiratesFaction(apply: true, root: _root));
Assert.Empty(Read().Pirates);
}
[Fact]
public void A_missing_name_file_refuses_and_lands_nothing()
{
var db = AetherDb.Open(catalogWritable: true, root: _root);
var pleiades = db.Cache.GetAll<NameFile>().Single(file => file.Name == "pleiades");
Assert.True(db.Cache.Remove(db.Cache.RefOf(pleiades).Key));
db.Cache.FlushAsync().Wait();
db.Cache.Dispose();
Assert.Throws<InvalidOperationException>(() => Program.PiratesFaction(apply: true, root: _root));
Assert.Empty(Read().Pirates);
}
}

View file

@ -25,7 +25,7 @@ public sealed partial class RunStartTests
var others = _cache.GetAll<Faction>().Where(faction => faction != pirates).Select(faction => _cache.RefOf(faction)).ToArray();
Assert.NotEmpty(others);
Assert.All(others, other => Assert.True(pirates.Allegiance.ContainsKey(other)));
Assert.NotNull(_cache.Get(pirates.GeonameFile));
Assert.NotEmpty(pirates.Places);
var products = PiratesProducts();
Assert.Equal(8, products.Length);

View file

@ -0,0 +1,366 @@
/* 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;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using GameCult.Caching;
using Xunit;
using Random = CultMath.Random;
// Each faction names the zones it owns from its own curated list (aetheria-release, cut faction-curated-place-names): the
// home zone takes the capital, the rest follow in distance order, a spent or absent list falls back to catalogue designations,
// and no name is used twice. Galaxies are built the way the game builds them, from the authored settings and the shipped catalog.
public sealed class PlaceNamesTests : IDisposable
{
private static readonly Regex Designation = new(@"^EAC-\d{1,4}$");
private static readonly AuthoredSettings Authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
private static readonly CultCache Shared = RestoredHullsTests.OpenCatalog();
private static readonly Dictionary<(bool, uint), Galaxy> Built = new();
private readonly List<CultCache> _own = new();
public void Dispose()
{
foreach (var cache in _own) cache.Dispose();
foreach (var root in _scratch) Directory.Delete(root, true);
}
private static Galaxy Build(CultCache cache, bool prelude, uint seed) => prelude
? new Galaxy(Authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"), Authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed)
: new Galaxy(Authored.Read<SectorGenerationSettings>("SectorGenerationSettings"), Authored.Read<SectorBackgroundSettings>("SectorBackgroundSettings"),
cache, _ => { }, null, seed);
// A prelude galaxy sometimes refuses to generate for a seed (RestoredHullsTests calls it Galaxy's own seed-dependent
// flakiness); those seeds are skipped, and a test that depends on how many built says so.
private static Galaxy TryBuild(bool prelude, uint seed)
{
lock (Built)
{
if (Built.TryGetValue((prelude, seed), out var known)) return known;
Galaxy galaxy;
try { galaxy = Build(Shared, prelude, seed); }
catch (Exception) { galaxy = null; }
return Built[(prelude, seed)] = galaxy;
}
}
private static IEnumerable<Galaxy> Galaxies(bool prelude, uint last) =>
Enumerable.Range(1, (int)last).Select(seed => TryBuild(prelude, (uint)seed)).Where(g => g != null);
private static Faction[] Placed(Galaxy galaxy) =>
galaxy.Factions.Where(f => galaxy.HomeZones.ContainsKey(f) && f.Places.Length != 0).ToArray();
private static GalaxyZone[] Owned(Galaxy galaxy, Faction faction) =>
galaxy.Zones.Select((zone, index) => (zone, index)).Where(z => z.zone.Owner == faction)
.OrderBy(z => z.zone.Distance[galaxy.HomeZones[faction]]).ThenBy(z => z.index).Select(z => z.zone).ToArray();
// Two factions can share a home zone (the prelude puts the quest faction on the buffer's); the zone's owner is the
// nearest home in faction order, and that owner's capital is what the zone is called.
[Fact]
public void TheHomeZoneTakesTheOwnersFirstName()
{
var own = 0;
var shared = 0;
foreach (var galaxy in Galaxies(true, 10).Concat(Galaxies(false, 10)))
foreach (var faction in galaxy.Factions.Where(galaxy.HomeZones.ContainsKey))
{
var home = galaxy.HomeZones[faction];
var owner = home.Owner;
Assert.NotNull(owner);
if (owner.Places.Length == 0) Assert.Matches(Designation, home.Name);
else Assert.Equal(owner.Places[0].Name, home.Name);
if (owner == faction) own++; else shared++;
}
Assert.True(own >= 20, $"only {own} factions owned their home");
Assert.True(shared > 0, "no galaxy shared a home zone, so the shared case was not exercised");
}
[Fact]
public void OwnedZonesTakeTheListInDistanceOrder()
{
var checkedFactions = 0;
foreach (var galaxy in Galaxies(true, 10).Concat(Galaxies(false, 10)))
foreach (var faction in Placed(galaxy))
{
var owned = Owned(galaxy, faction);
var listed = Math.Min(owned.Length, faction.Places.Length);
Assert.Equal(faction.Places.Take(listed).Select(p => p.Name), owned.Take(listed).Select(z => z.Name));
Assert.All(owned.Skip(listed).Select(z => z.Name), name => Assert.Matches(Designation, name));
checkedFactions++;
}
Assert.True(checkedFactions >= 20, $"only {checkedFactions} factions were checked");
}
[Fact]
public void NoZoneCarriesAnotherFactionsName()
{
var curated = Shared.GetAll<Faction>().SelectMany(f => f.Places.Select(p => (p.Name, Faction: f))).ToDictionary(t => t.Name, t => t.Faction, StringComparer.OrdinalIgnoreCase);
var zones = 0;
foreach (var galaxy in Galaxies(true, 10).Concat(Galaxies(false, 10)))
foreach (var zone in galaxy.Zones.Where(z => curated.ContainsKey(z.Name)))
{
Assert.True(ReferenceEquals(curated[zone.Name], zone.Owner),
$"zone '{zone.Name}' is owned by '{zone.Owner?.ShortName}' but curated by '{curated[zone.Name].ShortName}'");
zones++;
}
Assert.True(zones >= 50, $"only {zones} curated zones were checked");
}
[Fact]
public void ASpentListFallsBackToCatalogueDesignations()
{
// Its own cache: the faction's list is cut, which must not reach the galaxies the other tests share.
var cache = RestoredHullsTests.OpenCatalog();
_own.Add(cache);
var galaxy = Enumerable.Range(1, 20).Select(seed => { try { return Build(cache, true, (uint)seed); } catch (Exception) { return null; } })
.First(g => g != null && Placed(g).Any(f => Owned(g, f).Length >= 4));
var faction = Placed(galaxy).First(f => Owned(galaxy, f).Length >= 4);
faction.Places = faction.Places.Take(2).ToArray();
var random = new Random(7);
galaxy.NamePlaces(ref random);
var owned = Owned(galaxy, faction);
Assert.Equal(faction.Places.Select(p => p.Name), owned.Take(2).Select(z => z.Name));
Assert.True(ReferenceEquals(owned[0], galaxy.HomeZones[faction]), $"the nearest zone '{owned[0].Name}' is not {faction.ShortName}'s home '{galaxy.HomeZones[faction].Name}'");
Assert.All(owned.Skip(2).Select(z => z.Name), name => Assert.Matches(Designation, name));
}
// The table refuses a repeated name, but a list that did not come through it (a package's factions) can still hold
// one: the later faction skips a name the earlier one already used, whatever its case.
[Fact]
public void AnotherFactionsNameIsNotReusedWhateverItsCase()
{
var cache = RestoredHullsTests.OpenCatalog();
_own.Add(cache);
var galaxy = Enumerable.Range(1, 20).Select(seed => { try { return Build(cache, true, (uint)seed); } catch (Exception) { return null; } })
.First(g => g != null && Placed(g).Count(f => Owned(g, f).Length >= 2) >= 2);
var pair = Placed(galaxy).Where(f => Owned(galaxy, f).Length >= 2).Take(2).ToArray();
var (first, later) = (pair[0], pair[1]);
Assert.True(Array.IndexOf(galaxy.Factions, first) < Array.IndexOf(galaxy.Factions, later));
var taken = first.Places[0].Name;
var shouting = taken.ToUpperInvariant();
Assert.NotEqual(taken, shouting);
// Two consecutive names are taken, so the later faction must skip a run, not a single name.
var takenToo = first.Places[1].Name;
var own = later.Places[0].Name;
later.Places = new[] { new FactionPlace { Name = shouting }, new FactionPlace { Name = takenToo.ToUpperInvariant() }, new FactionPlace { Name = own } };
var random = new Random(7);
galaxy.NamePlaces(ref random);
Assert.Equal(taken, Owned(galaxy, first)[0].Name);
Assert.Equal(own, Owned(galaxy, later)[0].Name);
var names = galaxy.Zones.Select(z => z.Name).ToArray();
Assert.Equal(names.Length, names.Distinct(StringComparer.OrdinalIgnoreCase).Count());
}
// The rule is one function (FactionPlace.IsValidName); an entry that fails it counts as absent, so the next valid name
// is the capital and nothing invalid ever titles a zone.
[Theory]
[InlineData("")]
[InlineData(" ")]
[InlineData(" Padded")]
[InlineData("eac-6")]
[InlineData("EAC-6")]
[InlineData(null)]
public void AnInvalidEntryCountsAsAbsent(string bad)
{
var cache = RestoredHullsTests.OpenCatalog();
_own.Add(cache);
var galaxy = Enumerable.Range(1, 20).Select(seed => { try { return Build(cache, true, (uint)seed); } catch (Exception) { return null; } })
.First(g => g != null && Placed(g).Any(f => Owned(g, f).Length >= 3));
var faction = Placed(galaxy).First(f => Owned(galaxy, f).Length >= 3);
faction.Places = new[] { new FactionPlace { Name = bad }, new FactionPlace { Name = "Real" }, null, new FactionPlace { Name = "Second" } };
var random = new Random(7);
galaxy.NamePlaces(ref random);
Assert.Equal(new[] { "Real", "Second" }, Owned(galaxy, faction).Take(2).Select(z => z.Name));
Assert.Matches(Designation, Owned(galaxy, faction)[2].Name);
}
// A faction with no home does not stop the ones after it from naming their zones.
[Fact]
public void AFactionWithoutAHomeDoesNotEndTheNaming()
{
var cache = RestoredHullsTests.OpenCatalog();
_own.Add(cache);
var galaxy = Enumerable.Range(1, 20).Select(seed => { try { return Build(cache, true, (uint)seed); } catch (Exception) { return null; } })
.First(g => g != null && Placed(g).Count(f => g.HomeZones[f].Owner == f) >= 2);
var homeless = galaxy.Factions.First(galaxy.HomeZones.ContainsKey);
var later = galaxy.Factions.Where(f => f != homeless && galaxy.HomeZones.ContainsKey(f) && f.Places.Length != 0 && galaxy.HomeZones[f].Owner == f)
.First(f => Array.IndexOf(galaxy.Factions, f) > Array.IndexOf(galaxy.Factions, homeless));
galaxy.HomeZones.Remove(homeless);
var random = new Random(7);
galaxy.NamePlaces(ref random);
Assert.Equal(later.Places[0].Name, galaxy.HomeZones[later].Name);
}
[Fact]
public void NoTwoZonesShareAName()
{
var checkedGalaxies = 0;
foreach (var galaxy in Galaxies(true, 40).Concat(Galaxies(false, 40)))
{
var names = galaxy.Zones.Select(z => z.Name).ToArray();
Assert.All(names, name => Assert.False(string.IsNullOrEmpty(name)));
Assert.Equal(names.Length, names.Distinct(StringComparer.OrdinalIgnoreCase).Count());
checkedGalaxies++;
}
Assert.True(checkedGalaxies >= 40, $"only {checkedGalaxies} galaxies were checked");
}
[Fact]
public void AFactionWithoutAListGetsCatalogueDesignations()
{
// Its own cache: the list is cleared, which must not reach the galaxies the other tests share.
var cache = RestoredHullsTests.OpenCatalog();
_own.Add(cache);
var terris = cache.GetAll<Faction>().Single(f => f.ShortName == "Miss Terri's");
Assert.NotEmpty(terris.Places);
var galaxy = Enumerable.Range(1, 20).Select(seed => { try { return Build(cache, true, (uint)seed); } catch (Exception) { return null; } })
.First(g => g != null && g.Zones.Any(z => z.Owner == terris));
terris.Places = Array.Empty<FactionPlace>();
var random = new Random(7);
galaxy.NamePlaces(ref random);
var held = galaxy.Zones.Where(z => z.Owner == terris).ToArray();
Assert.NotEmpty(held);
Assert.All(held.Select(z => z.Name), name => Assert.Matches(Designation, name));
}
[Fact]
public void TheShippedListsAreTheTable()
{
var factions = Shared.GetAll<Faction>().ToArray();
foreach (var row in PlaceNamesCommand.Table)
{
var matching = factions.Where(f => Shared.RefOf(f).Key.Value == row.RecordKey).ToArray();
Assert.True(matching.Length == 1, $"{matching.Length} catalog factions carry the key of the {row.ShortName} row");
var faction = matching[0];
Assert.Equal(row.ShortName, faction.ShortName);
Assert.Equal(row.Names, faction.Places.Select(p => p.Name));
}
Assert.Equal(PlaceNamesCommand.Table.Length, factions.Count(f => f.Places.Length != 0));
var names = factions.SelectMany(f => f.Places.Select(p => p.Name)).ToArray();
Assert.All(names, name =>
{
Assert.False(string.IsNullOrEmpty(name));
Assert.Equal(name.Trim(), name);
Assert.False(name.StartsWith("EAC-", StringComparison.OrdinalIgnoreCase));
});
Assert.Equal(names.Length, names.Distinct(StringComparer.OrdinalIgnoreCase).Count());
}
// The command's scratch catalog: the shipped one with every list cleared, so an apply has all of it to author.
private string ScratchWithoutLists()
{
var root = Path.Combine(Path.GetTempPath(), "place-names-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(Path.Combine(root, "GameData"));
var path = Path.Combine(root, "GameData", "Aetheria.cc");
File.Copy(Path.Combine(RunStartTests.FindRepoRoot(), "GameData", "Aetheria.cc"), path);
using (var cache = new CultCache(TestCatalog.Registry()))
{
cache.AddBackingStore(new GameCult.Caching.MessagePack.SingleFileMessagePackBackingStore(path), AetheriaStores.CatalogTypes);
foreach (var faction in cache.GetAll<Faction>().ToArray())
{
faction.Places = Array.Empty<FactionPlace>();
cache.Commit(batch => batch.Upsert(typeof(Faction), faction, cache.RefOf(faction).Key));
}
cache.FlushAsync().Wait();
}
_scratch.Add(root);
return root;
}
private readonly List<string> _scratch = new();
private static string CatalogOf(string root) => Path.Combine(root, "GameData", "Aetheria.cc");
[Fact]
public void TheCommandIsIdempotentAndRefusesAMismatch()
{
var root = ScratchWithoutLists();
var before = File.ReadAllBytes(CatalogOf(root));
Assert.Equal(0, PlaceNamesCommand.Run(new string[0], root));
Assert.Equal(before, File.ReadAllBytes(CatalogOf(root)));
Assert.Equal(0, PlaceNamesCommand.Run(new[] { "apply" }, root));
var landed = File.ReadAllBytes(CatalogOf(root));
Assert.NotEqual(before, landed);
var db = AetherDb.Open(root: root);
try
{
foreach (var row in PlaceNamesCommand.Table)
Assert.Equal(row.Names, db.Cache.GetAll<Faction>().Single(f => db.Cache.RefOf(f).Key.Value == row.RecordKey).Places.Select(p => p.Name));
}
finally { db.Cache.Dispose(); }
Assert.Equal(0, PlaceNamesCommand.Run(new[] { "apply" }, root));
Assert.Equal(landed, File.ReadAllBytes(CatalogOf(root)));
// Aya's ShortName changed under its key: identity is the key, so the row no longer describes that faction.
var squatter = AetherDb.Open(catalogWritable: true, root: root);
var aya = squatter.Cache.GetAll<Faction>().Single(f => f.ShortName == "Aya");
aya.ShortName = "Ayx";
squatter.Cache.Commit(batch => batch.Upsert(typeof(Faction), aya, squatter.Cache.RefOf(aya).Key));
squatter.Cache.Dispose();
var changed = File.ReadAllBytes(CatalogOf(root));
var refusal = Assert.Throws<InvalidOperationException>(() => PlaceNamesCommand.Run(new[] { "apply" }, root));
Assert.DoesNotContain("Ayx", refusal.Message);
Assert.Equal(changed, File.ReadAllBytes(CatalogOf(root)));
}
[Fact]
public void ApplyRestoresAnEditedListOfTheSameLength()
{
var root = ScratchWithoutLists();
Assert.Equal(0, PlaceNamesCommand.Run(new[] { "apply" }, root));
var row = PlaceNamesCommand.Table.First();
var edited = AetherDb.Open(catalogWritable: true, root: root);
var faction = edited.Cache.GetAll<Faction>().Single(f => edited.Cache.RefOf(f).Key.Value == row.RecordKey);
faction.Places = row.Names.Reverse().Select(n => new FactionPlace { Name = n }).ToArray();
edited.Cache.Commit(batch => batch.Upsert(typeof(Faction), faction, edited.Cache.RefOf(faction).Key));
edited.Cache.Dispose();
Assert.Equal(0, PlaceNamesCommand.Run(new[] { "apply" }, root));
var db = AetherDb.Open(root: root);
try { Assert.Equal(row.Names, db.Cache.GetAll<Faction>().Single(f => db.Cache.RefOf(f).Key.Value == row.RecordKey).Places.Select(p => p.Name)); }
finally { db.Cache.Dispose(); }
}
[Theory]
[InlineData("EAC-12")]
[InlineData("eac-12")]
[InlineData(" Padded")]
[InlineData("")]
[InlineData("shunya")]
public void ATableWithABadNameIsRefusedWithNothingWritten(string bad)
{
var root = ScratchWithoutLists();
var before = File.ReadAllBytes(CatalogOf(root));
var table = PlaceNamesCommand.Table.Select((row, i) => i == 1 ? row with { List = row.List + " | " + bad } : row).ToArray();
var refusal = Assert.Throws<InvalidOperationException>(() => PlaceNamesCommand.Run(new[] { "apply" }, root, table));
if (bad.Length != 0) Assert.DoesNotContain(bad, refusal.Message);
Assert.Equal(before, File.ReadAllBytes(CatalogOf(root)));
}
[Fact]
public void ARowForAKeyTheCatalogDoesNotHoldIsRefusedWithNothingWritten()
{
var root = ScratchWithoutLists();
var before = File.ReadAllBytes(CatalogOf(root));
var table = PlaceNamesCommand.Table.Append(new PlaceNamesCommand.Row("00000000-0000-0000-0000-000000000000", "Nobody", "Elsewhere")).ToArray();
Assert.Throws<InvalidOperationException>(() => PlaceNamesCommand.Run(new[] { "apply" }, root, table));
Assert.Equal(before, File.ReadAllBytes(CatalogOf(root)));
}
}

View file

@ -91,9 +91,6 @@ public sealed class RestoredHullsTests
NoisePosition = 536.5106f, CloudExponent = 10, CloudAmplitude = .01f,
};
internal static NameGeneratorSettings TutorialNameSettings() =>
new NameGeneratorSettings { NameGeneratorMinLength = 5, NameGeneratorMaxLength = 10, NameGeneratorOrder = 3 };
// A station's orbit is a copy of its source planet's own (Parent, Distance) (ZoneGenerator.CreateLagrangeOrbit),
// so it always matches that one planet. It matches MORE than one planet only when the source planet shares its
// distance with a sibling under the same parent -- exactly what ZoneGenerator's potentialLagrangePoints filter
@ -153,8 +150,7 @@ public sealed class RestoredHullsTests
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
for (var seed = 1u; seed <= 64u; seed++)
{
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
foreach (var zone in galaxy.Zones.Where(z => !ReferenceEquals(z, galaxy.Entrance)))
{
var orbits = ParentedPlanetOrbits(cache, ZoneGenerator.GenerateZone(items, TutorialZoneSettings(), AuthoredPlanetSettings(), galaxy, zone, isTutorial: false, ambient: false));
@ -366,7 +362,6 @@ public sealed class RestoredHullsTests
var zoneSettings = TutorialZoneSettings();
var tutorialSettings = TutorialGalaxySettings();
var background = TutorialBackgroundSettings();
var names = TutorialNameSettings();
var gameData = TestCatalog.Repo;
var sawGenuineLagrangeOrbit = false;
for (uint seed = 1; seed <= 25; seed++)
@ -375,7 +370,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(tutorialSettings, background, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, AuthoredPlanetSettings(), galaxy, galaxy.Entrance, isTutorial: true);
@ -407,7 +402,6 @@ public sealed class RestoredHullsTests
var zoneSettings = TutorialZoneSettings();
var tutorialSettings = TutorialGalaxySettings();
var background = TutorialBackgroundSettings();
var names = TutorialNameSettings();
var gameData = TestCatalog.Repo;
var sawGenuineCandidate = false;
for (uint seed = 1; seed <= 25; seed++)
@ -416,7 +410,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(tutorialSettings, background, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, AuthoredPlanetSettings(), galaxy, galaxy.Entrance, isTutorial: true);
@ -454,8 +448,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var zoneSettings = TutorialZoneSettings();
@ -490,8 +483,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var zoneSettings = TutorialZoneSettings();
@ -532,7 +524,6 @@ public sealed class RestoredHullsTests
var zoneSettings = TutorialZoneSettings();
var tutorialSettings = TutorialGalaxySettings();
var background = TutorialBackgroundSettings();
var names = TutorialNameSettings();
var gameData = TestCatalog.Repo;
var checkedAny = false;
@ -542,7 +533,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(tutorialSettings, background, names, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(tutorialSettings, background, cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var pack = ZoneGenerator.GenerateZone(items, zoneSettings, AuthoredPlanetSettings(), galaxy, galaxy.Entrance, isTutorial: true);
@ -677,8 +668,7 @@ public sealed class RestoredHullsTests
// real zone from a real galaxy (e.g. one already known to have no Lagrange candidate) and make it the Entrance.
private static Galaxy BuildGalaxyWithForcedEntrance(CultCache cache, uint seed, string entranceZoneName)
{
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var target = galaxy.Zones.Single(z => z.Name == entranceZoneName);
var factions = galaxy.Factions;
@ -789,8 +779,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
int StationCount(GalaxyZone zone, bool isTutorial) =>
@ -830,8 +819,7 @@ public sealed class RestoredHullsTests
try
{
using var cache = OpenReadOnlyRealCatalogWithScratchRun(gameData, scratchRun);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed: 1u);
var items = new ItemManager(cache, new ProvenanceLedger(), Settings(), _ => { });
var pack = ZoneGenerator.GenerateZone(items, TutorialZoneSettings(), AuthoredPlanetSettings(), galaxy, galaxy.Entrance, isTutorial: true);
@ -866,8 +854,7 @@ public sealed class RestoredHullsTests
// Must fail if that guard is dropped for real.
private static Galaxy BuildGalaxyWithBoostedPresence(CultCache cache, uint seed, string zoneName)
{
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), TutorialNameSettings(),
cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var galaxy = new Galaxy(TutorialGalaxySettings(), TutorialBackgroundSettings(), cache, new PlayerSettings(), new DirectoryInfo(Path.GetTempPath()), _ => { }, null, seed);
var target = galaxy.Zones.Single(z => z.Name == zoneName);
var factions = galaxy.Factions;

View file

@ -51,7 +51,7 @@ public sealed partial class RunStartTests : IDisposable
_planetSettings = authored.Read<PlanetSettings>("PlanetSettings");
var tutorial = _tutorialSettings = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
_galaxy = new Galaxy(tutorial, authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), _cache, new PlayerSettings(),
_cache, new PlayerSettings(),
Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, GalaxySeed);
_items = new ItemManager(_cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
_protagonist = _galaxy.ResolveFaction(tutorial.ProtagonistFaction);
@ -122,7 +122,7 @@ public sealed partial class RunStartTests : IDisposable
var authored = AuthoredSettings.Load(FindRepoRoot());
return new Galaxy(authored.Read<SectorGenerationSettings>("SectorGenerationSettings"),
authored.Read<SectorBackgroundSettings>("SectorBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), _cache, _ => { }, null, GalaxySeed);
_cache, _ => { }, null, GalaxySeed);
}
// Every item a station carries, equipped or stocked, with the product it was made as.

View file

@ -55,7 +55,6 @@ public sealed partial class RunStartTests
authored.Read<SectorBackgroundSettings>("SectorBackgroundSettings"),
authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"),
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"),
_cache, new PlayerSettings(), Directory.CreateDirectory(Path.Combine(_root, "Narrative")), _ => { }, null, clock);
}

View file

@ -54,7 +54,7 @@ public sealed class StationPlacementTests : IDisposable
var settings = authored.Read<GameplaySettings>("GameplaySettings");
var galaxy = new Galaxy(authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings"),
authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), cache, new PlayerSettings(),
cache, new PlayerSettings(),
Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), settings, _ => { });
stock?.Invoke(items);

View file

@ -183,7 +183,7 @@ public sealed class SunsNeverGreenTests : IDisposable
var authored = AuthoredSettings.Load(RunStartTests.FindRepoRoot());
var tutorial = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
var galaxy = new Galaxy(tutorial, authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings"),
authored.Read<NameGeneratorSettings>("NameGeneratorSettings"), cache, new PlayerSettings(),
cache, new PlayerSettings(),
Directory.CreateDirectory(Path.Combine(dir, "Narrative")), _ => { }, null, seed);
var items = new ItemManager(cache, new ProvenanceLedger(), authored.Read<GameplaySettings>("GameplaySettings"), _ => { });
var zoneSettings = authored.Read<ZoneGenerationSettings>("ZoneSettings");

View file

@ -19,23 +19,22 @@ public static class MinorPowerFactionsCommand
{
// Own: the weights this faction names for the other minor powers; every other faction but the Pirates gets Default.
// Gain: the weight every existing faction (bar the Pirates and Miss Terri's) gains toward this faction.
// Geonames is the name file the faction draws place names from (question minor-power-geonames: the nearest existing file).
internal sealed record Def(string Name, string ShortName, string Description, float3 Primary, float3 Secondary,
string Geonames, int Influence, float Gain, float Default, (string ShortName, float Weight)[] Own);
int Influence, float Gain, float Default, (string ShortName, float Weight)[] Own);
internal static readonly Def[] Factions =
{
new("Aya Collective", "Aya",
"A federation of communes, clinics and repair crews. Its equipment comes with the drawings, so the people it protects can keep it running.",
new float3(.72f, .36f, .22f), new float3(.12f, .36f, .25f), "Congo", 3, .3f, .3f,
new float3(.72f, .36f, .22f), new float3(.12f, .36f, .25f), 3, .3f, .3f,
new[] { ("Forge", 1f), ("Megiddo", .5f) }),
new("Megiddo", "Megiddo",
"A defended fleet that holds its boundary until the question is answered. Others buy its protection without sharing its faith.",
new float3(.07f, .09f, .15f), new float3(.90f, .85f, .74f), "pleiades", 4, .3f, .2f,
new float3(.07f, .09f, .15f), new float3(.90f, .85f, .74f), 4, .3f, .2f,
new[] { ("Aya", .5f), ("Forge", .5f) }),
new("Orbital Forge", "Forge",
"Belt engineers who hollow asteroids into homes and yards. Their standards leave room to adapt them, and their drawings travel further than their ships.",
new float3(.42f, .30f, .22f), new float3(.30f, .38f, .52f), "pleiades", 3, .5f, .5f,
new float3(.42f, .30f, .22f), new float3(.30f, .38f, .52f), 3, .5f, .5f,
Array.Empty<(string, float)>()),
};
@ -51,17 +50,14 @@ public static class MinorPowerFactionsCommand
var apply = args.Contains("apply");
var db = AetherDb.Open(catalogWritable: apply, root: root);
var existing = db.Cache.GetAll<Faction>().ToArray();
var nameFiles = db.Cache.GetAll<NameFile>().ToArray();
// Refuse before any write: a name or short name held by a faction under another key, and a missing name file.
// Refuse before any write: a name or short name held by a faction under another key.
foreach (var def in Factions)
{
var key = KeyOf(def);
foreach (var other in existing.Where(f => f.Name == def.Name || f.ShortName == def.ShortName))
if (!db.Cache.RefOf(other).Key.Equals(key))
throw new InvalidOperationException("Faction Name or ShortName: held by a faction under another key (conflict).");
if (nameFiles.Count(n => n.Name == def.Geonames) != 1)
throw new InvalidOperationException("Faction GeonameFile: expected exactly one name file (missing).");
}
var minorKeys = Factions.ToDictionary(d => d.ShortName, KeyOf);
@ -80,7 +76,7 @@ public static class MinorPowerFactionsCommand
}
foreach (var def in created)
Console.WriteLine($"New faction \"{def.Name}\" ({def.ShortName}): influence {def.Influence}, names \"{def.Geonames}\", colours ({def.Primary.x:0.00}, {def.Primary.y:0.00}, {def.Primary.z:0.00}) / ({def.Secondary.x:0.00}, {def.Secondary.y:0.00}, {def.Secondary.z:0.00})\n {def.Description}\n allegiance: {string.Join(", ", def.Own.Select(o => $"{o.ShortName} {o.Weight:0.##}"))} others {def.Default:0.##}");
Console.WriteLine($"New faction \"{def.Name}\" ({def.ShortName}): influence {def.Influence}, colours ({def.Primary.x:0.00}, {def.Primary.y:0.00}, {def.Primary.z:0.00}) / ({def.Secondary.x:0.00}, {def.Secondary.y:0.00}, {def.Secondary.z:0.00})\n {def.Description}\n allegiance: {string.Join(", ", def.Own.Select(o => $"{o.ShortName} {o.Weight:0.##}"))} others {def.Default:0.##}");
foreach (var (holder, missing) in gains)
Console.WriteLine($" {holder.ShortName} gains {string.Join(", ", missing.Select(d => $"{d.ShortName} {d.Gain:0.##}"))}");
@ -101,7 +97,7 @@ public static class MinorPowerFactionsCommand
{
Name = def.Name, ShortName = def.ShortName, Description = def.Description,
PrimaryColor = def.Primary, SecondaryColor = def.Secondary,
GeonameFile = db.Cache.RefOf(nameFiles.Single(n => n.Name == def.Geonames)), InfluenceDistance = def.Influence,
InfluenceDistance = def.Influence,
};
foreach (var reference in everyone) faction.Allegiance[reference] = def.Default;
foreach (var other in Factions.Where(o => o != def))

View file

@ -0,0 +1,126 @@
/* 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;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GameCult.Caching;
// place-names [apply] (aetheria-release, cut faction-curated-place-names; ruling faction-curated-place-names):
// authors every curated faction's Places from the table below, which is the one source of those names. A faction is
// identified by its record key, and its ShortName must agree (ghostlight-verse ruling faction-identity-is-record-key).
// A faction without a row keeps the catalogue designations Galaxy.NamePlaces falls back to.
// Order is meaning: the first name is the faction's capital, and the home zone takes it. Dry run unless passed "apply".
public static class PlaceNamesCommand
{
// List holds the names separated by " | ", in authored order.
internal sealed record Row(string RecordKey, string ShortName, string List)
{
public string[] Names => List.Split(" | ");
}
internal static readonly Row[] Table =
{
new("bbd619ed-76c1-464c-829d-3f419cdc2525", "Adrasteia",
"Shunya | Unlisted | Tamas | Silent Address | Maya | The Warm Hour | Nishabda | The Vouch | Akasha | No Such Node | Chhaya | Heat Death Reach | Santulan | Cold Margin | Antara | Without Light | Nirvata | Redacted Deep | Quiet Reach | Last Known"),
new("8443f966-0d88-4ec7-a20e-8edf73fa62f6", "AU",
"Ceres Ramp | Pallas Provisional | Ramp Two | Claimshare | Vesta Works | Hygiea | Bloom Seven | Half Measure | Further Out | Semi-Permanent | Under Construction | Drawing Board | Survey Pending | Ceres Control | Margin of Error | Proof of Concept | Ramp Nine | Good Enough | Ambition | Not Yet Named"),
new("a1f76631-d6d1-443a-9be1-c9b1267d6ca2", "Alakrita",
"Aurea | Celeritas | Elegantia | Via Alakrita | Fatum | Quiet Bay | Laurea | Gloria | Prima Linea | Destiny Yard | Aequor | Corona | Vela | Lumen | Nobilis | Portus Aureus | Inscription | Tessera | Proportio | Decus"),
new("48bbb7537ff9ed0a73c3c353598e8f69", "Aya",
"Hellas | Sanctuary | Common Kitchen | Open Courtyard | Enceladus Commons | Mutual | Assembly Hall | Repair Circle | Refuge Route | Not Inevitable | Free Clinic | Shared Table | Drawings Included | All Welcome | Congress | Keep It Running | Terracotta | Open Door | Freehold Garden | Green Shade"),
// DME's voice is lowercase on purpose.
new("8419f9bc-0167-46ec-86c3-3265d2905fbc", "DME",
"the choir loft | encore | burn it down | turn it up | void communion | last call | margin poem | basement tape | loud room | second encore | feedback | no permit | the pit | spare amp | open mic | after hours | smoke machine | b-side | green room | yara's version"),
new("a9672650-c335-484a-9555-71a35c841aa1", "Ewan Hart",
"Homestead | Second Harvest | Convoy's End | Invented Spring | Grange Hall | Long Furrow | Silo Nine | Good Dog | Feed and Seed | False October | County Line | Still Growing | Tractor Pull | Prairie Bloom | Rain Check | Elevator | Hayloft | Late Frost | Sealed Summer | Old Blue"),
new("1dfaa629-6c63-42a7-91e1-f1ae90f92f09", "Finch",
"Tycho | Serenity Ward | Tranquility | Far Side | Nectaris Clinic | Grace | Crisium | Mireille | Copernicus | Imbrium | Full Coverage | Aitken | Fecunditas | Vaporum | Recovery Ring | Clavius | Earthrise | Second Opinion | Frigoris | Plato"),
new("2f74e6b4-d268-48a9-b2a8-24591c680e84", "Lightsail",
"Mile Zero | Ganymede Stop | Last Anchorage | Dispatch | The Drop Book | Truck Stop | Route Choir | On Time | Callisto Crossing | Haul Road | Weigh Station | Long Haul | Bill of Lading | Overnight | Deadhead | Jake Brake | Signed For | The Line | Breaker Nineteen | Final Mile"),
new("a012e0f4-4b22-47b7-8b24-ff3e1439c949", "Lucent",
"Ishtar Studios | Prime Time | Maxwell Heights | Cloud Nine Live | Channel Aphrodite | Clout | Buzz Market | Ratings Tower | Lagrange Feed | Spotlight | Exclusive | Trending | Broadcast Row | Red Carpet | Season Finale | Sponsored | Headliner | Viral | Golden Hour | Breaking News"),
new("087df3f89e59beab14f9d41c849b202f", "Megiddo",
"First Vigil | Earth Stars | Continuity | The Long Silence | Deferred Ground | Unverified | Boundary Watch | Custody | Not Our World | Pleiades Watch | Second Vault | Presumption | Under Orion | Night Watch | Witness | Flatland Edge | Old Sky | Record Keeper | Patience | Revelation Pending"),
// Real Australian place names, Indigenous-derived and English, a draft Emily may replace (ruling miss-terris-improvised-places).
new("d6466821-6776-4f40-9609-2342debf5c86", "Miss Terri's",
"Woolloomooloo | Wagga Wagga | Humpty Doo | Come By Chance | Yackandandah | Nowhere Else | Kurri Kurri | Lightning Ridge | Mooloolaba | Woy Woy | Banana | Coober Pedy | Gundagai | Burpengary | Toowoomba | Kalgoorlie | Bald Hills | Wollongong | Innaloo | Ballarat"),
new("73d368dd-58a8-4431-b9ba-2d44754d36a1", "NiteLife",
"Caloris Array | Arete Grid | Malam | Cahaya | Graveyard Shift | Lights On | Bintang | Night Count | Pelita | Uptime Pro | Bulan | Grid Plus | Kilat | Afterglow | Load Balance | Peak Hours | Terang | Version Two | Midnight Blue | Keep On"),
new("e34d0354f46873df7ec27273fa2abe3d", "Forge",
"Hollow Pallas | Cinderlace | Stubborn | Open Drawing | Pattern Yard | Bored Through | Slagside | Tolerance | Free Standard | Lathe | Anvil Rock | Copied Twice | Mandrel | Inner Shell | Quench | Forge Light | Billet | Rivet | Blueprint | Worlds Enough"),
// The two emoji are U+1F3F4 and U+1F99C (ruling pirates-record-content).
new("1d8a168def994a76a1ce7cb18f59c038", "Pirates",
"Trojan Nest | Genuine Rescue | Ring Shadow | Patched Cavity | \U0001F3F4 Nobody's | Repainted | Finders Keepers | Hidden Dock | Free Parking | Legit Waystation | New Management | No Questions | Second Hand | False Beacon | Salvage Rights | The Fence | Elegance. Ours. | \U0001F99C Roost | Maintenance Due | Not A Base"),
new("adfb9b08-aabc-45c6-9586-915698a9c4b0", "R&D",
"Prüfstand | Findings | Recertification | Configuration B | Asterisk | Freigabe | Friendly | Sicherheit | Compliance | Revision C | Normal Operation | Abnahme | Test Bench Four | Fine Print | Hold Music | Reassurance | Warranty | Grundlage | Standard Procedure | Kinderleicht"),
new("16bad4f9-7fb5-41c5-8da0-dc76709eb7d3", "Zhestokost",
"Cimmeria | Tharsis Bastion | Olimp | Molot | Valles Forward | Arsenal Commission | Eternal Council | Moneta | Kuznya | Overkill | Assignment Board | Stal | Krepost | Gromada | Campaign | Frontline | Beton | Plamya | Zhelezo | Pobeda"),
};
public static int Run(string[] args, string root = null) => Run(args, root, Table);
// The table is a parameter so a test can hand it a malformed one; production always passes Table.
internal static int Run(string[] args, string root, Row[] table)
{
var apply = args.Contains("apply");
var db = AetherDb.Open(catalogWritable: apply, root: root);
var factions = db.Cache.GetAll<Faction>().ToArray();
var keyOf = factions.ToDictionary(f => db.Cache.RefOf(f).Key.Value, StringComparer.Ordinal);
// Refuse before any write. A refusal never echoes the offending value.
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var row in table)
{
if (!keyOf.TryGetValue(row.RecordKey, out var faction))
throw new InvalidOperationException("Place names row: its key is not a faction in the catalog (missing).");
if (faction.ShortName != row.ShortName)
throw new InvalidOperationException("Place names row: its ShortName differs from the faction under its key (conflict).");
foreach (var name in row.Names)
{
if (!FactionPlace.IsValidName(name))
throw new InvalidOperationException("Place names row: a name is empty, untrimmed or a catalogue designation (invalid).");
if (!seen.Add(name))
throw new InvalidOperationException("Place names table: a name is repeated (conflict).");
}
}
var rows = table.ToDictionary(r => r.RecordKey, StringComparer.Ordinal);
var changed = table.Where(r => !keyOf[r.RecordKey].Places.Select(p => p.Name).SequenceEqual(r.Names)).ToArray();
foreach (var faction in factions.OrderBy(f => f.Name, StringComparer.Ordinal))
{
if (!rows.TryGetValue(db.Cache.RefOf(faction).Key.Value, out var row))
{
Console.WriteLine($"{faction.Name}: no list: catalogue designations");
continue;
}
Console.WriteLine($"{faction.Name}: {row.Names.Length} places, capital {row.Names[0]}, {(changed.Contains(row) ? "changed" : "unchanged")}");
}
if (changed.Length == 0)
{
Console.WriteLine("nothing to do");
return 0;
}
if (!apply)
{
Console.WriteLine($"\nDry run. Pass \"apply\" to author {changed.Length} place lists.");
return 0;
}
if (!db.Cache.Commit(batch =>
{
foreach (var row in changed)
{
var faction = keyOf[row.RecordKey];
faction.Places = row.Names.Select(n => new FactionPlace { Name = n }).ToArray();
batch.Upsert(typeof(Faction), faction, db.Cache.RefOf(faction).Key);
}
}))
throw new InvalidOperationException("The catalog changed while place names were being authored; nothing was written. Run it again.");
Console.WriteLine($"\nAuthored {changed.Length} place lists in Aetheria.cc");
return 0;
}
}

View file

@ -58,9 +58,12 @@ public static class Program
case "product-lines": return ProductLinesCommand.Run(args.Skip(1).ToArray());
case "minor-power-factions": return MinorPowerFactionsCommand.Run(args.Skip(1).ToArray());
case "assembly-from-maker": return AssemblyFromMakerCommand.Run(AetherDb.Open(catalogWritable: args.Contains("apply")).Cache, args.Contains("apply"), Console.Out);
case "place-names":
Console.OutputEncoding = new System.Text.UTF8Encoding(false);
return PlaceNamesCommand.Run(args.Skip(1).ToArray());
case "doctrine-catalog": return DoctrineCatalog.Run(AetherDb.Open(catalogWritable: args.Contains("apply")).Cache, args.Contains("apply"), Console.Out);
default:
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], minor-power-factions [apply], product-lines weapons|minor-powers|gear [apply], doctrine-catalog [apply], assembly-from-maker [apply]");
Console.WriteLine("commands: census, factions, station-fit, hardpoint-fit, loadout [seed], save, settings, settings-dump, dangling [clear <Type.Member>]... [apply], shield-migrate [apply], brownout-migrate [apply], roles-migrate [apply], firing-arc-migrate [apply], range-falloff [apply] [--effective <fraction>] [--end <multiplier>], targeting-catalog [apply], targeting-catalog-6c [apply], targeting-catalog-6d [apply], ship-authoring create|inspect|validate|compose, field-kinds [apply], mine-launcher [apply], smoke-hull [apply], smoke-rounds [apply], roles-backfill [apply], pd-gear [apply], consumables-first-set [apply], launch-suppressor [apply], pirates-faction [apply], verse-grammar [apply], generic-designs [apply], sensor-stat-set [apply], inspection-scanner [apply], minor-power-factions [apply], place-names [apply], product-lines weapons|minor-powers|gear [apply], doctrine-catalog [apply], assembly-from-maker [apply]");
return 1;
}
}
@ -542,7 +545,6 @@ public static class Program
var tutorial = authored.Read<TutorialGenerationSettings>("TutorialGenerationSettings");
var background = authored.Read<SectorBackgroundSettings>("TutorialBackgroundSettings");
var names = authored.Read<NameGeneratorSettings>("NameGeneratorSettings");
var zones = authored.Read<ZoneGenerationSettings>("ZoneSettings");
var gameplay = authored.Read<GameplaySettings>("GameplaySettings");
@ -552,7 +554,6 @@ public static class Program
$"{tutorial.ZoneCount} zones, link density {tutorial.LinkDensity}");
Console.WriteLine($"background: frequency {background.NoiseFrequency}, amplitude {background.NoiseAmplitude}, " +
$"cloud exponent {background.CloudExponent}, density at centre {background.CloudDensity(new float2(.5f)):0.000}");
Console.WriteLine($"names: order {names.NameGeneratorOrder}, length {names.NameGeneratorMinLength}-{names.NameGeneratorMaxLength}");
Console.WriteLine($"zones: sun mass {zones.SunMass}, planet mass {zones.PlanetMass}, satellite passes {zones.SatellitePasses}, " +
$"belt probability {zones.BeltProbability}");
Console.WriteLine($" zone radius curve {zones.ZoneRadius?.Minimum}-{zones.ZoneRadius?.Maximum} exponent {zones.ZoneRadius?.Exponent}");
@ -694,23 +695,22 @@ public static class Program
return false;
}
// Each faction's generation-critical links. Galaxy.GenerateNames dereferences the geoname file without a
// guard, so an unset link there stops galaxy generation outright.
// Each faction's generation-critical links and the size of its curated place list. An empty list is not broken:
// Galaxy.NamePlaces falls back to catalogue designations for a faction without one.
private static int Factions()
{
var db = AetherDb.Open();
var products = db.Cache.GetAll<FactionProductData>().ToArray();
Console.WriteLine($"{"faction",-26} {"short",-13} {"geonames",-22} {"boss hull",-14} {"influence",-9} {"doctrine",-10} products");
Console.WriteLine($"{"faction",-26} {"short",-13} {"places",-22} {"boss hull",-14} {"influence",-9} {"doctrine",-10} products");
var broken = 0;
foreach (var faction in db.Cache.GetAll<Faction>().OrderBy(f => f.Name))
{
var key = db.Cache.RefOf(faction).Key;
var geonames = faction.GeonameFile.IsSet() ? "set" : "UNSET";
var boss = !faction.BossHull.IsSet()
? "none"
: db.Cache.Get(faction.BossHull)?.Name ?? "DANGLING";
if (geonames == "UNSET" || boss == "DANGLING") broken++;
Console.WriteLine($"{faction.Name,-26} {faction.ShortName,-13} {geonames,-22} {boss,-14} {faction.InfluenceDistance,-9} {(faction.Doctrine == null ? "none" : faction.Doctrine.EngageOn.ToString()),-10} " +
if (boss == "DANGLING") broken++;
Console.WriteLine($"{faction.Name,-26} {faction.ShortName,-13} {faction.Places.Length,-22} {boss,-14} {faction.InfluenceDistance,-9} {(faction.Doctrine == null ? "none" : faction.Doctrine.EngageOn.ToString()),-10} " +
products.Count(p => p.Manufacturer.Key.Equals(key)));
}
Console.WriteLine($"\n{broken} factions carry a link that would break generation");
@ -2303,9 +2303,6 @@ public static class Program
string CodePoints(string text) => string.Join(" ", text.EnumerateRunes().Select(r => $"U+{r.Value:X4}"));
var nameFile = db.Cache.GetAll<NameFile>().FirstOrDefault(n => n.Name == "pleiades");
if (nameFile == null) throw new InvalidOperationException("No name file named \"pleiades\".");
var others = db.Cache.GetAll<Faction>().ToArray();
var pirates = new Faction
{
@ -2314,7 +2311,6 @@ public static class Program
Description = "A loose coalition of crews and hidden docks moving people and goods outside official control. No command, no common law: possession has a maintenance schedule.",
PrimaryColor = new float3(.42f, .40f, .37f),
SecondaryColor = new float3(1f, .45f, .05f),
GeonameFile = db.Cache.RefOf(nameFile),
InfluenceDistance = 3,
};
foreach (var other in AllegianceGaps(db, pirates, others)) pirates.Allegiance[db.Cache.RefOf(other)] = 1f;
@ -2338,7 +2334,7 @@ public static class Program
Roles: (design.Roles ?? new List<ItemRole>()).Select(role => new ProductRole { Role = role.Name, Mean = PirateProfile.Mean, StandardDeviation = PirateProfile.Dev }).ToList());
}).ToArray();
Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, names \"{nameFile.Name}\", allegiance 1.0 to {pirates.Allegiance.Count} factions.");
Console.WriteLine($"New faction \"{pirates.Name}\": influence {pirates.InfluenceDistance}, allegiance 1.0 to {pirates.Allegiance.Count} factions.");
Console.WriteLine($" colours: primary ({pirates.PrimaryColor.x:0.00}, {pirates.PrimaryColor.y:0.00}, {pirates.PrimaryColor.z:0.00}) primer grey, secondary ({pirates.SecondaryColor.x:0.00}, {pirates.SecondaryColor.y:0.00}, {pirates.SecondaryColor.z:0.00}) hazard orange");
Console.WriteLine($" {pirates.Description}");
Console.WriteLine($"\nNew products ({products.Length}):");