Io/zerospace converter #1
@@ -0,0 +1,261 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace ZeroSpace.MapEditor.Services;
|
||||
|
||||
/// <summary>
|
||||
/// One StarSystem markdown document: the editable map data plus the
|
||||
/// read-only details shown in the inspector. Has no MAUI dependencies
|
||||
/// so it can be tested outside the app.
|
||||
/// </summary>
|
||||
public sealed class StarSystemDoc
|
||||
{
|
||||
public string FilePath { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Kind { get; set; } = string.Empty;
|
||||
public bool HigherInfluenceYield { get; set; }
|
||||
public bool HigherCreditYield { get; set; }
|
||||
public bool HigherEnergyYield { get; set; }
|
||||
public bool HigherChanceForItems { get; set; }
|
||||
public string? OccupationBuff { get; set; }
|
||||
public string? SystemMastery { get; set; }
|
||||
public List<string> ConnectedSystems { get; } = new();
|
||||
public double X { get; set; }
|
||||
public double Y { get; set; }
|
||||
public bool HasPosition { get; set; }
|
||||
public bool IsDirty { get; set; }
|
||||
}
|
||||
|
||||
public static class StarSystemDocStore
|
||||
{
|
||||
/// <summary>World coordinate space used in the markdown files.</summary>
|
||||
public const double WorldSize = 1000;
|
||||
|
||||
public static List<StarSystemDoc> LoadFolder(string folder)
|
||||
{
|
||||
var docs = new List<StarSystemDoc>();
|
||||
foreach (var path in Directory.GetFiles(folder, "*.md").OrderBy(static p => p, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var doc = LoadFile(path);
|
||||
if (doc is not null)
|
||||
{
|
||||
docs.Add(doc);
|
||||
}
|
||||
}
|
||||
|
||||
// Any file missing x/y gets a deterministic spread-out placeholder.
|
||||
var missing = docs.Where(static d => !d.HasPosition).ToList();
|
||||
for (var i = 0; i < missing.Count; i++)
|
||||
{
|
||||
var (x, y) = DefaultPosition(i, missing.Count);
|
||||
missing[i].X = x;
|
||||
missing[i].Y = y;
|
||||
missing[i].IsDirty = true;
|
||||
}
|
||||
|
||||
return docs;
|
||||
}
|
||||
|
||||
public static StarSystemDoc? LoadFile(string path)
|
||||
{
|
||||
var lines = File.ReadAllLines(path);
|
||||
if (lines.Length < 2 || lines[0].Trim() != "---")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var values = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
var scalars = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
string? listKey = null;
|
||||
var closed = false;
|
||||
|
||||
for (var i = 1; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
if (line.Trim() == "---")
|
||||
{
|
||||
closed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (listKey is not null && line.StartsWith(" - ", StringComparison.Ordinal))
|
||||
{
|
||||
values[listKey].Add(CleanValue(line[4..]));
|
||||
continue;
|
||||
}
|
||||
|
||||
listKey = null;
|
||||
var separator = line.IndexOf(':');
|
||||
if (separator < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = line[..separator].Trim();
|
||||
var raw = line[(separator + 1)..].Trim();
|
||||
if (raw.Length == 0)
|
||||
{
|
||||
values[key] = new List<string>();
|
||||
listKey = key;
|
||||
}
|
||||
else
|
||||
{
|
||||
scalars[key] = CleanValue(raw);
|
||||
}
|
||||
}
|
||||
|
||||
if (!closed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!scalars.TryGetValue("category", out var category) ||
|
||||
!string.Equals(category, "StarSystem", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var doc = new StarSystemDoc
|
||||
{
|
||||
FilePath = path,
|
||||
Name = Path.GetFileNameWithoutExtension(path),
|
||||
OccupationBuff = scalars.TryGetValue("occupationBuff", out var buff) ? buff : null,
|
||||
SystemMastery = scalars.TryGetValue("systemMastery", out var mastery) ? mastery : null,
|
||||
};
|
||||
|
||||
if (IsTrue(scalars, "isSideSystem")) doc.Kind = "Side";
|
||||
if (IsTrue(scalars, "isHomeworldSystem")) doc.Kind = "Homeworld";
|
||||
if (IsTrue(scalars, "isCoreSystem")) doc.Kind = "Core";
|
||||
|
||||
doc.HigherInfluenceYield = IsTrue(scalars, "higherInfluenceYield");
|
||||
doc.HigherCreditYield = IsTrue(scalars, "higherCreditYield");
|
||||
doc.HigherEnergyYield = IsTrue(scalars, "higherEnergyYield");
|
||||
doc.HigherChanceForItems = IsTrue(scalars, "higherChanceForItems");
|
||||
|
||||
if (values.TryGetValue("connectedSystems", out var connected))
|
||||
{
|
||||
doc.ConnectedSystems.AddRange(connected.Where(static c => !string.IsNullOrWhiteSpace(c)));
|
||||
}
|
||||
|
||||
if (scalars.TryGetValue("x", out var xRaw) && TryParseDouble(xRaw, out var x) &&
|
||||
scalars.TryGetValue("y", out var yRaw) && TryParseDouble(yRaw, out var y))
|
||||
{
|
||||
doc.X = x;
|
||||
doc.Y = y;
|
||||
doc.HasPosition = true;
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes x/y back into each dirty file's frontmatter, preserving
|
||||
/// every other line byte-for-byte.
|
||||
/// </summary>
|
||||
public static void SavePositions(IEnumerable<StarSystemDoc> docs)
|
||||
{
|
||||
foreach (var doc in docs.Where(static d => d.IsDirty))
|
||||
{
|
||||
SavePosition(doc);
|
||||
doc.IsDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void SavePosition(StarSystemDoc doc)
|
||||
{
|
||||
var text = File.ReadAllText(doc.FilePath);
|
||||
var newline = text.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n";
|
||||
var lines = text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None).ToList();
|
||||
if (lines.Count < 2 || lines[0].Trim() != "---")
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var closeIndex = lines.FindIndex(1, static line => line.Trim() == "---");
|
||||
if (closeIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var xLine = $"x: {doc.X.ToString("F1", CultureInfo.InvariantCulture)}";
|
||||
var yLine = $"y: {doc.Y.ToString("F1", CultureInfo.InvariantCulture)}";
|
||||
var foundX = false;
|
||||
var foundY = false;
|
||||
|
||||
for (var i = 1; i < closeIndex; i++)
|
||||
{
|
||||
var key = KeyOf(lines[i]);
|
||||
if (string.Equals(key, "x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
lines[i] = xLine;
|
||||
foundX = true;
|
||||
}
|
||||
else if (string.Equals(key, "y", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
lines[i] = yLine;
|
||||
foundY = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundX)
|
||||
{
|
||||
lines.Insert(closeIndex, xLine);
|
||||
closeIndex++;
|
||||
}
|
||||
|
||||
if (!foundY)
|
||||
{
|
||||
lines.Insert(closeIndex, yLine);
|
||||
}
|
||||
|
||||
File.WriteAllText(doc.FilePath, string.Join(newline, lines));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic phyllotaxis (sunflower spiral) spread across the
|
||||
/// world space, so placeholder nodes never cover each other.
|
||||
/// </summary>
|
||||
public static (double X, double Y) DefaultPosition(int index, int count)
|
||||
{
|
||||
var goldenAngle = Math.PI * (3 - Math.Sqrt(5));
|
||||
var radius = (WorldSize / 2 - 30) * Math.Sqrt(index / (double)Math.Max(count, 1));
|
||||
var theta = index * goldenAngle;
|
||||
return (WorldSize / 2 + radius * Math.Cos(theta),
|
||||
WorldSize / 2 + radius * Math.Sin(theta));
|
||||
}
|
||||
|
||||
private static string? KeyOf(string line)
|
||||
{
|
||||
var separator = line.IndexOf(':');
|
||||
return separator < 0 ? null : line[..separator].Trim();
|
||||
}
|
||||
|
||||
private static bool IsTrue(Dictionary<string, string> scalars, string key)
|
||||
{
|
||||
return scalars.TryGetValue(key, out var value) &&
|
||||
string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool TryParseDouble(string raw, out double value)
|
||||
{
|
||||
return double.TryParse(raw.Trim('"', '\'', ' '),
|
||||
NumberStyles.Float | NumberStyles.AllowThousands,
|
||||
CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
internal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user