using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalaxyConstellationEdge(string FromName, string ToName);
public sealed record GalaxyConstellationDefinition(string Name, IReadOnlyList<GalaxyConstellationEdge> Edges);
public sealed record GalaxyConstellationLine(string FromName, string ToName, Coordinates From, Coordinates To);
public sealed record GalaxyResolvedConstellation(string Name, IReadOnlyList<GalaxyConstellationLine> Lines);
public sealed record GalaxyConstellationResolution(IReadOnlyList<GalaxyResolvedConstellation> Groups,
IReadOnlyList<string> MissingEndpointNames, int PendingGroupCount)
{
public bool IsComplete => PendingGroupCount == 0;
}
/// <summary>
/// The authored game graph, resolved exclusively against actual catalogue positions.
/// Original 3A28A40 stores compact XYZ / 32 without a shell or other spatial transform.
/// Coordinates here use the same light years with the catalogue's Sol origin subtracted.
/// </summary>
public static class GalaxyOriginalConstellations
{
public const string SourceResource = "d30f11c57448cff8ec3dd0c5c80d9868482ccadb";
public const string SourceSha256 = "ba4e049fd4eae066e72f29898a25d52692422e5905fc7a4a65105e44eb631ade";
public const double FullOpacityDistance = 8;
public const double InvisibleDistance = 24;
public const float SubmissionAlpha = .1f;
// Original rendercfg+190, style6 -> renderer+540. The group consumer
// replaces only its alpha before the shared StarMapLine64B writer.
public const int MaximumSegments = 206;
public const float WorldHalfWidth = .1f;
public const float TextureRepeatPerLightYear = 2.5f;
public static IReadOnlyList<GalaxyConstellationDefinition> Definitions { get; } = Read();
public static IReadOnlyList<string> EndpointNames { get; } = Array.AsReadOnly(Definitions
.SelectMany(g => g.Edges.SelectMany(e => new[] { e.FromName, e.ToName }))
.Distinct(StringComparer.OrdinalIgnoreCase).ToArray());
/// <summary>
/// A group is returned only when every endpoint is available, matching the original
/// ready-handle gate. Missing facts remain pending; no coordinates are synthesized.
/// Each distinct endpoint is read once so one resolution uses a consistent snapshot.
/// </summary>
public static GalaxyConstellationResolution Resolve(Func<string, Coordinates?> resolvePosition)
{
ArgumentNullException.ThrowIfNull(resolvePosition);
var positions = new Dictionary<string, Coordinates>(StringComparer.OrdinalIgnoreCase);
var missing = new List<string>();
foreach (var name in EndpointNames)
{
if (resolvePosition(name) is { IsValid: true } position) positions.Add(name, position);
else missing.Add(name);
}
var ready = new List<GalaxyResolvedConstellation>();
foreach (var group in Definitions)
{
if (group.Edges.Any(e => !positions.ContainsKey(e.FromName) || !positions.ContainsKey(e.ToName))) continue;
var lines = group.Edges.Select(e => new GalaxyConstellationLine(e.FromName, e.ToName,
positions[e.FromName], positions[e.ToName])).ToArray();
ready.Add(new(group.Name, Array.AsReadOnly(lines)));
}
return new(ready.AsReadOnly(), missing.AsReadOnly(), Definitions.Count - ready.Count);
}
/// <summary>
/// Original 3A1CF60: squared forward-facing cosine, multiplied by the linear
/// eye-to-Sol fade (opaque through 8 ly, zero at 24 ly). The group direction
/// averages ONLY the first endpoint of each authored edge, preserving duplicates.
/// The caller submits lines only when the returned alpha exceeds SubmissionAlpha.
/// </summary>
public static double Alpha(GalaxyResolvedConstellation group, Coordinates eye, Coordinates focus)
{
ArgumentNullException.ThrowIfNull(group);
if (!eye.IsValid || !focus.IsValid || group.Lines.Count == 0 ||
group.Lines.Any(line => !line.From.IsValid || !line.To.IsValid)) return 0;
double x = 0, y = 0, z = 0;
foreach (var line in group.Lines) { x += line.From.X; y += line.From.Y; z += line.From.Z; }
x /= group.Lines.Count; y /= group.Lines.Count; z /= group.Lines.Count;
var length = Math.Sqrt(x * x + y * y + z * z);
if (length == 0) { x = 0; y = 0; z = 1; }
else { x /= length; y /= length; z /= length; }
var vx = focus.X - eye.X; var vy = focus.Y - eye.Y; var vz = focus.Z - eye.Z;
var viewLength = Math.Sqrt(vx * vx + vy * vy + vz * vz);
// The original zero-length view branch retains its initial directional alpha 1.
var facing = viewLength > 0 ? Math.Clamp((vx * x + vy * y + vz * z) / viewLength, 0, 1) : 1;
var distance = Math.Sqrt(eye.X * eye.X + eye.Y * eye.Y + eye.Z * eye.Z);
return facing * facing * Math.Clamp((distance - InvisibleDistance) /
(FullOpacityDistance - InvisibleDistance), 0, 1);
}
private static IReadOnlyList<GalaxyConstellationDefinition> Read()
{
using var stream = typeof(GalaxyOriginalConstellations).Assembly
.GetManifestResourceStream("XFEEDToolBox.Core.Galaxy.original-constellations.json")
?? throw new InvalidDataException("原星座连接资源缺失。");
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
if (root.GetProperty("sourceResource").GetString() != SourceResource ||
root.GetProperty("sourceSha256").GetString() != SourceSha256)
throw new InvalidDataException("原星座连接来源校验失败。");
var groups = root.GetProperty("groups").EnumerateArray().Select(g => new GalaxyConstellationDefinition(
g.GetProperty("name").GetString()!, Array.AsReadOnly(g.GetProperty("edges").EnumerateArray()
.Select(e => new GalaxyConstellationEdge(e.GetProperty("fromName").GetString()!,
e.GetProperty("toName").GetString()!)).ToArray()))).ToArray();
if (groups.Length != 16 || groups.Sum(g => g.Edges.Count) != 206 ||
groups.Any(g => string.IsNullOrWhiteSpace(g.Name) || g.Edges.Count == 0 ||
g.Edges.Any(e => string.IsNullOrWhiteSpace(e.FromName) || string.IsNullOrWhiteSpace(e.ToName))))
throw new InvalidDataException("原星座连接图无效。");
return Array.AsReadOnly(groups);
}
}
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Galaxy;
public sealed record GalaxyConstellationEdge(string FromName, string ToName);
public sealed record GalaxyConstellationDefinition(string Name, IReadOnlyList<GalaxyConstellationEdge> Edges);
public sealed record GalaxyConstellationLine(string FromName, string ToName, Coordinates From, Coordinates To);
public sealed record GalaxyResolvedConstellation(string Name, IReadOnlyList<GalaxyConstellationLine> Lines);
public sealed record GalaxyConstellationResolution(IReadOnlyList<GalaxyResolvedConstellation> Groups,
IReadOnlyList<string> MissingEndpointNames, int PendingGroupCount)
{
public bool IsComplete => PendingGroupCount == 0;
}
/// <summary>
/// The authored game graph, resolved exclusively against actual catalogue positions.
/// Original 3A28A40 stores compact XYZ / 32 without a shell or other spatial transform.
/// Coordinates here use the same light years with the catalogue's Sol origin subtracted.
/// </summary>
public static class GalaxyOriginalConstellations
{
public const string SourceResource = "d30f11c57448cff8ec3dd0c5c80d9868482ccadb";
public const string SourceSha256 = "ba4e049fd4eae066e72f29898a25d52692422e5905fc7a4a65105e44eb631ade";
public const double FullOpacityDistance = 8;
public const double InvisibleDistance = 24;
public const float SubmissionAlpha = .1f;
// Original rendercfg+190, style6 -> renderer+540. The group consumer
// replaces only its alpha before the shared StarMapLine64B writer.
public const int MaximumSegments = 206;
public const float WorldHalfWidth = .1f;
public const float TextureRepeatPerLightYear = 2.5f;
public static IReadOnlyList<GalaxyConstellationDefinition> Definitions { get; } = Read();
public static IReadOnlyList<string> EndpointNames { get; } = Array.AsReadOnly(Definitions
.SelectMany(g => g.Edges.SelectMany(e => new[] { e.FromName, e.ToName }))
.Distinct(StringComparer.OrdinalIgnoreCase).ToArray());
/// <summary>
/// A group is returned only when every endpoint is available, matching the original
/// ready-handle gate. Missing facts remain pending; no coordinates are synthesized.
/// Each distinct endpoint is read once so one resolution uses a consistent snapshot.
/// </summary>
public static GalaxyConstellationResolution Resolve(Func<string, Coordinates?> resolvePosition)
{
ArgumentNullException.ThrowIfNull(resolvePosition);
var positions = new Dictionary<string, Coordinates>(StringComparer.OrdinalIgnoreCase);
var missing = new List<string>();
foreach (var name in EndpointNames)
{
if (resolvePosition(name) is { IsValid: true } position) positions.Add(name, position);
else missing.Add(name);
}
var ready = new List<GalaxyResolvedConstellation>();
foreach (var group in Definitions)
{
if (group.Edges.Any(e => !positions.ContainsKey(e.FromName) || !positions.ContainsKey(e.ToName))) continue;
var lines = group.Edges.Select(e => new GalaxyConstellationLine(e.FromName, e.ToName,
positions[e.FromName], positions[e.ToName])).ToArray();
ready.Add(new(group.Name, Array.AsReadOnly(lines)));
}
return new(ready.AsReadOnly(), missing.AsReadOnly(), Definitions.Count - ready.Count);
}
/// <summary>
/// Original 3A1CF60: squared forward-facing cosine, multiplied by the linear
/// eye-to-Sol fade (opaque through 8 ly, zero at 24 ly). The group direction
/// averages ONLY the first endpoint of each authored edge, preserving duplicates.
/// The caller submits lines only when the returned alpha exceeds SubmissionAlpha.
/// </summary>
public static double Alpha(GalaxyResolvedConstellation group, Coordinates eye, Coordinates focus)
{
ArgumentNullException.ThrowIfNull(group);
if (!eye.IsValid || !focus.IsValid || group.Lines.Count == 0 ||
group.Lines.Any(line => !line.From.IsValid || !line.To.IsValid)) return 0;
double x = 0, y = 0, z = 0;
foreach (var line in group.Lines) { x += line.From.X; y += line.From.Y; z += line.From.Z; }
x /= group.Lines.Count; y /= group.Lines.Count; z /= group.Lines.Count;
var length = Math.Sqrt(x * x + y * y + z * z);
if (length == 0) { x = 0; y = 0; z = 1; }
else { x /= length; y /= length; z /= length; }
var vx = focus.X - eye.X; var vy = focus.Y - eye.Y; var vz = focus.Z - eye.Z;
var viewLength = Math.Sqrt(vx * vx + vy * vy + vz * vz);
// The original zero-length view branch retains its initial directional alpha 1.
var facing = viewLength > 0 ? Math.Clamp((vx * x + vy * y + vz * z) / viewLength, 0, 1) : 1;
var distance = Math.Sqrt(eye.X * eye.X + eye.Y * eye.Y + eye.Z * eye.Z);
return facing * facing * Math.Clamp((distance - InvisibleDistance) /
(FullOpacityDistance - InvisibleDistance), 0, 1);
}
private static IReadOnlyList<GalaxyConstellationDefinition> Read()
{
using var stream = typeof(GalaxyOriginalConstellations).Assembly
.GetManifestResourceStream("XFEEDToolBox.Core.Galaxy.original-constellations.json")
?? throw new InvalidDataException("原星座连接资源缺失。");
using var document = JsonDocument.Parse(stream);
var root = document.RootElement;
if (root.GetProperty("sourceResource").GetString() != SourceResource ||
root.GetProperty("sourceSha256").GetString() != SourceSha256)
throw new InvalidDataException("原星座连接来源校验失败。");
var groups = root.GetProperty("groups").EnumerateArray().Select(g => new GalaxyConstellationDefinition(
g.GetProperty("name").GetString()!, Array.AsReadOnly(g.GetProperty("edges").EnumerateArray()
.Select(e => new GalaxyConstellationEdge(e.GetProperty("fromName").GetString()!,
e.GetProperty("toName").GetString()!)).ToArray()))).ToArray();
if (groups.Length != 16 || groups.Sum(g => g.Edges.Count) != 206 ||
groups.Any(g => string.IsNullOrWhiteSpace(g.Name) || g.Edges.Count == 0 ||
g.Edges.Any(e => string.IsNullOrWhiteSpace(e.FromName) || string.IsNullOrWhiteSpace(e.ToName))))
throw new InvalidDataException("原星座连接图无效。");
return Array.AsReadOnly(groups);
}
}