From 6bd3e1769022c71b621dfc1e4bb83c15cb4d5f65 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 12 Sep 2026 23:22:36 +0200 Subject: [PATCH] Fix station docking bays, save-safe instance keys, and zero-weight draws One rule now governs interior space: an empty hardpoint cell is usable, in both UnoccupiedSpace and ItemFits. ItemFits used to refuse hardpoint cells that UnoccupiedSpace offered, so stations failed to equip a docking bay. CraftedItemInstance.Ingredients and Product move from keys 3 and 4 to 9 and 10, past every key the derived instances use. At 3 and 4 an older save reads nil into a non-nullable Guid and fails to load. WeightedRandomElements returns an empty array when every weight is zero, instead of an array of nulls. LoadoutGenerator treats a missing Galaxy as "every product available", the seam a fixture needs to generate loadouts without a galaxy. ZoneGenerator skips a story station that fails to generate and logs it, instead of throwing. Verified: Aetheria.Shared and tools/AetherDb build headless; Unity 6000.3.24f1 batchmode compile succeeds; AetherDb doctor, station-fit and loadout pass. Co-Authored-By: Claude Opus 5 --- Assets/Scripts/ServerShared/Entity.cs | 10 ++++------ Assets/Scripts/ServerShared/Extensions.cs | 4 ++++ Assets/Scripts/ServerShared/ItemInstance.cs | 12 ++++++++---- Assets/Scripts/ServerShared/LoadoutGenerator.cs | 6 ++++-- Assets/Scripts/ServerShared/ZoneGenerator.cs | 5 +++++ 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/Assets/Scripts/ServerShared/Entity.cs b/Assets/Scripts/ServerShared/Entity.cs index 6fdad39bb..281cd9d58 100644 --- a/Assets/Scripts/ServerShared/Entity.cs +++ b/Assets/Scripts/ServerShared/Entity.cs @@ -532,13 +532,11 @@ public abstract class Entity // Check every cell of the item's shape foreach (var i in itemData.Shape.Coordinates) { - // If there is any gear already occupying that space, it won't fit - // If there's a hardpoint there, it won't fit - // Thermal items have their own layer and do not collide with gear + // An interior cell is usable when no gear occupies it. An empty hardpoint cell counts as usable: + // general gear may take it until hardpoint gear claims it, which is the same rule + // UnoccupiedSpace reports, so what generation is offered and what equipping accepts agree. var itemCoord = hullCoord + itemData.Shape.Rotate(i, item.Rotation); - if (!hullData.InteriorCells[itemCoord] || - itemData.HardpointType == HardpointType.Tool && Hardpoints[itemCoord.x, itemCoord.y] != null || - GearOccupancy[itemCoord.x, itemCoord.y] != null) + if (!hullData.InteriorCells[itemCoord] || GearOccupancy[itemCoord.x, itemCoord.y] != null) return false; } } diff --git a/Assets/Scripts/ServerShared/Extensions.cs b/Assets/Scripts/ServerShared/Extensions.cs index 4cbfdcf0a..ccd1676dd 100644 --- a/Assets/Scripts/ServerShared/Extensions.cs +++ b/Assets/Scripts/ServerShared/Extensions.cs @@ -34,6 +34,10 @@ public static class Extensions totalWeight += weights[x]; } + // Nothing can be drawn from a set whose weights all come to nothing. Returning an empty array says so, + // where a full array of unset elements would read as "found these" and hand the caller nulls. + if (totalWeight <= 0) return new T[0]; + var randomElements = new T[count]; for (int i = 0; i < count; i++) { diff --git a/Assets/Scripts/ServerShared/ItemInstance.cs b/Assets/Scripts/ServerShared/ItemInstance.cs index 3de52a20b..f1871f4bf 100644 --- a/Assets/Scripts/ServerShared/ItemInstance.cs +++ b/Assets/Scripts/ServerShared/ItemInstance.cs @@ -37,12 +37,16 @@ public abstract class CraftedItemInstance : ItemInstance // The workmanship of this particular unit, as distinct from the parts that went into it. [JsonProperty("quality"), Key(2)] public float Quality; - // The part filling each of the design's roles, one level deep. Empty on items made before roles existed, - // and on parts themselves; stats then read Quality as they always did. - [JsonProperty("ingredients"), Key(3)] public List Ingredients = new List(); + // Keys 9 and 10, past every key the derived instances use: an item saved before these existed has no + // element there at all. Keys 3 to 6 sit inside the old range and read back as nil, which a non-nullable + // Guid cannot deserialize, so nothing new may go there. + + // How good the part filling each of the design's roles turned out. Empty on items made before roles + // existed; stats then read Quality as they always did. + [JsonProperty("ingredients"), Key(9)] public List Ingredients = new List(); // The manufacturer's branded product this was built as, naming it and carrying its flavor text. - [JsonProperty("product"), Key(4)] public Guid Product; + [JsonProperty("product"), Key(10)] public Guid Product; // The quality a stat reads for one of the design's roles. An unnamed role, a design without roles, and an // item built before roles existed all fall back to this item's own workmanship. diff --git a/Assets/Scripts/ServerShared/LoadoutGenerator.cs b/Assets/Scripts/ServerShared/LoadoutGenerator.cs index 850a2b767..440aac6ab 100644 --- a/Assets/Scripts/ServerShared/LoadoutGenerator.cs +++ b/Assets/Scripts/ServerShared/LoadoutGenerator.cs @@ -146,14 +146,16 @@ public class LoadoutGenerator count); } + // No galaxy means no availability to filter by: every product is on offer. A fixture generates loadouts that + // way, so a test can exercise placement and products without standing up a whole galaxy; no game path does. private bool IsAvailable(FactionProductData product) => - Galaxy.IsPrelude || + Galaxy == null || Galaxy.IsPrelude || Galaxy.ContainsFaction(product.Manufacturer) && (Faction == null || Faction.Allegiance.ContainsKey(product.Manufacturer)); // Prioritize products from the zone faction and its allies, penalizing distance to the manufacturer's headquarters private float ManufacturerPreference(Guid manufacturer) { - if (Faction == null) return 1; + if (Faction == null || Galaxy == null) return 1; var allegiance = manufacturer == Faction.ID ? 1 : Faction.Allegiance.TryGetValue(manufacturer, out var a) ? a : 0; var home = Galaxy.HomeZones.FirstOrDefault(h => h.Key.ID == manufacturer).Value; diff --git a/Assets/Scripts/ServerShared/ZoneGenerator.cs b/Assets/Scripts/ServerShared/ZoneGenerator.cs index b342b9d95..a3408cd4c 100644 --- a/Assets/Scripts/ServerShared/ZoneGenerator.cs +++ b/Assets/Scripts/ServerShared/ZoneGenerator.cs @@ -296,6 +296,11 @@ public static class ZoneGenerator var orbit = selectedStationOrbits[i]; var lagrangeOrbit = CreateLagrangeOrbit(orbit); var station = GetLoadoutGenerator(story.Faction).GenerateStationLoadout(); + if (station == null) + { + itemManager.Log($"Story station for {story.Faction?.Name} could not be generated; its story has nowhere to live!"); + continue; + } station.Orbit = lagrangeOrbit.ID; station.SecurityLevel = story.Security; station.SecurityRadius = pack.Radius;