From f38862aabc55744d9736511c48aa780a2e78f2a8 Mon Sep 17 00:00:00 2001 From: Nibu Date: Fri, 13 Nov 2026 09:33:16 +0000 Subject: [PATCH 1/2] Test: reload keeps the hardpoint an origin two hardpoints accept (fails at base) Co-Authored-By: Claude Sonnet 5.5 --- .../HardpointOriginTests.cs | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 tests/Aetheria.Shared.Tests/HardpointOriginTests.cs diff --git a/tests/Aetheria.Shared.Tests/HardpointOriginTests.cs b/tests/Aetheria.Shared.Tests/HardpointOriginTests.cs new file mode 100644 index 000000000..7087f3090 --- /dev/null +++ b/tests/Aetheria.Shared.Tests/HardpointOriginTests.cs @@ -0,0 +1,67 @@ +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); + } +} -- 2.47.3 From eea98d44140b2484923727063f30d44632dc31a3 Mon Sep 17 00:00:00 2001 From: Nibu Date: Fri, 13 Nov 2026 09:33:23 +0000 Subject: [PATCH 2/2] HardpointAt prefers the hardpoint matching the item's Rotation Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/Entity.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index b520db17d..6f7683822 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -1545,7 +1545,7 @@ public abstract class Entity } else { - hardpoint = HardpointAt(itemData, hullData, hullCoord, moving); + hardpoint = HardpointAt(itemData, hullData, hullCoord, item.Rotation, moving); if (hardpoint == null) return false; item.Rotation = hardpoint.Rotation; } @@ -1560,10 +1560,12 @@ public abstract class Entity // The one placement rule for hardpoint gear. At this origin the item lies within a hardpoint of its type, turned to // that hardpoint's rotation (HardpointData.TakesAt, the fit rule); its own cells are free, since a hardpoint's // leftover cells may hold general gear (operator, 2026-09-30: "saving slots in a hardpoint can be a valid tradeoff - // for crowded ships"); and that hardpoint holds no other hardpoint item, since it holds one at most. - private HardpointData HardpointAt(EquippableItemData itemData, HullData hullData, int2 hullCoord, EquippedItem moving) + // for crowded ships"); and that hardpoint holds no other hardpoint item, since it holds one at most. Where several hardpoints accept the origin, the + // one whose rotation equals the item's own (saved) rotation is preferred, so reload seats gear where it was; an item whose + // rotation names none of them takes the first (stable order). + private HardpointData HardpointAt(EquippableItemData itemData, HullData hullData, int2 hullCoord, ItemRotation seated, EquippedItem moving) { - foreach (var hardpoint in hullData.Hardpoints) + foreach (var hardpoint in hullData.Hardpoints.OrderBy(h => h.Rotation == seated ? 0 : 1)) { if (!hardpoint.TakesAt(itemData, hullCoord - hardpoint.Position)) continue; var ownCellsFree = itemData.Shape.Coordinates -- 2.47.3