Notes and Vibe start
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,392 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user