Files
ZeroSpace/ZS/Build/Program.cs
T

393 lines
12 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, "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").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}");
}
}
static string FindSolutionRoot()
{
var directory = new DirectoryInfo(Environment.CurrentDirectory);
while (directory is not null)
{
if (directory.EnumerateFiles("ZS.sln").Any())
{
return directory.FullName;
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not find ZS.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 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)
{
return value is null
? "null"
: "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
}
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);