Files
ZeroSpace/ZeroSpace/ZeroSpace.Build/Program.cs
T

974 lines
31 KiB
C#

using System.Globalization;
using System.Text;
var solutionRoot = FindSolutionRoot();
var docsRoot = args.Length > 0
? Path.GetFullPath(args[0])
: Path.GetFullPath(Path.Combine(solutionRoot, "..", "zs.docs"));
var modelRoot = Path.Combine(solutionRoot, "ZeroSpace.Model");
var outputPath = Path.Combine(modelRoot, "Units.g.cs");
if (!Directory.Exists(docsRoot))
{
throw new DirectoryNotFoundException($"Could not find docs directory: {docsRoot}");
}
if (!Directory.Exists(modelRoot))
{
throw new DirectoryNotFoundException($"Could not find model project directory: {modelRoot}");
}
var units = new List<UnitSource>();
var skipped = new List<string>();
foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories).Order(StringComparer.OrdinalIgnoreCase))
{
var frontMatter = ReadFrontMatter(path);
if (frontMatter is null)
{
continue;
}
if (!IsUnit(frontMatter))
{
continue;
}
if (!TryReadUnit(path, frontMatter, out var unit, out var reason))
{
skipped.Add($"{Path.GetFileName(path)} ({reason})");
continue;
}
units.Add(unit);
}
units.Sort((left, right) =>
{
var faction = string.Compare(left.Faction, right.Faction, StringComparison.OrdinalIgnoreCase);
if (faction != 0)
{
return faction;
}
var tier = left.Tier.CompareTo(right.Tier);
return tier != 0
? tier
: string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase);
});
File.WriteAllText(outputPath, GenerateUnits(units), Encoding.UTF8);
Console.WriteLine($"Generated {units.Count} units: {Path.GetRelativePath(solutionRoot, outputPath)}");
if (skipped.Count > 0)
{
Console.WriteLine($"Skipped {skipped.Count} incomplete unit files:");
foreach (var entry in skipped.Order(StringComparer.OrdinalIgnoreCase))
{
Console.WriteLine($"- {entry}");
}
}
var starSystems = new List<StarSystemSource>();
var systemMasteries = new List<SystemMasterySource>();
var planetaryMasteryItems = new List<PlanetaryMasteryItemSource>();
var skippedDocs = new List<string>();
var usedIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var path in Directory.EnumerateFiles(docsRoot, "*.md", SearchOption.AllDirectories).Order(StringComparer.OrdinalIgnoreCase))
{
var (frontMatter, body) = ReadDocument(path);
if (frontMatter is null || !frontMatter.TryGetValue("category", out var categoryValue))
{
continue;
}
if (string.Equals(categoryValue.Scalar, "StarSystem", StringComparison.OrdinalIgnoreCase))
{
if (TryReadStarSystem(path, frontMatter, body, usedIds, out var system, out var reason))
{
starSystems.Add(system);
}
else
{
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
}
}
else if (string.Equals(categoryValue.Scalar, "SystemMastery", StringComparison.OrdinalIgnoreCase))
{
if (TryReadSystemMastery(path, frontMatter, body, usedIds, out var mastery, out var reason))
{
systemMasteries.Add(mastery);
}
else
{
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
}
}
else if (string.Equals(categoryValue.Scalar, "PlanetaryMasteryItem", StringComparison.OrdinalIgnoreCase))
{
if (TryReadPlanetaryMasteryItem(path, frontMatter, body, usedIds, out var item, out var reason))
{
planetaryMasteryItems.Add(item);
}
else
{
skippedDocs.Add($"{Path.GetFileName(path)} ({reason})");
}
}
}
starSystems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
systemMasteries.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
planetaryMasteryItems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase));
var starSystemsPath = Path.Combine(modelRoot, "StarSystems.g.cs");
var systemMasteriesPath = Path.Combine(modelRoot, "SystemMasteries.g.cs");
var planetaryMasteryItemsPath = Path.Combine(modelRoot, "PlanetaryMasteryItems.g.cs");
File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8);
File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8);
File.WriteAllText(planetaryMasteryItemsPath, GeneratePlanetaryMasteryItems(planetaryMasteryItems), Encoding.UTF8);
Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}");
Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}");
Console.WriteLine($"Generated {planetaryMasteryItems.Count} planetary mastery items: {Path.GetRelativePath(solutionRoot, planetaryMasteryItemsPath)}");
// Copy item icons next to the markdown docs into the web app's wwwroot so the
// items page can reference them. Images are copied, never moved: the docs folder
// stays the source of truth.
var itemImagesDir = Path.Combine(solutionRoot, "ZeroSpace.Web", "wwwroot", "images", "planetary-mastery-items");
Directory.CreateDirectory(itemImagesDir);
var expectedImages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var copiedImages = 0;
foreach (var item in planetaryMasteryItems)
{
if (item.ImageFileName is null || item.ImageSourcePath is null)
{
continue;
}
expectedImages.Add(item.ImageFileName);
var destination = Path.Combine(itemImagesDir, item.ImageFileName);
File.Copy(item.ImageSourcePath, destination, overwrite: true);
copiedImages++;
}
foreach (var stale in Directory.EnumerateFiles(itemImagesDir, "*.png"))
{
if (!expectedImages.Contains(Path.GetFileName(stale)))
{
File.Delete(stale);
Console.WriteLine($"Removed stale item image: {Path.GetRelativePath(solutionRoot, stale)}");
}
}
Console.WriteLine($"Copied {copiedImages} planetary mastery item images to {Path.GetRelativePath(solutionRoot, itemImagesDir)}");
if (skippedDocs.Count > 0)
{
Console.WriteLine($"Skipped {skippedDocs.Count} incomplete docs:");
foreach (var entry in skippedDocs.Order(StringComparer.OrdinalIgnoreCase))
{
Console.WriteLine($"- {entry}");
}
}
static string FindSolutionRoot()
{
var candidates = new[] { Environment.CurrentDirectory, AppContext.BaseDirectory };
foreach (var candidate in candidates)
{
var directory = new DirectoryInfo(candidate);
while (directory is not null)
{
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace.sln")))
{
return directory.FullName;
}
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace", "ZeroSpace.sln")))
{
return Path.Combine(directory.FullName, "ZeroSpace");
}
directory = directory.Parent;
}
}
throw new DirectoryNotFoundException("Could not find ZeroSpace.sln in the current directory or its parents.");
}
static Dictionary<string, FrontMatterValue>? ReadFrontMatter(string path)
{
var lines = File.ReadAllLines(path);
if (lines.Length == 0 || lines[0].Trim() != "---")
{
return null;
}
var values = new Dictionary<string, FrontMatterValue>(StringComparer.OrdinalIgnoreCase);
string? listKey = null;
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
if (line.Trim() == "---")
{
return values;
}
if (listKey is not null && line.StartsWith(" - ", StringComparison.Ordinal))
{
values[listKey].List.Add(CleanValue(line[4..]));
continue;
}
listKey = null;
var separatorIndex = line.IndexOf(':');
if (separatorIndex < 0)
{
continue;
}
var key = line[..separatorIndex].Trim();
var rawValue = line[(separatorIndex + 1)..].Trim();
if (rawValue.Length == 0)
{
values[key] = new FrontMatterValue(null);
listKey = key;
continue;
}
values[key] = new FrontMatterValue(CleanValue(rawValue));
}
return null;
}
static bool IsUnit(IReadOnlyDictionary<string, FrontMatterValue> frontMatter)
{
return frontMatter.TryGetValue("category", out var category)
&& string.Equals(category.Scalar, "Unit", StringComparison.OrdinalIgnoreCase);
}
static bool TryReadUnit(
string path,
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
out UnitSource unit,
out string reason)
{
unit = default!;
if (!TryGetRequiredString(frontMatter, "Faction", out var faction, out reason) ||
!TryGetRequiredInt(frontMatter, "hexite", out var hexite, out reason) ||
!TryGetRequiredInt(frontMatter, "Flux", out var flux, out reason) ||
!TryGetRequiredInt(frontMatter, "supply", out var supply, out reason) ||
!TryGetRequiredInt(frontMatter, "productionTime", out var productionTime, out reason) ||
!TryGetRequiredInt(frontMatter, "health", out var health, out reason) ||
!TryGetRequiredInt(frontMatter, "Energy", out var energy, out reason) ||
!TryGetRequiredInt(frontMatter, "Shields", out var shields, out reason) ||
!TryGetRequiredInt(frontMatter, "Armor rating", out var armorRating, out reason) ||
!TryGetRequiredInt(frontMatter, "movementSpeed", out var movementSpeed, out reason) ||
!TryGetRequiredInt(frontMatter, "damagePerSecond", out var damagePerSecond, out reason) ||
!TryGetRequiredInt(frontMatter, "attackRange", out var attackRange, out reason) ||
!TryGetRequiredInt(frontMatter, "Tier", out var tier, out reason) ||
!TryGetRequiredInt(frontMatter, "buildAtSameTime", out var buildAtSameTime, out reason))
{
return false;
}
var id = ToIdentifier(Path.GetFileNameWithoutExtension(path));
var hotkey = TryGetOptionalString(frontMatter, "Hotkey");
var attributes = frontMatter.TryGetValue("attributes", out var attributeValue)
? attributeValue.List.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray()
: [];
var limit = TryGetOptionalInt(frontMatter, "limit");
unit = new UnitSource(
id,
Path.GetFileNameWithoutExtension(path),
hexite,
flux,
supply,
productionTime,
health,
energy,
shields,
armorRating,
movementSpeed,
damagePerSecond,
attackRange,
attributes,
tier,
faction,
hotkey,
buildAtSameTime,
limit);
reason = string.Empty;
return true;
}
static bool TryGetRequiredString(
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
string key,
out string value,
out string reason)
{
if (!frontMatter.TryGetValue(key, out var frontMatterValue) ||
string.IsNullOrWhiteSpace(frontMatterValue.Scalar))
{
value = string.Empty;
reason = $"missing {key}";
return false;
}
value = frontMatterValue.Scalar;
reason = string.Empty;
return true;
}
static string? TryGetOptionalString(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
{
return frontMatter.TryGetValue(key, out var frontMatterValue) &&
!string.IsNullOrWhiteSpace(frontMatterValue.Scalar)
? frontMatterValue.Scalar
: null;
}
static bool TryGetRequiredInt(
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
string key,
out int value,
out string reason)
{
if (!frontMatter.TryGetValue(key, out var frontMatterValue) ||
string.IsNullOrWhiteSpace(frontMatterValue.Scalar))
{
value = default;
reason = $"missing {key}";
return false;
}
if (!int.TryParse(frontMatterValue.Scalar, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
{
reason = $"invalid {key}";
return false;
}
reason = string.Empty;
return true;
}
static int? TryGetOptionalInt(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
{
if (!frontMatter.TryGetValue(key, out var frontMatterValue) ||
string.IsNullOrWhiteSpace(frontMatterValue.Scalar))
{
return null;
}
return int.TryParse(frontMatterValue.Scalar, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
? value
: null;
}
static string CleanValue(string value)
{
value = value.Trim();
if (value.Length >= 2 && value[0] == '"' && value[^1] == '"')
{
value = value[1..^1];
}
if (value.StartsWith("[[", StringComparison.Ordinal) && value.EndsWith("]]", StringComparison.Ordinal))
{
value = value[2..^2];
}
return value;
}
static string ToIdentifier(string name)
{
var builder = new StringBuilder();
var capitalizeNext = true;
foreach (var character in name)
{
if (char.IsLetterOrDigit(character))
{
builder.Append(capitalizeNext ? char.ToUpperInvariant(character) : character);
capitalizeNext = false;
}
else
{
capitalizeNext = true;
}
}
if (builder.Length == 0)
{
return "Unit";
}
if (char.IsDigit(builder[0]))
{
builder.Insert(0, 'U');
}
return builder.ToString();
}
static string GenerateUnits(IReadOnlyList<UnitSource> units)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("namespace ZeroSpace.Model;");
builder.AppendLine();
builder.AppendLine("public static class Units");
builder.AppendLine("{");
foreach (var unit in units)
{
builder.AppendLine($" public static readonly UnitData {unit.Id} = new(");
builder.AppendLine($" Id: {Literal(unit.Id)},");
builder.AppendLine($" Name: {Literal(unit.Name)},");
builder.AppendLine($" Hexite: {unit.Hexite.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Flux: {unit.Flux.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Supply: {unit.Supply.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" ProductionTime: {unit.ProductionTime.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Health: {unit.Health.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Energy: {unit.Energy.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Shields: {unit.Shields.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" ArmorRating: {unit.ArmorRating.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" MovementSpeed: {unit.MovementSpeed.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" DamagePerSecond: {unit.DamagePerSecond.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" AttackRange: {unit.AttackRange.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Attributes: [{string.Join(", ", unit.Attributes.Select(Literal))}],");
builder.AppendLine($" Tier: {unit.Tier.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Faction: {Literal(unit.Faction)},");
builder.AppendLine($" Hotkey: {Literal(unit.Hotkey)},");
builder.AppendLine($" BuildAtSameTime: {unit.BuildAtSameTime.ToString(CultureInfo.InvariantCulture)},");
builder.AppendLine($" Limit: {unit.Limit?.ToString(CultureInfo.InvariantCulture) ?? "null"});");
builder.AppendLine();
}
builder.AppendLine(" public static IReadOnlyList<UnitData> All { get; } =");
builder.AppendLine(" [");
foreach (var unit in units)
{
builder.AppendLine($" {unit.Id},");
}
builder.AppendLine(" ];");
builder.AppendLine("}");
return builder.ToString();
}
static string Literal(string? value)
{
if (value is null)
{
return "null";
}
var builder = new StringBuilder("\"");
foreach (var character in value)
{
builder.Append(character switch
{
'\\' => "\\\\",
'"' => "\\\"",
'\n' => "\\n",
'\r' => "\\r",
'\t' => "\\t",
_ => character.ToString(),
});
}
builder.Append('"');
return builder.ToString();
}
static (Dictionary<string, FrontMatterValue>? FrontMatter, string Body) ReadDocument(string path)
{
var lines = File.ReadAllLines(path);
if (lines.Length == 0 || lines[0].Trim() != "---")
{
return (null, string.Empty);
}
var values = new Dictionary<string, FrontMatterValue>(StringComparer.OrdinalIgnoreCase);
string? listKey = null;
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
if (line.Trim() == "---")
{
var body = string.Join('\n', lines, i + 1, lines.Length - i - 1).Trim();
return (values, body);
}
if (listKey is not null && line.StartsWith(" - ", StringComparison.Ordinal))
{
values[listKey].List.Add(CleanValue(line[4..]));
continue;
}
listKey = null;
var separatorIndex = line.IndexOf(':');
if (separatorIndex < 0)
{
continue;
}
var key = line[..separatorIndex].Trim();
var rawValue = line[(separatorIndex + 1)..].Trim();
if (rawValue.Length == 0)
{
values[key] = new FrontMatterValue(null);
listKey = key;
continue;
}
values[key] = new FrontMatterValue(CleanValue(rawValue));
}
return (null, string.Empty);
}
static bool IsTrue(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
{
return frontMatter.TryGetValue(key, out var value)
&& string.Equals(value.Scalar, "true", StringComparison.OrdinalIgnoreCase);
}
static string ToUniqueIdentifier(string name, HashSet<string> usedIds)
{
var id = ToIdentifier(name);
if (usedIds.Add(id))
{
return id;
}
var suffix = 2;
while (!usedIds.Add($"{id}{suffix}"))
{
suffix++;
}
return $"{id}{suffix}";
}
static bool TryReadStarSystem(
string path,
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
string body,
HashSet<string> usedIds,
out StarSystemSource system,
out string reason)
{
system = default!;
string category;
if (frontMatter.TryGetValue("systemCategory", out var categoryVal) && !string.IsNullOrWhiteSpace(categoryVal.Scalar))
{
var catStr = categoryVal.Scalar.Trim();
if (catStr.Equals("Homeworld System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Homeworld", StringComparison.OrdinalIgnoreCase))
{
category = "HomeworldSystem";
}
else if (catStr.Equals("Core System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Core", StringComparison.OrdinalIgnoreCase))
{
category = "CoreSystem";
}
else if (catStr.Equals("Side System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Side", StringComparison.OrdinalIgnoreCase))
{
category = "SideSystem";
}
else
{
reason = $"unknown systemCategory '{catStr}'";
return false;
}
}
else
{
var kindCount = 0;
category = string.Empty;
if (IsTrue(frontMatter, "isSideSystem"))
{
category = "SideSystem";
kindCount++;
}
if (IsTrue(frontMatter, "isHomeworldSystem"))
{
category = "HomeworldSystem";
kindCount++;
}
if (IsTrue(frontMatter, "isCoreSystem"))
{
category = "CoreSystem";
kindCount++;
}
if (kindCount != 1)
{
reason = "missing or invalid systemCategory";
return false;
}
}
var owner = TryGetOptionalString(frontMatter, "owner") ?? "The Eden Accord";
var name = Path.GetFileNameWithoutExtension(path);
var connectedSystems = frontMatter.TryGetValue("connectedSystems", out var connectedValue)
? connectedValue.List.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray()
: [];
system = new StarSystemSource(
ToUniqueIdentifier(name, usedIds),
name,
category,
owner,
IsTrue(frontMatter, "higherInfluenceYield"),
IsTrue(frontMatter, "higherCreditYield"),
IsTrue(frontMatter, "higherEnergyYield"),
IsTrue(frontMatter, "higherChanceForItems"),
TryGetOptionalString(frontMatter, "occupationBuff"),
TryGetOptionalString(frontMatter, "systemMastery"),
connectedSystems,
GetDouble(frontMatter, "x"),
GetDouble(frontMatter, "y"),
body);
reason = string.Empty;
return true;
}
static bool TryReadSystemMastery(
string path,
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
string body,
HashSet<string> usedIds,
out SystemMasterySource mastery,
out string reason)
{
mastery = default!;
var levels = new string[5];
for (var level = 1; level <= 5; level++)
{
if (!TryGetRequiredString(frontMatter, $"level_{level}", out var text, out reason))
{
return false;
}
levels[level - 1] = text;
}
var availableSystems = frontMatter.TryGetValue("availableSystems", out var availableValue)
? availableValue.List.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray()
: [];
var stats = new List<(string Key, double Value)>();
foreach (var (key, value) in frontMatter)
{
if (IsMasteryField(key))
{
continue;
}
if (string.IsNullOrWhiteSpace(value.Scalar) ||
!double.TryParse(value.Scalar, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number))
{
Console.WriteLine($"Warning: {Path.GetFileName(path)} has non-numeric stat '{key}', skipping it.");
continue;
}
stats.Add((key, number));
}
var name = Path.GetFileNameWithoutExtension(path);
mastery = new SystemMasterySource(
ToUniqueIdentifier(name, usedIds),
name,
levels,
availableSystems,
stats,
body);
reason = string.Empty;
return true;
}
static bool TryReadPlanetaryMasteryItem(
string path,
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
string body,
HashSet<string> usedIds,
out PlanetaryMasteryItemSource item,
out string reason)
{
item = default!;
var levels = new string[5];
for (var level = 1; level <= 5; level++)
{
if (!TryGetRequiredString(frontMatter, $"level_{level}", out var text, out reason))
{
return false;
}
levels[level - 1] = text;
}
var name = Path.GetFileNameWithoutExtension(path);
// An icon with the same file name as the doc (e.g. "Battery.png" next to
// "Battery.md") is treated as this item's image when present.
string? imageFileName = null;
string? imageSourcePath = null;
var candidateImagePath = Path.ChangeExtension(path, ".png");
if (File.Exists(candidateImagePath))
{
imageFileName = Path.GetFileName(candidateImagePath);
imageSourcePath = candidateImagePath;
}
item = new PlanetaryMasteryItemSource(
ToUniqueIdentifier(name, usedIds),
name,
levels,
imageFileName,
imageSourcePath,
body);
reason = string.Empty;
return true;
}
static bool IsMasteryField(string key)
{
if (string.Equals(key, "category", StringComparison.OrdinalIgnoreCase) ||
string.Equals(key, "availableSystems", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return key.Length == 7 &&
key.StartsWith("level_", StringComparison.OrdinalIgnoreCase) &&
key[6] is >= '1' and <= '5';
}
static string GenerateStarSystems(IReadOnlyList<StarSystemSource> systems)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("namespace ZeroSpace.Model;");
builder.AppendLine();
builder.AppendLine("public static class StarSystems");
builder.AppendLine("{");
foreach (var system in systems)
{
builder.AppendLine($" public static readonly StarSystemData {system.Id} = new(");
builder.AppendLine($" Id: {Literal(system.Id)},");
builder.AppendLine($" Name: {Literal(system.Name)},");
builder.AppendLine($" SystemCategory: StarSystemCategory.{system.SystemCategory},");
builder.AppendLine($" Owner: {Literal(system.Owner)},");
builder.AppendLine($" HigherInfluenceYield: {BoolLiteral(system.HigherInfluenceYield)},");
builder.AppendLine($" HigherCreditYield: {BoolLiteral(system.HigherCreditYield)},");
builder.AppendLine($" HigherEnergyYield: {BoolLiteral(system.HigherEnergyYield)},");
builder.AppendLine($" HigherChanceForItems: {BoolLiteral(system.HigherChanceForItems)},");
builder.AppendLine($" OccupationBuff: {Literal(system.OccupationBuff)},");
builder.AppendLine($" SystemMastery: {Literal(system.SystemMastery)},");
builder.AppendLine($" ConnectedSystems: [{string.Join(", ", system.ConnectedSystems.Select(Literal))}],");
builder.AppendLine($" X: {system.X.ToString("R", CultureInfo.InvariantCulture)},");
builder.AppendLine($" Y: {system.Y.ToString("R", CultureInfo.InvariantCulture)},");
builder.AppendLine($" Body: {Literal(system.Body)});");
builder.AppendLine();
}
builder.AppendLine(" public static IReadOnlyList<StarSystemData> All { get; } =");
builder.AppendLine(" [");
foreach (var system in systems)
{
builder.AppendLine($" {system.Id},");
}
builder.AppendLine(" ];");
builder.AppendLine();
builder.AppendLine(" public static IReadOnlyDictionary<string, StarSystemData> ByName { get; } =");
builder.AppendLine(" All.ToDictionary(static system => system.Name, StringComparer.OrdinalIgnoreCase);");
builder.AppendLine("}");
return builder.ToString();
}
static string GenerateSystemMasteries(IReadOnlyList<SystemMasterySource> masteries)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("namespace ZeroSpace.Model;");
builder.AppendLine();
builder.AppendLine("public static class SystemMasteries");
builder.AppendLine("{");
foreach (var mastery in masteries)
{
builder.AppendLine($" public static readonly SystemMasteryData {mastery.Id} = new(");
builder.AppendLine($" Id: {Literal(mastery.Id)},");
builder.AppendLine($" Name: {Literal(mastery.Name)},");
builder.AppendLine($" Levels: [{string.Join(", ", mastery.Levels.Select(Literal))}],");
builder.AppendLine($" AvailableSystems: [{string.Join(", ", mastery.AvailableSystems.Select(Literal))}],");
builder.AppendLine(" Stats: new Dictionary<string, double>");
builder.AppendLine(" {");
foreach (var (key, value) in mastery.Stats)
{
builder.AppendLine($" [{Literal(key)}] = {value.ToString("R", CultureInfo.InvariantCulture)},");
}
builder.AppendLine(" },");
builder.AppendLine($" Body: {Literal(mastery.Body)});");
builder.AppendLine();
}
builder.AppendLine(" public static IReadOnlyList<SystemMasteryData> All { get; } =");
builder.AppendLine(" [");
foreach (var mastery in masteries)
{
builder.AppendLine($" {mastery.Id},");
}
builder.AppendLine(" ];");
builder.AppendLine();
builder.AppendLine(" public static IReadOnlyDictionary<string, SystemMasteryData> ByName { get; } =");
builder.AppendLine(" All.ToDictionary(static mastery => mastery.Name, StringComparer.OrdinalIgnoreCase);");
builder.AppendLine("}");
return builder.ToString();
}
static string GeneratePlanetaryMasteryItems(IReadOnlyList<PlanetaryMasteryItemSource> items)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("namespace ZeroSpace.Model;");
builder.AppendLine();
builder.AppendLine("public static class PlanetaryMasteryItems");
builder.AppendLine("{");
foreach (var item in items)
{
builder.AppendLine($" public static readonly PlanetaryMasteryItemData {item.Id} = new(");
builder.AppendLine($" Id: {Literal(item.Id)},");
builder.AppendLine($" Name: {Literal(item.Name)},");
builder.AppendLine($" Levels: [{string.Join(", ", item.Levels.Select(Literal))}],");
builder.AppendLine($" ImageFileName: {Literal(item.ImageFileName)},");
builder.AppendLine($" Body: {Literal(item.Body)});");
builder.AppendLine();
}
builder.AppendLine(" public static IReadOnlyList<PlanetaryMasteryItemData> All { get; } =");
builder.AppendLine(" [");
foreach (var item in items)
{
builder.AppendLine($" {item.Id},");
}
builder.AppendLine(" ];");
builder.AppendLine();
builder.AppendLine(" public static IReadOnlyDictionary<string, PlanetaryMasteryItemData> ByName { get; } =");
builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);");
builder.AppendLine("}");
return builder.ToString();
}
static string BoolLiteral(bool value)
{
return value ? "true" : "false";
}
static double GetDouble(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
{
if (frontMatter.TryGetValue(key, out var value) &&
double.TryParse(value.Scalar, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number))
{
return number;
}
return 0;
}
internal sealed class FrontMatterValue(string? scalar)
{
public string? Scalar { get; } = scalar;
public List<string> List { get; } = [];
}
internal sealed record UnitSource(
string Id,
string Name,
int Hexite,
int Flux,
int Supply,
int ProductionTime,
int Health,
int Energy,
int Shields,
int ArmorRating,
int MovementSpeed,
int DamagePerSecond,
int AttackRange,
IReadOnlyList<string> Attributes,
int Tier,
string Faction,
string? Hotkey,
int BuildAtSameTime,
int? Limit);
internal sealed record StarSystemSource(
string Id,
string Name,
string SystemCategory,
string Owner,
bool HigherInfluenceYield,
bool HigherCreditYield,
bool HigherEnergyYield,
bool HigherChanceForItems,
string? OccupationBuff,
string? SystemMastery,
IReadOnlyList<string> ConnectedSystems,
double X,
double Y,
string Body);
internal sealed record SystemMasterySource(
string Id,
string Name,
IReadOnlyList<string> Levels,
IReadOnlyList<string> AvailableSystems,
IReadOnlyList<(string Key, double Value)> Stats,
string Body);
internal sealed record PlanetaryMasteryItemSource(
string Id,
string Name,
IReadOnlyList<string> Levels,
string? ImageFileName,
string? ImageSourcePath,
string Body);