From aac66e2b9fcf3f2a11d88e25ee3a8c168c26ea1b Mon Sep 17 00:00:00 2001 From: Jonathan McCaffrey Date: Sat, 26 Sep 2026 16:18:04 +0000 Subject: [PATCH] AI: markdown-to-C# converter for StarSystem and SystemMastery --- ZeroSpace/ZeroSpace.Build/Program.cs | 392 ++++++++++++++++++++++++++- 1 file changed, 389 insertions(+), 3 deletions(-) diff --git a/ZeroSpace/ZeroSpace.Build/Program.cs b/ZeroSpace/ZeroSpace.Build/Program.cs index b1568de..9c1ca60 100644 --- a/ZeroSpace/ZeroSpace.Build/Program.cs +++ b/ZeroSpace/ZeroSpace.Build/Program.cs @@ -69,6 +69,63 @@ if (skipped.Count > 0) } } +var starSystems = new List(); +var systemMasteries = new List(); +var skippedDocs = new List(); +var usedIds = new HashSet(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})"); + } + } +} + +starSystems.Sort((left, right) => string.Compare(left.Name, right.Name, StringComparison.OrdinalIgnoreCase)); +systemMasteries.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"); + +File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8); +File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8); + +Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}"); +Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}"); +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 }; @@ -368,9 +425,317 @@ static string GenerateUnits(IReadOnlyList units) static string Literal(string? value) { - return value is null - ? "null" - : "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\""; + 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? 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(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 frontMatter, string key) +{ + return frontMatter.TryGetValue(key, out var value) + && string.Equals(value.Scalar, "true", StringComparison.OrdinalIgnoreCase); +} + +static string ToUniqueIdentifier(string name, HashSet 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 frontMatter, + string body, + HashSet usedIds, + out StarSystemSource system, + out string reason) +{ + system = default!; + + var kind = string.Empty; + var kindCount = 0; + if (IsTrue(frontMatter, "isSideSystem")) + { + kind = "Side"; + kindCount++; + } + + if (IsTrue(frontMatter, "isHomeworldSystem")) + { + kind = "Homeworld"; + kindCount++; + } + + if (IsTrue(frontMatter, "isCoreSystem")) + { + kind = "Core"; + kindCount++; + } + + if (kindCount != 1) + { + reason = "expected exactly one system kind flag"; + return false; + } + + 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, + kind, + IsTrue(frontMatter, "higherInfluenceYield"), + IsTrue(frontMatter, "higherCreditYield"), + IsTrue(frontMatter, "higherEnergyYield"), + IsTrue(frontMatter, "higherChanceForItems"), + TryGetOptionalString(frontMatter, "occupationBuff"), + TryGetOptionalString(frontMatter, "systemMastery"), + connectedSystems, + body); + reason = string.Empty; + return true; +} + +static bool TryReadSystemMastery( + string path, + IReadOnlyDictionary frontMatter, + string body, + HashSet 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 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 systems) +{ + var builder = new StringBuilder(); + builder.AppendLine("// "); + 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($" Kind: StarSystemKind.{system.Kind},"); + 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($" Body: {Literal(system.Body)});"); + builder.AppendLine(); + } + + builder.AppendLine(" public static IReadOnlyList All { get; } ="); + builder.AppendLine(" ["); + + foreach (var system in systems) + { + builder.AppendLine($" {system.Id},"); + } + + builder.AppendLine(" ];"); + builder.AppendLine(); + builder.AppendLine(" public static IReadOnlyDictionary ByName { get; } ="); + builder.AppendLine(" All.ToDictionary(static system => system.Name, StringComparer.OrdinalIgnoreCase);"); + builder.AppendLine("}"); + + return builder.ToString(); +} + +static string GenerateSystemMasteries(IReadOnlyList masteries) +{ + var builder = new StringBuilder(); + builder.AppendLine("// "); + 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"); + 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 All { get; } ="); + builder.AppendLine(" ["); + + foreach (var mastery in masteries) + { + builder.AppendLine($" {mastery.Id},"); + } + + builder.AppendLine(" ];"); + builder.AppendLine(); + builder.AppendLine(" public static IReadOnlyDictionary ByName { get; } ="); + builder.AppendLine(" All.ToDictionary(static mastery => mastery.Name, StringComparer.OrdinalIgnoreCase);"); + builder.AppendLine("}"); + + return builder.ToString(); +} + +static string BoolLiteral(bool value) +{ + return value ? "true" : "false"; } internal sealed class FrontMatterValue(string? scalar) @@ -400,3 +765,24 @@ internal sealed record UnitSource( string? Hotkey, int BuildAtSameTime, int? Limit); + +internal sealed record StarSystemSource( + string Id, + string Name, + string Kind, + bool HigherInfluenceYield, + bool HigherCreditYield, + bool HigherEnergyYield, + bool HigherChanceForItems, + string? OccupationBuff, + string? SystemMastery, + IReadOnlyList ConnectedSystems, + string Body); + +internal sealed record SystemMasterySource( + string Id, + string Name, + IReadOnlyList Levels, + IReadOnlyList AvailableSystems, + IReadOnlyList<(string Key, double Value)> Stats, + string Body);