using System; using System.Collections.Generic; using System.IO; using System.Linq; using CultMath; using GameCult.Caching; using Xunit; // Goods moving bay to bay between ships and stations: the one conserving commit (EquippedCargoBay.TryTransferItem) that // the docked trade, the sale and the give all go through. Fixture as StationServicesTests: authored settings, Ore. public sealed class CargoTransferTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), "aetheria-cargotransfer-" + Guid.NewGuid().ToString("N")); private readonly CultCache _cache; private readonly ItemManager _items; private readonly Zone _zone; public CargoTransferTests() { Directory.CreateDirectory(_root); _cache = AetheriaStores.Open(Path.Combine(_root, "Aetheria.cc"), catalogWritable: true); _cache.Upsert(new VerseGrammar { Revision = 1 }); var shape = new Shape(5, 5); foreach (var cell in shape.AllCoordinates) shape[cell] = true; _cache.Upsert(new HullData { Name = "Skiff", HullType = HullType.Ship, Shape = shape, Price = 1000, Durability = 10, Armor = 10 }); _cache.Upsert(new HullData { Name = "Hub", HullType = HullType.Station, Shape = shape, Price = 5000, Durability = 10 }); var bay = new Shape(4, 4); foreach (var cell in bay.AllCoordinates) bay[cell] = true; _cache.Upsert(new CargoBayData { Name = "Crate", Shape = new Shape(), InteriorShape = bay, Price = 20, Durability = 4 }); var pocket = new Shape(1, 1); foreach (var cell in pocket.AllCoordinates) pocket[cell] = true; _cache.Upsert(new CargoBayData { Name = "Pocket", Shape = new Shape(), InteriorShape = pocket, Price = 5, Durability = 4 }); _cache.Upsert(new SimpleCommodityData { Name = "Ore", Shape = new Shape(), Price = 7, MaxStack = 100 }); _cache.Upsert(new SimpleCommodityData { Name = "Grit", Shape = new Shape(), Price = 1, MaxStack = 2 }); _cache.Upsert(new SimpleCommodityData { Name = "Slag", Shape = new Shape(), Price = 1, MaxStack = 100 }); var slot = new Shape(1, 2); foreach (var cell in slot.AllCoordinates) slot[cell] = true; _cache.Upsert(new CargoBayData { Name = "Slot", Shape = new Shape(), InteriorShape = slot, Price = 5, Durability = 4 }); var rod = new Shape(2, 1); foreach (var cell in rod.AllCoordinates) rod[cell] = true; _cache.Upsert(new SimpleCommodityData { Name = "Rod", Shape = rod, Price = 1, MaxStack = 1 }); _cache.Upsert(new DockingBayData { Name = "Berth", Shape = new Shape(), InteriorShape = bay, MaxSize = new int2(2, 2), Price = 60, Durability = 6 }); _cache.Upsert(new GearData { Name = "Gun", Hardpoint = HardpointType.Sensors, Shape = new Shape(), Price = 400, Durability = 8 }); _cache.FlushAsync().Wait(); var settings = AuthoredSettings.Load(RestoredHullsTests.FindRepoRoot()).Read("GameplaySettings"); _items = new ItemManager(1u, _cache, new ProvenanceLedger(), settings, _ => { }); _zone = new Zone(_items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); } public void Dispose() { _cache.Dispose(); Directory.Delete(_root, true); } private EquippableItem Mint(string design) { var data = _cache.GetByName(design); return new EquippableItem { Data = _cache.RefOf(data), Durability = data.Durability, Lot = _items.Lots.Add(new Lot { Design = _cache.RefOf(data), Origin = new Attributed(), Quality = .5f, Roles = new List() }) }; } private SimpleCommodity Commodity(string design, int quantity, ItemRotation rotation = ItemRotation.None) => new SimpleCommodity { Data = _cache.RefOf(_cache.GetByName(design)), Quantity = quantity, Rotation = rotation }; // Bays built directly, so a ship can hold several (TryEquip is not under test). private Ship ShipWithBays(params string[] bays) { var ship = new Ship(_items, _zone, Mint("Skiff"), new EntitySettings()); for (var i = 0; i < bays.Length; i++) ship.CargoBays.Add(new EquippedCargoBay(_items, Mint(bays[i]), new int2(1 + i, 1), ship, $"bay{i}")); return ship; } // Off the origin, so a distance taken from the wrong sum or difference of positions shows. private void Place(Ship giver, Ship receiver) { giver.Position = new float3(-50, 0, 10); receiver.Position = giver.Position + new float3(5, 0, 0); _zone.Admit(giver, piloted: false); _zone.Admit(receiver, piloted: false); } private static int Stacks(EquippedCargoBay bay) => bay.Cargo.Keys.Count; private Ship ShipWith(string bay) { var ship = new Ship(_items, _zone, Mint("Skiff"), new EntitySettings()); Assert.True(ship.TryEquip(Mint(bay), new int2(1, 1))); return ship; } private OrbitalEntity Station() { var station = new OrbitalEntity(_items, _zone, Mint("Hub"), default, new EntitySettings()); Assert.True(station.TryEquip(Mint("Crate"), new int2(1, 1))); return station; } // The units of the same design as `like` that the bay holds, over every stack. private static int Units(EquippedCargoBay bay, SimpleCommodity like) => bay.Cargo.Keys.OfType().Where(c => c.Data.Key.Equals(like.Data.Key)).Sum(c => c.Quantity); [Fact] public void PartialCommodityTransferConserves() { var from = ShipWith("Crate").CargoBays[0]; var to = ShipWith("Crate").CargoBays[0]; var ore = Commodity("Ore", 10); Assert.True(from.TryStore(ore)); var massBefore = from.Mass + to.Mass; Assert.True(from.TryTransferItem(to, ore, 4)); Assert.True(from.TryTransferItem(to, ore, 4)); Assert.Equal(2, Units(from, ore)); Assert.Equal(8, Units(to, ore)); Assert.Empty(from.Cargo.Keys.Intersect(to.Cargo.Keys)); Assert.Equal(massBefore, from.Mass + to.Mass, 3); } [Fact] public void ABoughtPartOfAStationStackLeavesTheRestWithTheStation() { // TradeMenu.Buy delivers by this exact commit: the station's stack to the player's bay, a part at a time. var station = Station(); var stationBay = station.CargoBays[0]; var player = ShipWith("Crate").CargoBays[0]; var stock = Commodity("Ore", 10); Assert.True(stationBay.TryStore(stock)); var credits = 1000; Assert.Equal(BuyResult.Bought, StationServices.TryBuy(_items, stock, 4, ref credits, () => stationBay.TryTransferItem(player, stock, 4))); Assert.Equal(BuyResult.Bought, StationServices.TryBuy(_items, stock, 4, ref credits, () => stationBay.TryTransferItem(player, stock, 4))); Assert.Equal(1000 - 2 * 4 * 7, credits); Assert.Equal(2, Units(stationBay, stock)); Assert.Equal(8, Units(player, stock)); Assert.Empty(stationBay.Cargo.Keys.Intersect(player.Cargo.Keys)); } [Fact] public void RefusedCommodityTransferChangesNothing() { var from = ShipWith("Crate").CargoBays[0]; var to = ShipWith("Pocket").CargoBays[0]; var ore = Commodity("Ore", 10); Assert.True(from.TryStore(ore)); Assert.True(to.TryStore(Commodity("Slag", 5))); var at = from.Cargo[ore]; var massBefore = from.Mass + to.Mass; Assert.False(from.TryTransferItem(to, ore, 4)); Assert.Equal(10, ore.Quantity); Assert.Equal(at, from.Cargo[ore]); Assert.Equal(10, Units(from, ore)); Assert.Equal(0, Units(to, ore)); Assert.Equal(massBefore, from.Mass + to.Mass, 3); } [Fact] public void ATransferThatNeedsMoreRoomThanTheStackHasLeftChangesNothing() { // The receiver's only stack of Ore has room for 2 of the 4: the move is whole or nothing. var from = ShipWith("Crate").CargoBays[0]; var to = ShipWith("Pocket").CargoBays[0]; var ore = Commodity("Ore", 10); Assert.True(from.TryStore(ore)); Assert.True(to.TryStore(Commodity("Ore", 98))); Assert.False(from.TryTransferItem(to, ore, 4)); Assert.Equal(10, Units(from, ore)); Assert.Equal(98, Units(to, ore)); } // ---- The give verb ------------------------------------------------------------------------------------------ private const float Dt = .1f; // Two ships in the zone, alongside each other, the giver holding `ore` units of Ore. private (Ship Giver, Ship Receiver, SimpleCommodity Stack) Alongside(int ore, string receiverBay = "Crate") { var giver = ShipWith("Crate"); var receiver = ShipWith(receiverBay); var stack = Commodity("Ore", ore); if (ore > 0) Assert.True(giver.CargoBays[0].TryStore(stack)); // Off the origin, so a distance taken from the wrong sum or difference of positions shows. giver.Position = new float3(-50, 0, 10); receiver.Position = giver.Position + new float3(5, 0, 0); _zone.Admit(giver, piloted: false); _zone.Admit(receiver, piloted: false); return (giver, receiver, stack); } private static int Held(Entity entity, SimpleCommodity like) => entity.CargoBays.Sum(bay => Units(bay, like)); private void Steps(int count) { for (var i = 0; i < count; i++) _zone.Update(Dt); } [Fact] public void GiveMovesAtTheRate() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); for (var i = 1; i <= 10; i++) { Steps(1); Assert.Equal(10, Held(giver, ore) + Held(receiver, ore)); } // Rate 2 for one second: exactly two units, not one and not three. Assert.Equal(2, Held(receiver, ore)); Assert.Equal(8, Held(giver, ore)); Assert.NotNull(giver.Giving); } [Fact] public void GiveEndsDoneAtItsQuantity() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 3); Steps(40); Assert.Equal(3, Held(receiver, ore)); Assert.Equal(7, Held(giver, ore)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void GiveEndsOutOfRange() { var (giver, receiver, ore) = Alongside(10); var range = _items.GameplaySettings.DockingDistance; receiver.Position = giver.Position + new float3(range - 1, 0, 0); giver.Command.Give(receiver, ore, 10); Steps(10); Assert.Equal(2, Held(receiver, ore)); Assert.Null(giver.LastGiveEnd); receiver.Position = giver.Position + new float3(range + 1, 0, 0); Steps(10); Assert.Equal(2, Held(receiver, ore)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.OutOfRange, giver.LastGiveEnd); } [Fact] public void AReceiverExactlyAtTheDockingDistanceIsStillAlongside() { var (giver, receiver, ore) = Alongside(10); receiver.Position = giver.Position + new float3(0, 0, _items.GameplaySettings.DockingDistance); giver.Command.Give(receiver, ore, 10); Steps(10); Assert.Equal(2, Held(receiver, ore)); Assert.Null(giver.LastGiveEnd); } [Fact] public void AStepOfExactlyOneUnitMovesOneUnit() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); _zone.Update(1f / _items.GameplaySettings.TransferRate); Assert.Equal(1, Held(receiver, ore)); } [Fact] public void ALongStepNeverMovesMoreThanTheQuantity() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 3); _zone.Update(5f); Assert.Equal(3, Held(receiver, ore)); Assert.Equal(7, Held(giver, ore)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void AGiveOfOneMovesOne() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 1); Steps(20); Assert.Equal(1, Held(receiver, ore)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void AGiveOfACraftedItemTheGiverDoesNotHoldEndsNothingLeft() { var (giver, receiver, _) = Alongside(0); var gun = Mint("Gun"); Assert.True(receiver.CargoBays[0].TryStore(gun)); giver.Command.Give(receiver, gun, 1); Steps(20); Assert.Equal(GiveEnd.NothingLeft, giver.LastGiveEnd); Assert.True(receiver.CargoBays[0].Cargo.ContainsKey(gun)); } [Fact] public void ALongStepAfterTheReceiverLeavesMovesNothing() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); Steps(1); _zone.Remove(receiver); _zone.Update(5f); Assert.Equal(0, Held(receiver, ore)); Assert.Equal(10, Held(giver, ore)); Assert.Equal(GiveEnd.ReceiverGone, giver.LastGiveEnd); } [Fact] public void ALongStepOutOfRangeMovesNothing() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); Steps(1); receiver.Position = giver.Position + new float3(0, 0, _items.GameplaySettings.DockingDistance + 1); _zone.Update(5f); Assert.Equal(0, Held(receiver, ore)); Assert.Equal(GiveEnd.OutOfRange, giver.LastGiveEnd); } [Fact] public void GiveRangeIsMeasuredOnTheFlatPlane() { // The plane is xz: a ship far apart in y only is still alongside. var (giver, receiver, ore) = Alongside(10); receiver.Position = giver.Position + new float3(5, 1000, 0); giver.Command.Give(receiver, ore, 10); Steps(10); Assert.Equal(2, Held(receiver, ore)); } [Fact] public void GiveEndsWhenTheReceiverLeaves() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); Steps(10); Assert.Equal(2, Held(receiver, ore)); _zone.Remove(receiver); Steps(10); Assert.Equal(8, Held(giver, ore)); Assert.Equal(2, Held(receiver, ore)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.ReceiverGone, giver.LastGiveEnd); } [Fact] public void DockingEndsTheGiversGive() { var (giver, receiver, ore) = Alongside(10); var station = new OrbitalEntity(_items, _zone, Mint("Hub"), default, new EntitySettings()); Assert.True(station.TryEquip(Mint("Berth"), new int2(1, 1))); giver.Command.Give(receiver, ore, 10); Steps(10); Assert.NotNull(station.TryDock(giver)); Steps(10); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.GiverDocked, giver.LastGiveEnd); Assert.Equal(2, Held(receiver, ore)); } [Fact] public void GiveEndsOnAFullHold() { var (giver, receiver, ore) = Alongside(10, "Pocket"); Assert.True(receiver.CargoBays[0].TryStore(Commodity("Slag", 5))); giver.Command.Give(receiver, ore, 10); Steps(20); Assert.Equal(10, Held(giver, ore)); Assert.Equal(0, Held(receiver, ore)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.ReceiverFull, giver.LastGiveEnd); } [Fact] public void GiveEndsWhenNothingIsLeft() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 50); Steps(100); Assert.Equal(10, Held(receiver, ore)); Assert.Equal(0, Held(giver, ore)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.NothingLeft, giver.LastGiveEnd); } [Fact] public void AGiveDrawsOnEveryStackOfTheDesign() { // Grit stacks two to a stack, so three units are two stacks: the order is for the design, not the stack named. var (giver, receiver, _) = Alongside(0); var grit = Commodity("Grit", 2); Assert.True(giver.CargoBays[0].TryStore(grit)); Assert.True(giver.CargoBays[0].TryStore(Commodity("Grit", 1))); Assert.Equal(2, giver.CargoBays[0].ItemsOfType[grit.Data.Key].Count); giver.Command.Give(receiver, grit, 3); Steps(40); Assert.Equal(3, Held(receiver, grit)); Assert.Equal(0, Held(giver, grit)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void AGivenCraftedItemIsThatInstance() { var (giver, receiver, _) = Alongside(0); var first = Mint("Gun"); var second = Mint("Gun"); Assert.True(giver.CargoBays[0].TryStore(first)); Assert.True(giver.CargoBays[0].TryStore(second)); giver.Command.Give(receiver, second, 5); Steps(20); Assert.True(receiver.CargoBays[0].Cargo.ContainsKey(second)); Assert.False(receiver.CargoBays[0].Cargo.ContainsKey(first)); Assert.True(giver.CargoBays[0].Cargo.ContainsKey(first)); Assert.False(giver.CargoBays[0].Cargo.ContainsKey(second)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void TheLastGivePressInAStepWins() { var (giver, receiver, ore) = Alongside(10); var third = ShipWith("Crate"); third.Position = giver.Position + new float3(0, 0, 5); _zone.Admit(third, piloted: false); // A give then a stop leaves nothing, and nothing ended. giver.Command.Give(receiver, ore, 10); giver.Command.StopGiving(); Steps(1); Assert.Null(giver.Giving); Assert.Null(giver.LastGiveEnd); // A stop then a give leaves the give. giver.Command.StopGiving(); giver.Command.Give(receiver, ore, 10); Steps(1); Assert.Same(receiver, giver.Giving.Receiver); // A second give in the same step replaces the first before it starts. giver.Command.Give(receiver, ore, 10); giver.Command.Give(third, ore, 10); Steps(1); Assert.Same(third, giver.Giving.Receiver); Assert.Equal(GiveEnd.Stopped, giver.LastGiveEnd); // A give in a later step replaces the one under way, which ends Stopped. giver.Command.Give(receiver, ore, 10); Steps(1); Assert.Same(receiver, giver.Giving.Receiver); } [Fact] public void StoppingEndsTheGiveAndKeepsWhatMoved() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); Steps(10); giver.Command.StopGiving(); Steps(10); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.Stopped, giver.LastGiveEnd); Assert.Equal(2, Held(receiver, ore)); Assert.Equal(8, Held(giver, ore)); } [Fact] public void AGiveToOneselfOrOfNothingIsIgnored() { var (giver, receiver, ore) = Alongside(10); giver.Command.Give(receiver, ore, 10); Steps(1); var order = giver.Giving; giver.Command.Give(giver, ore, 5); giver.Command.Give(receiver, ore, 0); giver.Command.Give(receiver, ore, -3); giver.Command.Give(null, ore, 5); giver.Command.Give(receiver, null, 5); Steps(1); Assert.Same(order, giver.Giving); Assert.Null(giver.LastGiveEnd); } [Fact] public void AGiverStaysInTheZone() { var (giver, receiver, ore) = Alongside(10); giver.Command.Designate(new TargetRef(receiver)); receiver.Command.Designate(new TargetRef(giver)); giver.Command.Give(receiver, ore, 10); Steps(20); Assert.True(_zone.Entities.Contains(giver)); Assert.True(_zone.Entities.Contains(receiver)); Assert.Same(receiver, giver.Target.Value.Entity); Assert.Same(giver, receiver.Target.Value.Entity); } [Fact] public void NoOrderMovesNothing() { // No order, no work: a zone of ships that are not giving moves nothing. var (giver, receiver, ore) = Alongside(10); Steps(20); Assert.Equal(10, Held(giver, ore)); Assert.Equal(0, Held(receiver, ore)); Assert.Null(giver.LastGiveEnd); } // ---- Quantities below one -------------------------------------------------------------------------------- [Theory] [InlineData(0)] [InlineData(-5)] public void ATransferBelowOneUnitChangesNothing(int quantity) { var from = ShipWith("Crate").CargoBays[0]; var to = ShipWith("Crate").CargoBays[0]; var ore = Commodity("Ore", 10); Assert.True(from.TryStore(ore)); Assert.True(to.TryStore(Commodity("Ore", 3))); var mass = (from.Mass, to.Mass); var stacks = (Stacks(from), Stacks(to)); Assert.False(from.TryTransferItem(to, ore, quantity)); Assert.Equal(10, Units(from, ore)); Assert.Equal(3, Units(to, ore)); Assert.Equal(mass, (from.Mass, to.Mass)); Assert.Equal(stacks, (Stacks(from), Stacks(to))); } [Theory] [InlineData(0)] [InlineData(-5)] public void ARemovalBelowOneUnitTakesNothing(int quantity) { var bay = ShipWith("Crate").CargoBays[0]; var ore = Commodity("Ore", 10); Assert.True(bay.TryStore(ore)); var mass = bay.Mass; Assert.Null(bay.Remove(ore, quantity)); Assert.Equal(10, Units(bay, ore)); Assert.Equal(mass, bay.Mass); Assert.Equal(1, Stacks(bay)); } // ---- The give: where units go, and when it ends (the soul-tv probes, adopted) ---------------------------- // First receiver bay full, second has room: the unit goes to the second. [Fact] public void AGivenCommodityGoesToTheSecondBayWhenTheFirstIsFull() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Pocket", "Crate"); var ore = Commodity("Ore", 10); Assert.True(giver.CargoBays[0].TryStore(ore)); Assert.True(receiver.CargoBays[0].TryStore(Commodity("Slag", 5))); Place(giver, receiver); giver.Command.Give(receiver, ore, 3); Steps(40); Assert.Equal(3, Units(receiver.CargoBays[1], ore)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } // Two open receiver bays: one unit per move, not one per bay. [Fact] public void AGiveMovesOneUnitPerMoveWithTwoOpenBays() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Crate", "Crate"); var ore = Commodity("Ore", 10); Assert.True(giver.CargoBays[0].TryStore(ore)); Place(giver, receiver); giver.Command.Give(receiver, ore, 10); _zone.Update(1f); Assert.Equal(2, Held(receiver, ore)); Assert.Equal(8, Held(giver, ore)); } [Fact] public void AGivenCraftedItemGoesToTheSecondBayWhenTheFirstIsFull() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Pocket", "Crate"); var gun = Mint("Gun"); Assert.True(giver.CargoBays[0].TryStore(gun)); Assert.True(receiver.CargoBays[0].TryStore(Commodity("Slag", 5))); Place(giver, receiver); giver.Command.Give(receiver, gun, 1); Steps(10); Assert.True(receiver.CargoBays[1].Cargo.ContainsKey(gun)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } [Fact] public void AGivenCraftedItemMovesOnceIntoTwoOpenBays() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Crate", "Crate"); var gun = Mint("Gun"); Assert.True(giver.CargoBays[0].TryStore(gun)); Place(giver, receiver); giver.Command.Give(receiver, gun, 1); Steps(10); Assert.True(receiver.CargoBays[0].Cargo.ContainsKey(gun)); Assert.Equal(GiveEnd.Done, giver.LastGiveEnd); } // A refused one-unit move of a whole one-unit stack re-files it at the end of ItemsOfType, so the next Source would be // a different stack. Rods are 2x1; the receiver's Slot (1x2) takes only the turned one. After the give has ended // ReceiverFull nothing may move. [Fact] public void NothingMovesAfterAGiveEndsReceiverFull() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Slot"); var flat = Commodity("Rod", 1); Assert.True(giver.CargoBays[0].TryStore(flat)); Assert.True(giver.CargoBays[0].TryStore(Commodity("Rod", 1, ItemRotation.Clockwise))); Place(giver, receiver); giver.Command.Give(receiver, flat, 2); _zone.Update(5f); Assert.Equal(GiveEnd.ReceiverFull, giver.LastGiveEnd); Assert.Equal(0, Held(receiver, flat)); Assert.Equal(2, Held(giver, flat)); } // Refused moves do not spend the order's units. [Fact] public void AnEndedOrderKeepsItsRemainingUnits() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Pocket"); var ore = Commodity("Ore", 10); Assert.True(giver.CargoBays[0].TryStore(ore)); Assert.True(receiver.CargoBays[0].TryStore(Commodity("Slag", 5))); Place(giver, receiver); giver.Command.Give(receiver, ore, 5); _zone.Update(.01f); // applies the command, earns no unit var order = giver.Giving; Assert.NotNull(order); _zone.Update(5f); Assert.Equal(GiveEnd.ReceiverFull, giver.LastGiveEnd); Assert.Equal(5, order.Remaining); } // A give of more than the hold has moves the real units and then has nothing left. [Fact] public void AGiveOfMoreThanTheHoldHasEndsNothingLeft() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Crate"); var grit = Commodity("Grit", 2); Assert.True(giver.CargoBays[0].TryStore(grit)); Place(giver, receiver); giver.Command.Give(receiver, grit, 3); _zone.Update(5f); Assert.Equal(2, Held(receiver, grit)); Assert.Equal(GiveEnd.NothingLeft, giver.LastGiveEnd); } // Warping and dying both leave through Zone.Remove; docking has already ended the give GiverDocked. [Fact] public void AGiveEndsWhenItsGiverLeavesTheZone() { var giver = ShipWithBays("Crate"); var receiver = ShipWithBays("Crate"); var ore = Commodity("Ore", 10); Assert.True(giver.CargoBays[0].TryStore(ore)); Place(giver, receiver); giver.Command.Give(receiver, ore, 10); _zone.Update(.5f); Assert.True(_zone.Remove(giver)); Assert.Null(giver.Giving); Assert.Equal(GiveEnd.GiverGone, giver.LastGiveEnd); } // ---- The lot a purchase delivers --------------------------------------------------------------------------- [Fact] public void APurchaseOfAStackIsOneLotChargedForWhatItDelivers() { // Grit stacks hold two: a station stack of two is one lot. var stock = Commodity("Grit", 2); var credits = 100; var delivered = new List(); var result = StationServices.TryBuyLots(_items, stock, 2, ref credits, lot => { delivered.Add(lot); return true; }); Assert.Equal(BuyResult.Bought, result); Assert.Equal(new[] { 2 }, delivered); Assert.Equal(100 - 2, credits); } [Fact] public void AStackAboveMaxStackFromOutsideABayIsRefusedBeforeAnythingMoves() { var stock = Commodity("Grit", 5); var credits = 100; var delivered = 0; var result = StationServices.TryBuyLots(_items, stock, 2, ref credits, lot => { delivered++; return true; }); Assert.Equal(BuyResult.NoRoom, result); Assert.Equal(0, delivered); Assert.Equal(100, credits); } [Theory] [InlineData(-5, 1000, 1)] [InlineData(0, 1000, 1)] [InlineData(4, 1000, 4)] [InlineData(50, 1000, 10)] [InlineData(50, 21, 3)] [InlineData(5, 3, 1)] public void TheBuyQuantityIsClampedToWhatIsBuyable(int typed, int credits, int expected) { var stock = Commodity("Ore", 10); // 7 credits each Assert.Equal(expected, StationServices.BuyableQuantity(_items, stock, typed, credits)); } // ---- The bay owns the quantity that may sit in it --------------------------------------------------------- [Theory] [InlineData("Ore", 0)] [InlineData("Ore", -3)] [InlineData("Ore", int.MinValue)] [InlineData("Grit", 3)] // MaxStack 2 [InlineData("Ore", 101)] public void TheBayRefusesAStackBelowOneOrAboveMaxStack(string design, int quantity) { var bay = ShipWith("Crate").CargoBays[0]; var massBefore = bay.Mass; Assert.False(bay.TryStore(Commodity(design, quantity))); Assert.False(bay.TryStore(Commodity(design, quantity), new int2(0, 0))); Assert.Empty(bay.Cargo); Assert.Equal(massBefore, bay.Mass); } [Fact] public void ANegativeStackNeverMergesIntoAHeldOne() { var bay = ShipWith("Crate").CargoBays[0]; var held = Commodity("Ore", 5); Assert.True(bay.TryStore(held)); var at = bay.Cargo[held]; Assert.False(bay.TryStore(Commodity("Ore", -3), at)); Assert.Equal(5, held.Quantity); } [Fact] public void AMergeThatOverflowsFillsTheHeldStackToMaxAndLeavesTheRestWithTheCaller() { var bay = ShipWith("Crate").CargoBays[0]; var held = Commodity("Grit", 1); Assert.True(bay.TryStore(held)); var incoming = Commodity("Grit", 2); Assert.False(bay.TryStore(incoming, bay.Cargo[held])); Assert.Equal(2, held.Quantity); Assert.Equal(1, incoming.Quantity); } [Fact] public void APurchaseAboveTheStationStockIsRefusedWithoutCharge() { // Real bays, Grit stacks hold two: a stock of 3 cannot be bought as 5. Before, the second lot took the 1-unit // remainder and was charged for 2. var station = Station().CargoBays[0]; var player = ShipWith("Crate").CargoBays[0]; var stock = Commodity("Grit", 2); Assert.True(station.TryStore(stock)); var second = Commodity("Grit", 1); Assert.True(station.TryStore(second, new int2(1, 0))); var credits = 100; var result = StationServices.TryBuyLots(_items, stock, 5, ref credits, lot => station.TryTransferItem(player, stock, lot)); Assert.Equal(BuyResult.NoRoom, result); Assert.Equal(100, credits); Assert.Equal(0, Units(player, stock)); } [Theory] [InlineData(-5)] [InlineData(0)] public void APurchaseOfNoUnitsIsRefusedNotBought(int quantity) { var stock = Commodity("Ore", 10); var credits = 100; var result = StationServices.TryBuyLots(_items, stock, quantity, ref credits, lot => true); Assert.Equal(BuyResult.NoRoom, result); Assert.Equal(100, credits); } [Fact] public void APurchaseOfAnEmptyStationStackChargesNothingAndDeliversNothing() { // An old save's zero stack, as the station's stock. The field clamps the typed quantity up to one unit. var stock = Commodity("Ore", 0); var credits = 100; var delivered = 0; var result = StationServices.TryBuyLots(_items, stock, StationServices.BuyableQuantity(_items, stock, 1, credits), ref credits, lot => { delivered += lot; return true; }); Assert.Equal(BuyResult.NoRoom, result); Assert.Equal(100, credits); Assert.Equal(0, delivered); } [Fact] public void ThePurchaseOfTheWholeStockChargesTheUnitsTheBayMoved() { var station = Station().CargoBays[0]; var player = ShipWith("Crate").CargoBays[0]; var stock = Commodity("Grit", 2); Assert.True(station.TryStore(stock)); var more = Commodity("Grit", 1); Assert.True(station.TryStore(more, new int2(1, 0))); var credits = 100; var result = StationServices.TryBuyLots(_items, stock, 2, ref credits, lot => station.TryTransferItem(player, stock, lot)); Assert.Equal(BuyResult.Bought, result); Assert.Equal(98, credits); Assert.Equal(2, Units(player, stock)); } [Fact] public void ARestoredBayDropsAStackBelowOneAndLogsWhy() { var log = new List(); var settings = AuthoredSettings.Load(RestoredHullsTests.FindRepoRoot()).Read("GameplaySettings"); var items = new ItemManager(1u, _cache, _items.Lots, settings, log.Add); var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); var ship = ShipWith("Crate"); Assert.True(ship.CargoBays[0].TryStore(Commodity("Slag", 4))); var pack = (ShipPack) EntitySerializer.Pack(ship); // The typed -5 purchase of old saves: a negative stack in the hold, beside a good one. pack.CargoContents[0] = pack.CargoContents[0].Concat(new[] { (new int2(3, 3), (ItemInstance) Commodity("Ore", -5)) }).ToArray(); var restored = (Ship) EntitySerializer.Unpack(items, zone, pack); var bay = restored.CargoBays[0]; Assert.Equal(new[] { 4 }, bay.Cargo.Keys.OfType().Select(c => c.Quantity).ToArray()); Assert.Contains(log, l => l.Contains("-5") && l.Contains("Ore")); } // ---- Saves from before the bay capped a stack ------------------------------------------------------------- // A pre-f0d7c12c purchase above MaxStack left a whole oversize stack in the hold. The save holds it as one stack. private (Ship restored, List log) RestoreWithOversize(string[] bays, int oversize, int filler) { var log = new List(); var settings = AuthoredSettings.Load(RestoredHullsTests.FindRepoRoot()).Read("GameplaySettings"); var items = new ItemManager(1u, _cache, _items.Lots, settings, log.Add); var zone = new Zone(items, new PlanetSettings(), new ZonePack(), new GalaxyZone { Name = "Test Zone", Owner = null }, null); var ship = new Ship(_items, _zone, Mint("Skiff"), new EntitySettings()); for (var i = 0; i < bays.Length; i++) Assert.True(ship.TryEquip(Mint(bays[i]), new int2(1 + 2 * i, 1))); var pack = (ShipPack) EntitySerializer.Pack(ship); // Bay 0 holds the oversize stack at (3,3) and `filler` full Slag stacks in the other cells; later bays are empty. for (var b = 0; b < bays.Length; b++) { var contents = new List<(int2, ItemInstance)>(); if (b == 0) contents.Add((new int2(3, 3), Commodity("Ore", oversize))); for (var n = 0; n < (b == 0 ? filler : 0); n++) contents.Add((new int2(n % 4, n / 4), Commodity("Slag", 100))); pack.CargoContents[b] = contents.ToArray(); } return ((Ship) EntitySerializer.Unpack(items, zone, pack), log); } [Fact] public void ARestoredOversizeStackSplitsIntoMaxStackStacksInTheSameBay() { var (restored, log) = RestoreWithOversize(new[] { "Crate" }, 250, 0); var ore = restored.CargoBays[0].Cargo.Keys.OfType().Select(c => c.Quantity).OrderByDescending(q => q).ToArray(); Assert.Equal(new[] { 100, 100, 50 }, ore); Assert.DoesNotContain(log, l => l.Contains("Dropped")); } [Fact] public void ARestoredOversizeStackSpillsToAnotherBayWhenItsOwnIsFull() { // Bay 0 has 15 cells of filler and the oversize stack's own cell: only 100 fits there. var (restored, log) = RestoreWithOversize(new[] { "Crate", "Crate" }, 250, 15); Assert.Equal(new[] { 100, 100, 50 }, restored.CargoBays.SelectMany(b => b.Cargo.Keys.OfType()) .Where(c => _cache.GetByName("Ore").Name == _items.GetData(c).Name).Select(c => c.Quantity).OrderByDescending(q => q).ToArray()); Assert.Equal(1, restored.CargoBays[0].Cargo.Keys.OfType().Count(c => _items.GetData(c).Name == "Ore")); Assert.DoesNotContain(log, l => l.Contains("Dropped")); } [Fact] public void ARestoredOversizeStackDropsOnlyWhatFitsNowhereAndLogsIt() { var (restored, log) = RestoreWithOversize(new[] { "Crate" }, 250, 15); Assert.Equal(new[] { 100 }, restored.CargoBays[0].Cargo.Keys.OfType() .Where(c => _items.GetData(c).Name == "Ore").Select(c => c.Quantity).ToArray()); Assert.Contains(log, l => l.Contains("Ore") && l.Contains("150")); } }