Aetheria/tests/Aetheria.Shared.Tests/HardpointOriginTests.cs
Nibu 95568140e2 Pin the deployed guard of the out ItemFits overload
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-13 13:35:15 +00:00

163 lines
8.2 KiB
C#

using System.Collections.Generic;
using System.Linq;
using CultMath;
using Xunit;
using static CultMath.math;
// Hardpoint gear holds the hardpoint it was seated in across save and reload: an origin two hardpoints accept is
// resolved by the item's saved Rotation (HardpointData.Rotation), not by hardpoint declaration order.
public sealed partial class RunStartTests
{
// Two disjoint ballistic hardpoints, A (None, cell (6,5)) and B (Reversed, cell (5,5)). The edge gun's single cell is
// (1,0): origin (5,5) puts it at (6,5) unturned and at (5,5) turned 180 degrees, so both hardpoints take it there.
private (HullData hull, HardpointData a, HardpointData b) TwinMountHull(string name)
{
var a = Mount(HardpointType.Ballistic, 6, 5, rotation: ItemRotation.None);
var b = Mount(HardpointType.Ballistic, 5, 5, rotation: ItemRotation.Reversed);
return (ScratchHull(name, 12, a, b), a, b);
}
[Fact]
public void AnOriginTwoHardpointsAcceptReloadsIntoTheHardpointItHeld()
{
var (hull, _, b) = TwinMountHull("Origin Twin Mounts");
var filler = ScratchGear("Origin Filler", HardpointType.Ballistic, 1, 1);
var edge = ScratchGear("Origin Edge Gun", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
Assert.True(entity.TryEquip(Instance(filler), int2(6, 5)));
Assert.True(entity.TryEquip(Instance(edge), int2(5, 5)));
var held = entity.Equipment.Single(i => i.Data == edge);
Assert.Same(b, held.Hardpoint);
Assert.Equal(ItemRotation.Reversed, held.EquippableItem.Rotation);
// Un-equip the filler as a player would, then save and reload.
entity.TryUnequip(entity.Equipment.Single(i => i.Data == filler));
var reloaded = EntitySerializer.Unpack(_items, null, EntitySerializer.Pack(entity));
var after = reloaded.Equipment.Single(i => i.Data == edge);
Assert.Equal(1, reloaded.HullData.Hardpoints.IndexOf(after.Hardpoint));
}
[Fact]
public void ReloadKeepsTheFirstHardpointToo()
{
var (hull, _, _) = TwinMountHull("Origin Twin Mounts First");
var edge = ScratchGear("Origin Edge Gun First", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
Assert.True(entity.TryEquip(Instance(edge), int2(5, 5)));
Assert.Equal(ItemRotation.None, entity.Equipment.Single(i => i.Data == edge).EquippableItem.Rotation);
var reloaded = EntitySerializer.Unpack(_items, null, EntitySerializer.Pack(entity));
var after = reloaded.Equipment.Single(i => i.Data == edge);
Assert.Equal(0, reloaded.HullData.Hardpoints.IndexOf(after.Hardpoint));
}
[Fact]
public void AnItemWhoseRotationNamesNoAcceptingHardpointStillSeats()
{
var (hull, a, _) = TwinMountHull("Origin Twin Mounts Stray");
var edge = ScratchGear("Origin Edge Gun Stray", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
var item = Instance(edge);
item.Rotation = ItemRotation.Clockwise;
Assert.True(entity.ItemFits(item, int2(5, 5)));
Assert.True(entity.TryEquip(item, int2(5, 5)));
var held = entity.Equipment.Single(i => i.Data == edge);
Assert.Same(a, held.Hardpoint);
Assert.Equal(ItemRotation.None, held.EquippableItem.Rotation);
}
// A probe reports; only TryEquip seats. The probe used to turn the item to the hardpoint it found, so the same item
// probed twice could resolve differently, and a failed or merely previewed placement left it turned.
[Fact]
public void AProbeLeavesTheItemsRotationAlone()
{
var (hull, a, b) = TwinMountHull("Origin Twin Mounts Probe");
var edge = ScratchGear("Origin Edge Gun Probe", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
var item = Instance(edge);
item.Rotation = ItemRotation.Clockwise;
Assert.True(entity.ItemFits(item, int2(5, 5)));
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.ItemFits(item, int2(5, 5), out var seated));
Assert.Equal(ItemRotation.None, seated);
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.TryFindSpace(item, out _));
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.False(entity.ItemFits(item, int2(0, 0), out var unseated));
Assert.Equal(ItemRotation.Clockwise, unseated);
Assert.Equal(ItemRotation.Clockwise, item.Rotation);
Assert.True(entity.TryEquip(item, int2(5, 5)));
var held = entity.Equipment.Single(i => i.Data == edge);
Assert.Same(a, held.Hardpoint);
Assert.Equal(ItemRotation.None, held.EquippableItem.Rotation);
var reversed = Instance(edge);
reversed.Rotation = ItemRotation.Reversed;
var fresh = BareHull(hull);
Assert.True(fresh.ItemFits(reversed, int2(5, 5), out var first));
Assert.True(fresh.ItemFits(reversed, int2(5, 5), out var second));
Assert.Equal(ItemRotation.Reversed, first);
Assert.Equal(first, second);
Assert.Equal(b.Rotation, first);
}
// A deployed entity takes no gear, so no probe reports a fit or a seat on one (the out overload's own guard).
// Kills: a probe that ignores the deployed state and offers a placement TryEquip would then refuse.
[Fact]
public void ADeployedEntityTakesNoProbe()
{
var (hull, _, _) = TwinMountHull("Origin Twin Mounts Deployed");
var edge = ScratchGear("Origin Edge Gun Deployed", HardpointType.Ballistic, EdgeShape());
var entity = BareHull(hull);
var item = Instance(edge);
item.Rotation = ItemRotation.Clockwise;
Assert.True(entity.ItemFits(item, int2(5, 5), out _));
var zone = Arena(false);
entity.Zone = zone;
zone.Admit(entity, piloted: false);
Assert.False(entity.ItemFits(item, int2(5, 5), out var seated));
Assert.Equal(ItemRotation.Clockwise, seated);
Assert.False(entity.ItemFits(item, int2(5, 5)));
Assert.False(entity.TryEquip(item, int2(5, 5)));
}
}
// The shipped catalog authors no origin that two differently turned hardpoints of one type both take. The fit rule alone
// over-approximates what Entity.HardpointAt sees (occupancy only removes candidates), so zero here is stricter than needed.
public sealed class ShippedHardpointOriginTests
{
[Fact]
public void TheShippedCatalogHasNoAmbiguousOrigin()
{
using var cache = RestoredHullsTests.OpenCatalog();
var designs = cache.GetAll<EquippableItemData>().Where(d => !(d is HullData) && d.HardpointType != HardpointType.Tool).ToArray();
var ambiguous = new List<string>();
var pairs = 0;
var probes = 0;
foreach (var hull in cache.GetAll<HullData>())
for (var i = 0; i < hull.Hardpoints.Count; i++)
for (var j = i + 1; j < hull.Hardpoints.Count; j++)
{
var a = hull.Hardpoints[i];
var b = hull.Hardpoints[j];
if (a.Type != b.Type || a.Rotation == b.Rotation) continue;
pairs++;
foreach (var design in designs.Where(d => d.HardpointType == a.Type))
for (var x = -20; x < hull.Shape.Width; x++)
for (var y = -20; y < hull.Shape.Height; y++)
{
var origin = int2(x, y);
probes++;
if (a.TakesAt(design, origin - a.Position) && b.TakesAt(design, origin - b.Position))
ambiguous.Add($"{hull.Name} {design.Name} at ({x},{y}): hardpoints {i} and {j}");
}
}
System.Console.WriteLine($"ShippedHardpointOriginTests visited {pairs} same-type different-rotation pairs");
// A scan that visits nothing would pass; the catalog has such pairs today, so losing them must fail loudly.
Assert.True(pairs > 0 && probes > 0, $"the scan visited {pairs} pairs and {probes} origins");
Assert.True(ambiguous.Count == 0, $"{ambiguous.Count} ambiguous origins: " + string.Join("; ", ambiguous.Take(10)));
}
}