974 lines
31 KiB
C#
974 lines
31 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, "ZeroSpace.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", SearchOption.AllDirectories).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}");
|
|
}
|
|
}
|
|
|
|
var starSystems = new List<StarSystemSource>();
|
|
var systemMasteries = new List<SystemMasterySource>();
|
|
var planetaryMasteryItems = new List<PlanetaryMasteryItemSource>();
|
|
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})");
|
|
}
|
|
}
|
|
else if (string.Equals(categoryValue.Scalar, "PlanetaryMasteryItem", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (TryReadPlanetaryMasteryItem(path, frontMatter, body, usedIds, out var item, out var reason))
|
|
{
|
|
planetaryMasteryItems.Add(item);
|
|
}
|
|
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));
|
|
planetaryMasteryItems.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");
|
|
var planetaryMasteryItemsPath = Path.Combine(modelRoot, "PlanetaryMasteryItems.g.cs");
|
|
|
|
File.WriteAllText(starSystemsPath, GenerateStarSystems(starSystems), Encoding.UTF8);
|
|
File.WriteAllText(systemMasteriesPath, GenerateSystemMasteries(systemMasteries), Encoding.UTF8);
|
|
File.WriteAllText(planetaryMasteryItemsPath, GeneratePlanetaryMasteryItems(planetaryMasteryItems), Encoding.UTF8);
|
|
|
|
Console.WriteLine($"Generated {starSystems.Count} star systems: {Path.GetRelativePath(solutionRoot, starSystemsPath)}");
|
|
Console.WriteLine($"Generated {systemMasteries.Count} system masteries: {Path.GetRelativePath(solutionRoot, systemMasteriesPath)}");
|
|
Console.WriteLine($"Generated {planetaryMasteryItems.Count} planetary mastery items: {Path.GetRelativePath(solutionRoot, planetaryMasteryItemsPath)}");
|
|
|
|
// Copy item icons next to the markdown docs into the web app's wwwroot so the
|
|
// items page can reference them. Images are copied, never moved: the docs folder
|
|
// stays the source of truth.
|
|
var itemImagesDir = Path.Combine(solutionRoot, "ZeroSpace.Web", "wwwroot", "images", "planetary-mastery-items");
|
|
Directory.CreateDirectory(itemImagesDir);
|
|
|
|
var expectedImages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
var copiedImages = 0;
|
|
foreach (var item in planetaryMasteryItems)
|
|
{
|
|
if (item.ImageFileName is null || item.ImageSourcePath is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
expectedImages.Add(item.ImageFileName);
|
|
var destination = Path.Combine(itemImagesDir, item.ImageFileName);
|
|
File.Copy(item.ImageSourcePath, destination, overwrite: true);
|
|
copiedImages++;
|
|
}
|
|
|
|
foreach (var stale in Directory.EnumerateFiles(itemImagesDir, "*.png"))
|
|
{
|
|
if (!expectedImages.Contains(Path.GetFileName(stale)))
|
|
{
|
|
File.Delete(stale);
|
|
Console.WriteLine($"Removed stale item image: {Path.GetRelativePath(solutionRoot, stale)}");
|
|
}
|
|
}
|
|
|
|
Console.WriteLine($"Copied {copiedImages} planetary mastery item images to {Path.GetRelativePath(solutionRoot, itemImagesDir)}");
|
|
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 };
|
|
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var directory = new DirectoryInfo(candidate);
|
|
|
|
while (directory is not null)
|
|
{
|
|
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace.sln")))
|
|
{
|
|
return directory.FullName;
|
|
}
|
|
|
|
if (File.Exists(Path.Combine(directory.FullName, "ZeroSpace", "ZeroSpace.sln")))
|
|
{
|
|
return Path.Combine(directory.FullName, "ZeroSpace");
|
|
}
|
|
|
|
directory = directory.Parent;
|
|
}
|
|
}
|
|
|
|
throw new DirectoryNotFoundException("Could not find ZeroSpace.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 ZeroSpace.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)
|
|
{
|
|
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!;
|
|
|
|
string category;
|
|
if (frontMatter.TryGetValue("systemCategory", out var categoryVal) && !string.IsNullOrWhiteSpace(categoryVal.Scalar))
|
|
{
|
|
var catStr = categoryVal.Scalar.Trim();
|
|
if (catStr.Equals("Homeworld System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Homeworld", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
category = "HomeworldSystem";
|
|
}
|
|
else if (catStr.Equals("Core System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Core", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
category = "CoreSystem";
|
|
}
|
|
else if (catStr.Equals("Side System", StringComparison.OrdinalIgnoreCase) || catStr.Equals("Side", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
category = "SideSystem";
|
|
}
|
|
else
|
|
{
|
|
reason = $"unknown systemCategory '{catStr}'";
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var kindCount = 0;
|
|
category = string.Empty;
|
|
if (IsTrue(frontMatter, "isSideSystem"))
|
|
{
|
|
category = "SideSystem";
|
|
kindCount++;
|
|
}
|
|
|
|
if (IsTrue(frontMatter, "isHomeworldSystem"))
|
|
{
|
|
category = "HomeworldSystem";
|
|
kindCount++;
|
|
}
|
|
|
|
if (IsTrue(frontMatter, "isCoreSystem"))
|
|
{
|
|
category = "CoreSystem";
|
|
kindCount++;
|
|
}
|
|
|
|
if (kindCount != 1)
|
|
{
|
|
reason = "missing or invalid systemCategory";
|
|
return false;
|
|
}
|
|
}
|
|
|
|
var owner = TryGetOptionalString(frontMatter, "owner") ?? "The Eden Accord";
|
|
|
|
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,
|
|
category,
|
|
owner,
|
|
IsTrue(frontMatter, "higherInfluenceYield"),
|
|
IsTrue(frontMatter, "higherCreditYield"),
|
|
IsTrue(frontMatter, "higherEnergyYield"),
|
|
IsTrue(frontMatter, "higherChanceForItems"),
|
|
TryGetOptionalString(frontMatter, "occupationBuff"),
|
|
TryGetOptionalString(frontMatter, "systemMastery"),
|
|
connectedSystems,
|
|
GetDouble(frontMatter, "x"),
|
|
GetDouble(frontMatter, "y"),
|
|
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 TryReadPlanetaryMasteryItem(
|
|
string path,
|
|
IReadOnlyDictionary<string, FrontMatterValue> frontMatter,
|
|
string body,
|
|
HashSet<string> usedIds,
|
|
out PlanetaryMasteryItemSource item,
|
|
out string reason)
|
|
{
|
|
item = 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 name = Path.GetFileNameWithoutExtension(path);
|
|
|
|
// An icon with the same file name as the doc (e.g. "Battery.png" next to
|
|
// "Battery.md") is treated as this item's image when present.
|
|
string? imageFileName = null;
|
|
string? imageSourcePath = null;
|
|
var candidateImagePath = Path.ChangeExtension(path, ".png");
|
|
if (File.Exists(candidateImagePath))
|
|
{
|
|
imageFileName = Path.GetFileName(candidateImagePath);
|
|
imageSourcePath = candidateImagePath;
|
|
}
|
|
|
|
item = new PlanetaryMasteryItemSource(
|
|
ToUniqueIdentifier(name, usedIds),
|
|
name,
|
|
levels,
|
|
imageFileName,
|
|
imageSourcePath,
|
|
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($" SystemCategory: StarSystemCategory.{system.SystemCategory},");
|
|
builder.AppendLine($" Owner: {Literal(system.Owner)},");
|
|
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($" X: {system.X.ToString("R", CultureInfo.InvariantCulture)},");
|
|
builder.AppendLine($" Y: {system.Y.ToString("R", CultureInfo.InvariantCulture)},");
|
|
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 GeneratePlanetaryMasteryItems(IReadOnlyList<PlanetaryMasteryItemSource> items)
|
|
{
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine("// <auto-generated />");
|
|
builder.AppendLine("namespace ZeroSpace.Model;");
|
|
builder.AppendLine();
|
|
builder.AppendLine("public static class PlanetaryMasteryItems");
|
|
builder.AppendLine("{");
|
|
|
|
foreach (var item in items)
|
|
{
|
|
builder.AppendLine($" public static readonly PlanetaryMasteryItemData {item.Id} = new(");
|
|
builder.AppendLine($" Id: {Literal(item.Id)},");
|
|
builder.AppendLine($" Name: {Literal(item.Name)},");
|
|
builder.AppendLine($" Levels: [{string.Join(", ", item.Levels.Select(Literal))}],");
|
|
builder.AppendLine($" ImageFileName: {Literal(item.ImageFileName)},");
|
|
builder.AppendLine($" Body: {Literal(item.Body)});");
|
|
builder.AppendLine();
|
|
}
|
|
|
|
builder.AppendLine(" public static IReadOnlyList<PlanetaryMasteryItemData> All { get; } =");
|
|
builder.AppendLine(" [");
|
|
|
|
foreach (var item in items)
|
|
{
|
|
builder.AppendLine($" {item.Id},");
|
|
}
|
|
|
|
builder.AppendLine(" ];");
|
|
builder.AppendLine();
|
|
builder.AppendLine(" public static IReadOnlyDictionary<string, PlanetaryMasteryItemData> ByName { get; } =");
|
|
builder.AppendLine(" All.ToDictionary(static item => item.Name, StringComparer.OrdinalIgnoreCase);");
|
|
builder.AppendLine("}");
|
|
|
|
return builder.ToString();
|
|
}
|
|
|
|
static string BoolLiteral(bool value)
|
|
{
|
|
return value ? "true" : "false";
|
|
}
|
|
|
|
static double GetDouble(IReadOnlyDictionary<string, FrontMatterValue> frontMatter, string key)
|
|
{
|
|
if (frontMatter.TryGetValue(key, out var value) &&
|
|
double.TryParse(value.Scalar, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var number))
|
|
{
|
|
return number;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
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);
|
|
|
|
internal sealed record StarSystemSource(
|
|
string Id,
|
|
string Name,
|
|
string SystemCategory,
|
|
string Owner,
|
|
bool HigherInfluenceYield,
|
|
bool HigherCreditYield,
|
|
bool HigherEnergyYield,
|
|
bool HigherChanceForItems,
|
|
string? OccupationBuff,
|
|
string? SystemMastery,
|
|
IReadOnlyList<string> ConnectedSystems,
|
|
double X,
|
|
double Y,
|
|
string Body);
|
|
|
|
internal sealed record SystemMasterySource(
|
|
string Id,
|
|
string Name,
|
|
IReadOnlyList<string> Levels,
|
|
IReadOnlyList<string> AvailableSystems,
|
|
IReadOnlyList<(string Key, double Value)> Stats,
|
|
string Body);
|
|
|
|
internal sealed record PlanetaryMasteryItemSource(
|
|
string Id,
|
|
string Name,
|
|
IReadOnlyList<string> Levels,
|
|
string? ImageFileName,
|
|
string? ImageSourcePath,
|
|
string Body);
|