Io/zerospace converter #1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user