393 lines
12 KiB
C#
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);
|