From 043a73eb0af9f3b3ba34e730256e39106fb4e34e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 21:32:50 +0200 Subject: [PATCH] Smoke Test Hull: 2x2 side mounts on a two-cell margin; test classes read sold hulls only The 1x1 Ballistic side mount tripped IsFilledByAgreesWithACellOracle (Shape.FitsWithin never places a design whose only cell is not its origin, so Takes and the oracle disagree on a 1x1 hardpoint); the side mounts are 2x2 now, which needs two columns of margin on each side, so every Longinus cell and hardpoint moves right by two. The two catalog-wide tests that listed every hull now list sold hulls. This commit does not pass the suite: GameData/Aetheria.cc still holds the previous hull until the next commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/ServerShared/Scenarios/Arcs.cs | 30 +++++++------- .../ServerShared/Scenarios/WeaponFeel.cs | 24 ++++++------ .../RestoredHullsTests.cs | 5 ++- tests/Aetheria.Shared.Tests/RunStartTests.cs | 6 +-- tests/Aetheria.Shared.Tests/ScenarioTests.cs | 2 +- .../SmokeHullCommandTests.cs | 13 ++++--- .../SmokeHullScenarioTests.cs | 6 +-- tools/AetherDb/Program.cs | 39 +++++++++++-------- 8 files changed, 68 insertions(+), 57 deletions(-) diff --git a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs index c9b0d0f9a..b054d8e12 100644 --- a/Assets/Scripts/ServerShared/Scenarios/Arcs.cs +++ b/Assets/Scripts/ServerShared/Scenarios/Arcs.cs @@ -24,22 +24,22 @@ public sealed class Arcs : Scenario public override void Stage(ScenarioStage stage) { var fighter = stage.Fit("Smoke Test Hull", - ("Cockpit 2x2", int2(2, 6), None), - ("Large Drive", int2(1, 0), Reversed), + ("Cockpit 2x2", int2(4, 6), None), ("Large Drive", int2(3, 0), Reversed), - ("Talaria", int2(2, 14), CounterClockwise), - ("Talaria", int2(3, 14), Clockwise), - ("Core Power", int2(2, 4), None), - ("GT 3K", int2(0, 5), None), - ("GT 3K", int2(5, 5), None), - ("FastBlast+-", int2(1, 8), None), - ("FastBlast+-", int2(4, 8), None), - ("Iapyx", int2(2, 2), CounterClockwise), - ("Iapyx", int2(3, 2), Clockwise), - ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(1, 6), Clockwise), - ("Point Defense Gun", int2(5, 4), Clockwise), - ("Point Defense Laser", int2(0, 4), CounterClockwise)); + ("Large Drive", int2(5, 0), Reversed), + ("Talaria", int2(4, 14), CounterClockwise), + ("Talaria", int2(5, 14), Clockwise), + ("Core Power", int2(4, 4), None), + ("GT 3K", int2(2, 5), None), + ("GT 3K", int2(7, 5), None), + ("FastBlast+-", int2(3, 8), None), + ("FastBlast+-", int2(6, 8), None), + ("Iapyx", int2(4, 2), CounterClockwise), + ("Iapyx", int2(5, 2), Clockwise), + ("not if i see you first", int2(5, 10), None), + ("Fire Control Array", int2(3, 6), Clockwise), + ("Autocannon", int2(8, 4), Clockwise), + ("ChargeBlast SG", int2(0, 4), CounterClockwise)); var turret = stage.Fit("Turret", ("ClearPath", int2(1, 2), None), ("ClearPath", int2(5, 2), None), diff --git a/Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs b/Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs index 9e7f8b597..95cc36123 100644 --- a/Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs +++ b/Assets/Scripts/ServerShared/Scenarios/WeaponFeel.cs @@ -18,19 +18,19 @@ public sealed class WeaponFeel : Scenario public override void Stage(ScenarioStage stage) { var shooter = stage.Fit("Smoke Test Hull", - ("Cockpit 2x2", int2(2, 6), None), - ("Large Drive", int2(1, 0), Reversed), + ("Cockpit 2x2", int2(4, 6), None), ("Large Drive", int2(3, 0), Reversed), - ("Talaria", int2(2, 14), CounterClockwise), - ("Talaria", int2(3, 14), Clockwise), - ("Core Power", int2(2, 4), None), - ("ChargeBlast SG", int2(1, 17), None), - ("Autocannon", int2(3, 17), None), - ("FastBlast+-", int2(1, 8), None), - ("Iapyx", int2(2, 2), CounterClockwise), - ("Iapyx", int2(3, 2), Clockwise), - ("not if i see you first", int2(3, 10), None), - ("Fire Control Array", int2(1, 6), Clockwise)); + ("Large Drive", int2(5, 0), Reversed), + ("Talaria", int2(4, 14), CounterClockwise), + ("Talaria", int2(5, 14), Clockwise), + ("Core Power", int2(4, 4), None), + ("ChargeBlast SG", int2(3, 17), None), + ("Autocannon", int2(5, 17), None), + ("FastBlast+-", int2(3, 8), None), + ("Iapyx", int2(4, 2), CounterClockwise), + ("Iapyx", int2(5, 2), Clockwise), + ("not if i see you first", int2(5, 10), None), + ("Fire Control Array", int2(3, 6), Clockwise)); stage.Player(shooter, float2(0, 0), facing: float2(0, 1)); stage.Place(stage.Bare("Longinus"), Target); diff --git a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs index e2ac85f49..4086e4b4a 100644 --- a/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs +++ b/tests/Aetheria.Shared.Tests/RestoredHullsTests.cs @@ -248,13 +248,14 @@ public sealed class RestoredHullsTests } // The dock intro dereferences the ship's VelocityLimit unguarded (ActionGameManager), so every ship hull the catalog - // ships carries exactly one, with a top speed that is a finite positive number. After the retirement + // sells carries exactly one, with a top speed that is a finite positive number. After the retirement // (ruling retire-longinusx) the shipped ship hulls are the Longinus and the Djinni. [Fact] public void EveryShippedShipHullHasATopSpeed() { using var cache = OpenCatalog(); - var ships = cache.GetAll().Where(h => h.HullType == HullType.Ship).OrderBy(h => h.Name, StringComparer.Ordinal).ToArray(); + var sold = cache.GetAll().Select(product => product.Design.Key).ToHashSet(); + var ships = cache.GetAll().Where(h => h.HullType == HullType.Ship && sold.Contains(cache.RefOf(h).Key)).OrderBy(h => h.Name, StringComparer.Ordinal).ToArray(); Assert.Equal(new[] { "Djinni", "Longinus" }, ships.Select(h => h.Name)); foreach (var hull in ships) { diff --git a/tests/Aetheria.Shared.Tests/RunStartTests.cs b/tests/Aetheria.Shared.Tests/RunStartTests.cs index f1e76f5f6..321048e44 100644 --- a/tests/Aetheria.Shared.Tests/RunStartTests.cs +++ b/tests/Aetheria.Shared.Tests/RunStartTests.cs @@ -439,13 +439,13 @@ public sealed partial class RunStartTests : IDisposable Assert.True(disagreements.Count == 0, string.Join("; ", disagreements.Take(10)) + $" ({disagreements.Count} in all)"); } - // The sold hardpoint gear that no hull fits even loosely today, stated so that authoring a hull that takes one - // shows up here as a changed list. + // The sold hardpoint gear that no sold hull fits even loosely today (a test hull no product sells gives no gear a + // home), stated so that authoring a hull that takes one shows up here as a changed list. [Fact] public void TheSoldHardpointGearWithNoHomeToday() { - var hulls = _cache.GetAll().ToArray(); var sold = _cache.GetAll().Select(product => product.Design.Key).ToHashSet(); + var hulls = _cache.GetAll().Where(hull => sold.Contains(_cache.RefOf(hull).Key)).ToArray(); var homeless = _cache.GetAll() .Where(design => sold.Contains(_cache.RefOf(design).Key) && design.Price > 0) .Where(design => design.HardpointType != HardpointType.Tool && design.HardpointType != HardpointType.Hull) diff --git a/tests/Aetheria.Shared.Tests/ScenarioTests.cs b/tests/Aetheria.Shared.Tests/ScenarioTests.cs index 8241db13c..73097e9ab 100644 --- a/tests/Aetheria.Shared.Tests/ScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/ScenarioTests.cs @@ -489,7 +489,7 @@ public sealed partial class RunStartTests var turret = Assert.IsType(staged.Entities[2]); Assert.Equal((Arcs.Bow, Arcs.Stern, Arcs.Turret), (bow.Position.xz, stern.Position.xz, turret.Position.xz)); - var mounts = player.Equipment.Where(item => item.Behaviors.OfType().Any() && item.Hardpoint.Rotation == ItemRotation.None).ToList(); + var mounts = player.Equipment.Where(item => item.Behaviors.OfType().Any() && item.EquippableItem.Rotation == ItemRotation.None).ToList(); Assert.NotEmpty(mounts); foreach (var mount in mounts) { diff --git a/tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs b/tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs index 2badf502e..1fc75c453 100644 --- a/tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs +++ b/tests/Aetheria.Shared.Tests/SmokeHullCommandTests.cs @@ -37,7 +37,7 @@ public sealed class SmokeHullCommandTests : IDisposable var hulls = db.Cache.GetAll().Where(h => h.Name == HullName).ToArray(); var keys = hulls.Select(h => db.Cache.RefOf(h).Key).ToArray(); var products = db.Cache.GetAll().Count(p => keys.Any(k => p.Design.Key.Equals(k))); - var fitted = new[] { "ChargeBlast SG", "Autocannon", "Point Defense Gun", "Point Defense Laser" } + var fitted = new[] { "ChargeBlast SG", "Autocannon" } .Select(name => db.Cache.GetAll().Single(d => d.Name == name)).ToArray(); return (hulls, db.Cache.GetAll().Single(h => h.Name == "Longinus"), fitted, products); } @@ -61,14 +61,17 @@ public sealed class SmokeHullCommandTests : IDisposable var hull = Assert.Single(hulls); Assert.Equal(0, products); - // The Longinus's own hardpoints survive, and four are added: each takes the gear the scenarios fit to it. + // The Longinus's own hardpoints survive (moved over by the margin the flank blocks need), and four are added: each takes + // the gear the scenarios fit to it. Assert.Equal(longinus.Hardpoints.Count + 4, hull.Hardpoints.Count); var added = hull.Hardpoints.Skip(longinus.Hardpoints.Count).ToArray(); - var (chargeBlast, autocannon, pdGun, pdLaser) = (fitted[0], fitted[1], fitted[2], fitted[3]); + var (chargeBlast, autocannon) = (fitted[0], fitted[1]); + Assert.Equal(longinus.Hardpoints.Select(h => (h.Type, h.Position.x + 2, h.Position.y, h.Rotation)), + hull.Hardpoints.Take(longinus.Hardpoints.Count).Select(h => (h.Type, h.Position.x, h.Position.y, h.Rotation))); Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.None && h.Takes(chargeBlast)); Assert.Single(added, h => h.Type == HardpointType.Ballistic && h.Rotation == ItemRotation.None && h.Takes(autocannon)); - Assert.Single(added, h => h.Type == HardpointType.Ballistic && h.Rotation == ItemRotation.Clockwise && h.Takes(pdGun)); - Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.CounterClockwise && h.Takes(pdLaser)); + Assert.Single(added, h => h.Type == HardpointType.Ballistic && h.Rotation == ItemRotation.Clockwise && h.Takes(autocannon)); + Assert.Single(added, h => h.Type == HardpointType.Energy && h.Rotation == ItemRotation.CounterClockwise && h.Takes(chargeBlast)); foreach (var hardpoint in added) foreach (var cell in hardpoint.Shape.Coordinates) Assert.True(hull.Shape[hardpoint.Position + cell], "a new hardpoint lies outside the hull's cells"); diff --git a/tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs b/tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs index f67fff0a6..b7eb3edfd 100644 --- a/tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs +++ b/tests/Aetheria.Shared.Tests/SmokeHullScenarioTests.cs @@ -49,9 +49,9 @@ public sealed partial class RunStartTests Assert.Equal((Arcs.Port, Arcs.Starboard), (port.Position.xz, starboard.Position.xz)); var mounts = WeaponMounts(player); - var forward = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.None).ToList(); - var starboardMounts = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.Clockwise).ToList(); - var portMounts = mounts.Where(item => item.Hardpoint.Rotation == ItemRotation.CounterClockwise).ToList(); + var forward = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.None).ToList(); + var starboardMounts = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.Clockwise).ToList(); + var portMounts = mounts.Where(item => item.EquippableItem.Rotation == ItemRotation.CounterClockwise).ToList(); Assert.NotEmpty(forward); Assert.NotEmpty(starboardMounts); Assert.NotEmpty(portMounts); diff --git a/tools/AetherDb/Program.cs b/tools/AetherDb/Program.cs index 38d0fa039..b24161383 100644 --- a/tools/AetherDb/Program.cs +++ b/tools/AetherDb/Program.cs @@ -1606,8 +1606,9 @@ public static class Program // Smoke Test Hull, a copy of the Longinus hull that no product sells, so no generated ship, station or shop offers // it and only a scenario fits it. It adds what no shipped ship hull has, where the Longinus runs out of cells: two // rows of cells past the bow carrying a 2x2 Energy hardpoint (ChargeBlast SG) and a 2x2 Ballistic hardpoint - // (Autocannon), and a 1x1 side mount on each flank, a Clockwise Ballistic one on the starboard and a - // CounterClockwise Energy one on the port. The Longinus's own hardpoints are untouched. Its prefab is the + // (Autocannon), and a 2x2 side mount on each flank, a Clockwise Ballistic one on the starboard and a + // CounterClockwise Energy one on the port, which need two columns of margin on each side: every Longinus cell and + // hardpoint moves right by two. Their gear is unchanged. Its prefab is the // Longinus's, which has no barrel for the new hardpoints: they fire from the hull centre. Dry run unless passed // "apply". public static int SmokeHullCatalog(bool apply, string root = null) @@ -1626,26 +1627,32 @@ public static class Program hull.Name = name; hull.Description = "The Longinus with two forward 2x2 hardpoints and a side mount on each flank, for scenarios. No faction builds it."; - // Two rows of cells past the bow, over the width of the tail's neighbours (x 1..4). - var grown = new Shape(longinus.Shape.Width, longinus.Shape.Height + 2); + // Two cells of margin on each side (every Longinus cell and hardpoint moves right by Margin), two rows of cells past + // the bow over the width of the tail's neighbours, and a 2x2 block of cells on each flank at the wide section. + const int Margin = 2; + var width = longinus.Shape.Width + 2 * Margin; + var bow = longinus.Shape.Height; + var grown = new Shape(width, bow + 2); for (var x = 0; x < longinus.Shape.Width; x++) - for (var y = 0; y < longinus.Shape.Height; y++) grown.Cells[x, y] = longinus.Shape.Cells[x, y]; - for (var x = 1; x <= 4; x++) - for (var y = longinus.Shape.Height; y < grown.Height; y++) grown.Cells[x, y] = true; + for (var y = 0; y < bow; y++) grown.Cells[x + Margin, y] = longinus.Shape.Cells[x, y]; + for (var x = Margin + 1; x <= Margin + 4; x++) + for (var y = bow; y < grown.Height; y++) grown.Cells[x, y] = true; + foreach (var x in new[] { 0, 1, width - 2, width - 1 }) + for (var y = 4; y <= 5; y++) grown.Cells[x, y] = true; hull.Shape = grown; + foreach (var existing in hull.Hardpoints) existing.Position = CultMath.math.int2(existing.Position.x + Margin, existing.Position.y); - static Shape Cells(int width, int height) + static Shape Cells(int side) { - var shape = new Shape(width, height); - for (var x = 0; x < width; x++) - for (var y = 0; y < height; y++) shape.Cells[x, y] = true; + var shape = new Shape(side, side); + for (var x = 0; x < side; x++) + for (var y = 0; y < side; y++) shape.Cells[x, y] = true; return shape; } - var bow = longinus.Shape.Height; - hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(1, bow), Shape = Cells(2, 2), Rotation = ItemRotation.None, Armor = 20f }); - hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(3, bow), Shape = Cells(2, 2), Rotation = ItemRotation.None, Armor = 20f }); - hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(longinus.Shape.Width - 1, 4), Shape = Cells(1, 1), Rotation = ItemRotation.Clockwise, Armor = 10f }); - hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(0, 4), Shape = Cells(1, 1), Rotation = ItemRotation.CounterClockwise, Armor = 10f }); + hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(Margin + 1, bow), Shape = Cells(2), Rotation = ItemRotation.None, Armor = 20f }); + hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(Margin + 3, bow), Shape = Cells(2), Rotation = ItemRotation.None, Armor = 20f }); + hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Ballistic, Position = CultMath.math.int2(width - 2, 4), Shape = Cells(2), Rotation = ItemRotation.Clockwise, Armor = 20f }); + hull.Hardpoints.Add(new HardpointData { Type = HardpointType.Energy, Position = CultMath.math.int2(0, 4), Shape = Cells(2), Rotation = ItemRotation.CounterClockwise, Armor = 20f }); Console.WriteLine($"Adds \"{name}\": a Longinus copy grown from {longinus.Shape.Width}x{longinus.Shape.Height} to {grown.Width}x{grown.Height}, no product."); foreach (var hardpoint in hull.Hardpoints.Skip(longinus.Hardpoints.Count))