Aetheria/tests/Aetheria.Shared.Tests/ZoneFingerprint.cs
soul d0fc8adb70 Gravity: a wider zone fingerprint, one copied-rail helper, belts kinded at generation, adjacent belts pinned
ZoneFingerprint digests rock spin and periods, orbital entities (type, position, rail), other entities by type, the
pack radius, a 9x9 height grid at two times and optionally PopulatedRadius (finding fingerprint-omits).
TrojanSwarmTests.BaseDigest is repinned with it on Yggdrasil at C1's generator, swarms off.

BeltShapes.CopiedRail replaces StationPlacementTests.SwarmRail and the RidesRail slot filters of
ChildrenWithinReachTests: a rail a belt rides that copies a body's rail is never a placed slot.

The Zones fixture counts the belts the generator writes and those with no field kind, before Zone's own
FieldKinds.Ensure; EveryBeltIsKindedAtGeneration pins it at zero (finding ensure-unpinned).
AdjacentBeltsKeepTheirSpacing places two belts side by side and pins the second's inner edge a sibling spacing
outside the first's outer edge (follow-up belt-after-belt-edge-pin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:43:23 +02:00

50 lines
3.1 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using CultMath;
using GameCult.Caching;
using static CultMath.math;
using float2 = CultMath.float2;
// A digest of what a zone's generation and build decide, so a test can pin it to a value computed at an earlier commit:
// every orbit's (Distance, Phase, Period, Reach, Support); every body's (type, Mass, rail Distance) and each belt rock's
// (Distance, Phase, Size, RotationSpeed, Period); every orbital entity's (type, position, rail Distance and Phase) and any other
// entity's type alone (a ship packed at the origin is scattered from the clock-seeded item stream, Zone.cs:276); the pack radius;
// GetHeight on a 9x9 grid over the pack at t 0 and 777.7; and, when asked, PopulatedRadius. Cache keys and names are left
// out; they are minted, not decided.
public static class ZoneFingerprint
{
public static string Of(IEnumerable<Zone> zones, Func<CultRecordKey, bool> skipOrbit, Func<BodyData, bool> skipBody, bool populatedRadius)
{
var text = new StringBuilder();
foreach (var zone in zones) text.Append(Describe(zone, skipOrbit, skipBody, populatedRadius)).Append('\n');
return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text.ToString()))).ToLowerInvariant();
}
private static string Describe(Zone z, Func<CultRecordKey, bool> skipOrbit, Func<BodyData, bool> skipBody, bool populatedRadius)
{
string F(double v) => v.ToString("R", System.Globalization.CultureInfo.InvariantCulture);
var orbits = z.Orbits.Where(o => !skipOrbit(o.Key))
.Select(o => $"{F(o.Value.Data.Distance)},{F(o.Value.Data.Phase)},{F(o.Value.Period)},{F(z.Wells.Reach(o.Key))},{F(z.Wells.Support(o.Key))}")
.OrderBy(x => x, StringComparer.Ordinal);
var bodies = z.Planets.Where(b => !skipBody(b.Value))
.Select(b => $"{b.Value.GetType().Name},{F(b.Value.Mass)},{F(z.Orbits[b.Value.Orbit.Key].Data.Distance)}," +
(b.Value is AsteroidBeltData belt ? string.Join("|", belt.Asteroids.Select((r, i) =>
$"{F(r.Distance)}/{F(r.Phase)}/{F(r.Size)}/{F(r.RotationSpeed)}/{F(z.AsteroidBelts[b.Key].Periods[i])}")) : ""))
.OrderBy(x => x, StringComparer.Ordinal);
var entities = z.Entities.Select(e => e is OrbitalEntity o
? $"{e.GetType().Name},{F(e.Position.x)},{F(e.Position.y)},{F(e.Position.z)},{F(z.Orbits[o.OrbitData].Data.Distance)},{F(z.Orbits[o.OrbitData].Data.Phase)}"
: e.GetType().Name)
.OrderBy(x => x, StringComparer.Ordinal);
var r = z.Pack.Radius;
var heights = new List<string>();
foreach (var t in new[] { 0.0, 777.7 })
for (var i = -4; i <= 4; i++)
for (var j = -4; j <= 4; j++)
heights.Add(F(z.GetHeight(float2(i * r / 4f, j * r / 4f), t)));
return string.Join(";", orbits) + "#" + string.Join(";", bodies) + "#" + string.Join(";", entities) + "#" + F(r) + "#" +
string.Join(",", heights) + (populatedRadius ? "#" + F(z.PopulatedRadius) : "");
}
}