AI: markdown-to-C# converter for StarSystem and SystemMastery

This commit is contained in:
2026-09-29 18:21:21 +00:00
parent 04f2e6474c
commit 84bf436577
+389 -3
View File
@@ -69,6 +69,63 @@ if (skipped.Count > 0)
}
}
var starSystems = new List<StarSystemSource>();
var systemMasteries = new List<SystemMasterySource>();
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})");
}
}
}
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<UnitSource> 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<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!;
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<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 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($" 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<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 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<string> ConnectedSystems,
string Body);
internal sealed record SystemMasterySource(
string Id,
string Name,
IReadOnlyList<string> Levels,
IReadOnlyList<string> AvailableSystems,
IReadOnlyList<(string Key, double Value)> Stats,
string Body);