From df67c875e349a85a274018ca80950d5636eacf2e Mon Sep 17 00:00:00 2001 From: Nibu Date: Fri, 13 Nov 2026 03:03:25 +0000 Subject: [PATCH] FireControl.Extract stamps every mined stack with the Extracted lot of its zone, belt and ore Extracted gains Body at key 2. ItemManager.ExtractedLot finds or mints the lot; its (zone, belt, ore) index is derived from the ledger at construction and never saved. SavedGame roots skip null items and Lot 0. Co-Authored-By: Claude Sonnet 5.5 --- Assets/Scripts/ServerShared/FireControl.cs | 7 ++++-- Assets/Scripts/ServerShared/ItemManager.cs | 26 ++++++++++++++++++++++ Assets/Scripts/ServerShared/Provenance.cs | 2 ++ Assets/Scripts/ServerShared/SavedGame.cs | 2 +- 4 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Assets/Scripts/ServerShared/FireControl.cs b/Assets/Scripts/ServerShared/FireControl.cs index 3df60cf50..bb6d48d6c 100644 --- a/Assets/Scripts/ServerShared/FireControl.cs +++ b/Assets/Scripts/ServerShared/FireControl.cs @@ -1864,14 +1864,17 @@ public static class FireControl var at = zone.ChunkPose(chunk.Field, chunk.Index).xy; var velocity = zone.ChunkVelocity(chunk); + var zoneIndex = zone.Galaxy?.Zones == null ? -1 : Array.IndexOf(zone.Galaxy.Zones, zone.GalaxyZone); var present = zone.Entities.Contains(shooter); for (var i = 0; i < ores.Length; i++) { var key = ores[i].Commodity.Key; - var maxStack = Math.Max(1, ((SimpleCommodityData) catalog.Get(ores[i].Commodity)).MaxStack); + var commodity = (SimpleCommodityData) catalog.Get(ores[i].Commodity); + var maxStack = Math.Max(1, commodity.MaxStack); + var lot = counts[i] > 0 ? shooter.ItemManager.ExtractedLot(zoneIndex, chunk.Field, commodity) : 0; for (var left = counts[i]; left > 0; left -= maxStack) { - var stack = new SimpleCommodity { Data = new CultRecordRef(key), Quantity = Math.Min(left, maxStack) }; + var stack = new SimpleCommodity { Data = new CultRecordRef(key), Lot = lot, Quantity = Math.Min(left, maxStack) }; // A partial store leaves the remainder in Quantity and returns false, so the next bay is offered the rest. var stored = false; if (present) foreach (var bay in shooter.CargoBays) if (stored = bay.TryStore(stack)) break; diff --git a/Assets/Scripts/ServerShared/ItemManager.cs b/Assets/Scripts/ServerShared/ItemManager.cs index 774abbe83..c91568efd 100644 --- a/Assets/Scripts/ServerShared/ItemManager.cs +++ b/Assets/Scripts/ServerShared/ItemManager.cs @@ -25,6 +25,11 @@ public class ItemManager public CultCache ItemData { get; } public ProvenanceLedger Lots { get; } + + // (zone index, belt, commodity) -> the Extracted lot minted for it. Derived from Lots at construction and on mint, + // never persisted: a reload rebuilds it from the ledger, so the same source finds the same lot. + private readonly Dictionary<(int, CultRecordKey, CultRecordKey), int> _extracted = + new Dictionary<(int, CultRecordKey, CultRecordKey), int>(); public GameplaySettings GameplaySettings { get; } public ItemManager(uint seed, CultCache itemData, ProvenanceLedger lots, GameplaySettings settings, Action logger) @@ -33,6 +38,9 @@ public class ItemManager Random = new Random(seed); ItemData = itemData; Lots = lots; + foreach (var (id, lot) in lots.Lots) + if (lot.Origin is Extracted extracted) + _extracted[(extracted.Zone, extracted.Body.Key, extracted.Commodity.Key)] = id; GameplaySettings = settings; _logger = logger; } @@ -181,6 +189,24 @@ public class ItemManager return Lots.Add(lot); } + // Mining Cut 5b: the lot of every unit mined from one commodity in one belt of one zone, found or minted. Unbranded + // (Product unset) and unworked (Quality 1, no roles). FireControl.Extract is the only caller. + public int ExtractedLot(int zone, CultRecordKey belt, SimpleCommodityData commodity) + { + var key = (zone, belt, ItemData.RefOf(commodity).Key); + if (_extracted.TryGetValue(key, out var found)) return found; + var id = Lots.Add(new Lot + { + Design = ItemData.RefOf(commodity), + Origin = new Extracted + { + Zone = zone, Body = new CultRecordRef(belt), Commodity = ItemData.RefOf(commodity) + }, + Quality = 1 + }); + return _extracted[key] = id; + } + // The one writer of a crafted instance's Data: copied from the lot's design, never independently set. public CraftedItemInstance CreateInstance(int lot) { diff --git a/Assets/Scripts/ServerShared/Provenance.cs b/Assets/Scripts/ServerShared/Provenance.cs index 25a4a6175..71c260496 100644 --- a/Assets/Scripts/ServerShared/Provenance.cs +++ b/Assets/Scripts/ServerShared/Provenance.cs @@ -137,6 +137,8 @@ public class Produced : Provenance [MessagePackObject] public class Extracted : Provenance { + // Zone is the zone's index in galaxy.Zones (SavedGame's convention), -1 with no galaxy; Body is the belt. [Key(0)] public int Zone; [Key(1)] public CultRecordRef Commodity; + [Key(2)] public CultRecordRef Body; } diff --git a/Assets/Scripts/ServerShared/SavedGame.cs b/Assets/Scripts/ServerShared/SavedGame.cs index 21f3f3b71..7c7952494 100644 --- a/Assets/Scripts/ServerShared/SavedGame.cs +++ b/Assets/Scripts/ServerShared/SavedGame.cs @@ -225,7 +225,7 @@ public static class RunSave var roots = zones .SelectMany(zone => zone.Contents?.Entities ?? new List()) .SelectMany(EntitySerializer.Items) - .Where(item => item.Lot != 0) + .Where(item => item != null && item.Lot != 0) .Select(item => item.Lot); cache.Commit(batch => {