438 lines
14 KiB
C#
438 lines
14 KiB
C#
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using Chrono.Model;
|
|
|
|
var repoRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
|
|
var docsDir = Path.Combine(repoRoot, "chrono.docs");
|
|
var webWwwRoot = Path.Combine(repoRoot, "Chrono", "Web", "wwwroot");
|
|
var generatedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Cards.g.cs");
|
|
|
|
Console.WriteLine($"Repo root: {repoRoot}");
|
|
Console.WriteLine($"Docs dir: {docsDir}");
|
|
Console.WriteLine($"Generated: {generatedFile}");
|
|
|
|
if (!Directory.Exists(docsDir))
|
|
{
|
|
Console.Error.WriteLine($"ERROR: docs dir not found: {docsDir}");
|
|
return 1;
|
|
}
|
|
|
|
var mdFiles = Directory.GetFiles(docsDir, "*.md", SearchOption.AllDirectories);
|
|
var cards = new List<CardData>();
|
|
|
|
foreach (var file in mdFiles)
|
|
{
|
|
var content = Encoding.UTF8.GetString(File.ReadAllBytes(file));
|
|
if (!content.StartsWith("---"))
|
|
continue;
|
|
|
|
var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal);
|
|
if (endIndex < 0)
|
|
continue;
|
|
|
|
var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n");
|
|
var yaml = ParseYaml(frontmatter);
|
|
|
|
var name = Path.GetFileNameWithoutExtension(file);
|
|
var category = yaml.GetValueOrDefault("category");
|
|
if (category == null || category == "Deck") continue;
|
|
|
|
var imageFile = StripWikiLink(yaml.GetValueOrDefault("imageLink"));
|
|
if (imageFile != null && !imageFile.EndsWith(".png"))
|
|
imageFile += ".png";
|
|
|
|
var card = new CardData
|
|
{
|
|
Name = name,
|
|
Category = category,
|
|
Cost = ParseInt(yaml, "cost"),
|
|
Attack = ParseInt(yaml, "attack"),
|
|
Health = ParseInt(yaml, "health"),
|
|
Description = StripWikiLinks(yaml.GetValueOrDefault("description")),
|
|
Faction = StripWikiLink(yaml.GetValueOrDefault("faction")),
|
|
Set = StripWikiLink(yaml.GetValueOrDefault("set")),
|
|
Speed = StripWikiLink(yaml.GetValueOrDefault("speed")),
|
|
Archetypes = ParseList(yaml, "archetypes").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
|
ImmortalizeTo = yaml.ContainsKey("immortalizeTo")
|
|
? ParseListOrScalar(yaml, "immortalizeTo").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList()
|
|
: null,
|
|
ImmortalizeFrom = StripWikiLink(yaml.GetValueOrDefault("immortalizeFrom")),
|
|
ImmortalizeWhen = NullIfNa(StripWikiLinks(yaml.GetValueOrDefault("immortalizeWhen"))),
|
|
ImageFile = imageFile
|
|
};
|
|
|
|
cards.Add(card);
|
|
}
|
|
|
|
// Copy PNGs to wwwroot/cards
|
|
var cardsDir = Path.Combine(webWwwRoot, "cards");
|
|
Directory.CreateDirectory(cardsDir);
|
|
|
|
var pngFiles = Directory.GetFiles(docsDir, "*.png", SearchOption.AllDirectories);
|
|
var pngMap = pngFiles
|
|
.GroupBy(Path.GetFileName)
|
|
.ToDictionary(g => g.Key!, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
|
|
|
foreach (var card in cards)
|
|
{
|
|
if (card.ImageFile == null) continue;
|
|
if (pngMap.TryGetValue(card.ImageFile, out var src))
|
|
{
|
|
var dst = Path.Combine(cardsDir, card.ImageFile);
|
|
File.Copy(src, dst, true);
|
|
}
|
|
}
|
|
|
|
// Generate C# source file
|
|
Directory.CreateDirectory(Path.GetDirectoryName(generatedFile)!);
|
|
using var writer = new StreamWriter(generatedFile, false, Encoding.UTF8);
|
|
writer.WriteLine("// <auto-generated/>");
|
|
writer.WriteLine("#nullable enable");
|
|
writer.WriteLine();
|
|
writer.WriteLine("namespace Chrono.Model;");
|
|
writer.WriteLine();
|
|
writer.WriteLine("public static class CardDatabase");
|
|
writer.WriteLine("{");
|
|
writer.WriteLine(" public static readonly System.Collections.Generic.List<CardData> Cards =");
|
|
writer.WriteLine(" [");
|
|
|
|
for (var i = 0; i < cards.Count; i++)
|
|
{
|
|
var c = cards[i];
|
|
writer.WriteLine(" new()");
|
|
writer.WriteLine(" {");
|
|
WriteProp(writer, "Name", c.Name, 3);
|
|
WriteProp(writer, "Category", c.Category, 3);
|
|
WriteNullProp(writer, "Cost", c.Cost, 3);
|
|
WriteNullProp(writer, "Attack", c.Attack, 3);
|
|
WriteNullProp(writer, "Health", c.Health, 3);
|
|
WriteStrProp(writer, "Description", c.Description, 3);
|
|
WriteStrProp(writer, "Faction", c.Faction, 3);
|
|
WriteStrProp(writer, "Set", c.Set, 3);
|
|
WriteStrProp(writer, "Speed", c.Speed, 3);
|
|
WriteListProp(writer, "Archetypes", c.Archetypes, 3);
|
|
WriteListProp(writer, "ImmortalizeTo", c.ImmortalizeTo, 3);
|
|
WriteStrProp(writer, "ImmortalizeFrom", c.ImmortalizeFrom, 3);
|
|
WriteStrProp(writer, "ImmortalizeWhen", c.ImmortalizeWhen, 3);
|
|
WriteStrProp(writer, "ImageFile", c.ImageFile, 3);
|
|
|
|
var comma = i < cards.Count - 1 ? "," : "";
|
|
writer.WriteLine($" }}{comma}");
|
|
}
|
|
|
|
writer.WriteLine(" ];");
|
|
writer.WriteLine("}");
|
|
|
|
Console.WriteLine($"Generated {cards.Count} cards in {generatedFile}");
|
|
|
|
// ── Decks ──
|
|
var decksDir = Path.Combine(docsDir, "Decks");
|
|
var deckFiles = Directory.Exists(decksDir) ? Directory.GetFiles(decksDir, "*.md", SearchOption.AllDirectories) : [];
|
|
var decks = new List<DeckData>();
|
|
|
|
foreach (var file in deckFiles)
|
|
{
|
|
var content = Encoding.UTF8.GetString(File.ReadAllBytes(file));
|
|
if (!content.StartsWith("---")) continue;
|
|
var endIndex = content.IndexOf("---", 3, StringComparison.Ordinal);
|
|
if (endIndex < 0) continue;
|
|
|
|
var frontmatter = content[3..endIndex].Trim().Replace("\r\n", "\n").Replace("\r", "\n");
|
|
var yaml = ParseYaml(frontmatter);
|
|
|
|
var name = Path.GetFileNameWithoutExtension(file);
|
|
var isVisible = yaml.GetValueOrDefault("isVisible") == "true";
|
|
|
|
var deck = new DeckData
|
|
{
|
|
Name = name,
|
|
Cards = ParseList(yaml, "cards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
|
Keycards = ParseList(yaml, "keycards").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
|
Divers = ParseList(yaml, "divers").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
|
Description = StripWikiLinks(NullIfNa(yaml.GetValueOrDefault("description")))?.Replace("\r\n", "\n")
|
|
.Replace("\r", "\n").Replace("\n\n", "\n").Replace("\n\n", "\n"),
|
|
Factions = ParseList(yaml, "factions").Select(s => StripWikiLink(s) ?? "").Where(s => s != "").ToList(),
|
|
IsVisible = isVisible
|
|
};
|
|
|
|
decks.Add(deck);
|
|
}
|
|
|
|
Console.WriteLine($"Parsed {decks.Count} deck files");
|
|
|
|
// Generate Decks.g.cs
|
|
var deckGeneratedFile = Path.Combine(repoRoot, "Chrono", "Web", "Generated", "Decks.g.cs");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(deckGeneratedFile)!);
|
|
using var deckWriter = new StreamWriter(deckGeneratedFile, false, Encoding.UTF8);
|
|
deckWriter.WriteLine("// <auto-generated/>");
|
|
deckWriter.WriteLine("#nullable enable");
|
|
deckWriter.WriteLine();
|
|
deckWriter.WriteLine("namespace Chrono.Model;");
|
|
deckWriter.WriteLine();
|
|
deckWriter.WriteLine("public static class DeckDatabase");
|
|
deckWriter.WriteLine("{");
|
|
deckWriter.WriteLine(" public static readonly System.Collections.Generic.List<DeckData> Decks =");
|
|
deckWriter.WriteLine(" [");
|
|
|
|
for (var i = 0; i < decks.Count; i++)
|
|
{
|
|
var d = decks[i];
|
|
deckWriter.WriteLine(" new()");
|
|
deckWriter.WriteLine(" {");
|
|
WriteProp(deckWriter, "Name", d.Name, 3);
|
|
WriteListProp(deckWriter, "Cards", d.Cards, 3);
|
|
WriteListProp(deckWriter, "Keycards", d.Keycards, 3);
|
|
WriteListProp(deckWriter, "Divers", d.Divers, 3);
|
|
WriteStrProp(deckWriter, "Description", d.Description, 3);
|
|
WriteListProp(deckWriter, "Factions", d.Factions, 3);
|
|
deckWriter.WriteLine($" IsVisible = {(d.IsVisible ? "true" : "false")},");
|
|
var comma = i < decks.Count - 1 ? "," : "";
|
|
deckWriter.WriteLine($" }}{comma}");
|
|
}
|
|
|
|
deckWriter.WriteLine(" ];");
|
|
deckWriter.WriteLine("}");
|
|
|
|
Console.WriteLine($"Generated {decks.Count} decks in {deckGeneratedFile}");
|
|
return 0;
|
|
|
|
// --- Helpers ---
|
|
|
|
static int? ParseInt(Dictionary<string, string> yaml, string key)
|
|
{
|
|
if (!yaml.TryGetValue(key, out var val)) return null;
|
|
if (int.TryParse(val, out var i)) return i;
|
|
return null;
|
|
}
|
|
|
|
static string? StripWikiLink(string? s)
|
|
{
|
|
if (s == null || s == "N/A") return null;
|
|
return Regex.Replace(s, @"\[\[([^\]]*)\]\]", "$1").Trim('"');
|
|
}
|
|
|
|
static string? StripWikiLinks(string? s)
|
|
{
|
|
if (s == null || s == "N/A") return null;
|
|
return Regex.Replace(s.Trim('"'), @"\[\[([^\]]*)\]\]", "$1").Trim();
|
|
}
|
|
|
|
static string? NullIfNa(string? s)
|
|
{
|
|
return s is "N/A" or null ? null : s.Trim('"');
|
|
}
|
|
|
|
static List<string> ParseList(Dictionary<string, string> yaml, string key)
|
|
{
|
|
if (!yaml.TryGetValue(key, out var raw)) return [];
|
|
var result = new List<string>();
|
|
foreach (var item in raw.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var trimmed = item.TrimStart('-', ' ').Trim(' ', '"');
|
|
if (trimmed.Length > 0) result.Add(trimmed);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static List<string> ParseListOrScalar(Dictionary<string, string> yaml, string key)
|
|
{
|
|
if (!yaml.TryGetValue(key, out var raw)) return [];
|
|
raw = raw.Trim();
|
|
if (!raw.StartsWith('-'))
|
|
return [raw.Trim('"')];
|
|
|
|
var result = new List<string>();
|
|
foreach (var item in raw.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
|
{
|
|
var trimmed = item.TrimStart('-', ' ').Trim(' ', '"');
|
|
if (trimmed.Length > 0) result.Add(trimmed);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static Dictionary<string, string> ParseYaml(string yaml)
|
|
{
|
|
var dict = new Dictionary<string, string>();
|
|
var lines = yaml.Split('\n');
|
|
string? currentKey = null;
|
|
var listBuffer = new List<string>();
|
|
var inBlockScalar = false;
|
|
string? blockScalarKey = null;
|
|
var blockScalarLines = new List<string>();
|
|
string? quoteKey = null;
|
|
var quoteLines = new List<string>();
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
var trimmed = line.Trim();
|
|
|
|
if (inBlockScalar)
|
|
{
|
|
if (line.Length == 0 || line[0] == ' ' || line[0] == '\t')
|
|
{
|
|
blockScalarLines.Add(trimmed);
|
|
continue;
|
|
}
|
|
|
|
if (blockScalarKey != null)
|
|
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
|
inBlockScalar = false;
|
|
blockScalarKey = null;
|
|
blockScalarLines.Clear();
|
|
}
|
|
|
|
if (quoteKey != null)
|
|
{
|
|
if (line.Length == 0 || line[0] == ' ' || line[0] == '\t')
|
|
{
|
|
quoteLines.Add(trimmed);
|
|
if (trimmed.EndsWith("\""))
|
|
{
|
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml)).TrimEnd('"');
|
|
quoteKey = null;
|
|
quoteLines.Clear();
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
|
quoteKey = null;
|
|
quoteLines.Clear();
|
|
}
|
|
|
|
if (trimmed.Length == 0) continue;
|
|
|
|
if (trimmed.StartsWith("- "))
|
|
{
|
|
listBuffer.Add(trimmed);
|
|
continue;
|
|
}
|
|
|
|
if (listBuffer.Count > 0 && currentKey != null)
|
|
{
|
|
dict[currentKey] = string.Join("\n", listBuffer);
|
|
listBuffer.Clear();
|
|
}
|
|
|
|
var colonIndex = trimmed.IndexOf(':');
|
|
if (colonIndex < 0) continue;
|
|
|
|
currentKey = trimmed[..colonIndex].Trim();
|
|
var value = trimmed[(colonIndex + 1)..].Trim();
|
|
|
|
if (value is "|" or "|-")
|
|
{
|
|
inBlockScalar = true;
|
|
blockScalarKey = currentKey;
|
|
blockScalarLines.Clear();
|
|
continue;
|
|
}
|
|
|
|
if (value.StartsWith("\"") && !value.EndsWith("\""))
|
|
{
|
|
quoteKey = currentKey;
|
|
quoteLines.Clear();
|
|
quoteLines.Add(value.TrimStart('"'));
|
|
continue;
|
|
}
|
|
|
|
dict[currentKey] = value;
|
|
}
|
|
|
|
if (listBuffer.Count > 0 && currentKey != null)
|
|
dict[currentKey] = string.Join("\n", listBuffer);
|
|
|
|
if (inBlockScalar && blockScalarKey != null)
|
|
dict[blockScalarKey] = string.Join("\n", blockScalarLines);
|
|
|
|
if (quoteKey != null)
|
|
dict[quoteKey] = string.Join("\n", quoteLines.Select(UnescapeYaml));
|
|
|
|
return dict;
|
|
}
|
|
|
|
static string UnescapeYaml(string s)
|
|
{
|
|
var sb = new StringBuilder();
|
|
for (var i = 0; i < s.Length; i++)
|
|
if (s[i] == '\\' && i + 1 < s.Length)
|
|
switch (s[i + 1])
|
|
{
|
|
case 'r':
|
|
sb.Append('\r');
|
|
i++;
|
|
break;
|
|
case 'n':
|
|
sb.Append('\n');
|
|
i++;
|
|
break;
|
|
case 't':
|
|
sb.Append('\t');
|
|
i++;
|
|
break;
|
|
case '\\':
|
|
sb.Append('\\');
|
|
i++;
|
|
break;
|
|
case '"':
|
|
sb.Append('"');
|
|
i++;
|
|
break;
|
|
default: sb.Append(s[i]); break;
|
|
}
|
|
else
|
|
sb.Append(s[i]);
|
|
|
|
return sb.ToString();
|
|
}
|
|
|
|
static void WriteProp(StreamWriter w, string name, string value, int indent)
|
|
{
|
|
w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
|
|
}
|
|
|
|
static void WriteStrProp(StreamWriter w, string name, string? value, int indent)
|
|
{
|
|
if (value == null) return;
|
|
w.WriteLine($"{new string(' ', indent * 4)}{name} = {ToLiteral(value)},");
|
|
}
|
|
|
|
static void WriteNullProp(StreamWriter w, string name, int? value, int indent)
|
|
{
|
|
if (value == null) return;
|
|
w.WriteLine($"{new string(' ', indent * 4)}{name} = {value},");
|
|
}
|
|
|
|
static void WriteListProp(StreamWriter w, string name, List<string>? values, int indent)
|
|
{
|
|
if (values == null) return;
|
|
var pad = new string(' ', indent * 4);
|
|
w.WriteLine($"{pad}{name} = [");
|
|
foreach (var v in values)
|
|
w.WriteLine($"{pad} {ToLiteral(v)},");
|
|
w.WriteLine($"{pad}],");
|
|
}
|
|
|
|
static string ToLiteral(string? s)
|
|
{
|
|
if (s == null) return "null";
|
|
var sb = new StringBuilder();
|
|
sb.Append('"');
|
|
foreach (var c in s)
|
|
switch (c)
|
|
{
|
|
case '"': sb.Append("\\\""); break;
|
|
case '\\': sb.Append("\\\\"); break;
|
|
case '\n': sb.Append("\\n"); break;
|
|
case '\r': sb.Append("\\r"); break;
|
|
case '\t': sb.Append("\\t"); break;
|
|
case '\0': sb.Append("\\0"); break;
|
|
default: sb.Append(c); break;
|
|
}
|
|
|
|
sb.Append('"');
|
|
return sb.ToString();
|
|
} |