Aetheria/Assets/Scripts/ServerShared/CargoTransfer.cs
2026-10-22 22:15:41 +02:00

125 lines
4.5 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System.Linq;
using CultMath;
using static CultMath.math;
// Why a give ended. Notification only: nothing reads it to decide anything.
public enum GiveEnd
{
Done,
Stopped,
OutOfRange,
ReceiverGone,
ReceiverFull,
NothingLeft,
GiverDocked,
GiverGone
}
// A pilot's standing offer of goods to a ship alongside. Runtime only: nothing about a give is saved.
public sealed class GiveOrder
{
public readonly Entity Receiver;
// A commodity order is for any stack of this item's design in the giver's bays; a crafted order is for this instance.
public readonly ItemInstance Item;
// Units still to move.
public int Remaining { get; internal set; }
// Units earned and not yet moved, fractional between steps.
internal double Budget;
internal GiveOrder(Entity receiver, ItemInstance item, int quantity)
{
Receiver = receiver;
Item = item;
Remaining = quantity;
}
}
// The one owner of a give's progress and its end (ruling tender-transfer-verb). A ship's own pilot asks for a give through
// PilotCommand.Give and PilotApplier starts it here; nothing else moves goods out of a hold in space (ruling
// transfer-is-give-only). Every unit moves through EquippedCargoBay.TryTransferItem, the one conserving bay-to-bay commit.
public static class CargoTransfer
{
internal static void Begin(Entity giver, GiveOrder order)
{
End(giver, GiveEnd.Stopped);
giver.Giving = order;
}
// Ends the giver's give, if it has one, for the reason.
internal static void End(Entity giver, GiveEnd reason)
{
if (giver.Giving == null) return;
giver.Giving = null;
giver.LastGiveEnd = reason;
}
// One step of every give in the zone: the receiver is still here and alongside, and the rate earns units, each moved
// into the first receiver bay that takes it.
public static void Step(Zone zone, float deltaTime)
{
foreach (var giver in zone.Entities.ToArray())
{
var order = giver.Giving;
if (order == null) continue;
var settings = giver.ItemManager.GameplaySettings;
var range = settings.DockingDistance;
if (!zone.Entities.Contains(order.Receiver))
{
End(giver, GiveEnd.ReceiverGone);
continue;
}
if (lengthsq(order.Receiver.Position.xz - giver.Position.xz) > range * range)
{
End(giver, GiveEnd.OutOfRange);
continue;
}
order.Budget += (double) settings.TransferRate * deltaTime;
while (order.Budget >= 1 && order.Remaining > 0)
{
var source = Source(giver, order);
if (source.Bay == null)
{
End(giver, GiveEnd.NothingLeft);
break;
}
if (!Move(source.Bay, source.Item, order.Receiver))
{
End(giver, GiveEnd.ReceiverFull);
break;
}
order.Budget -= 1;
order.Remaining--;
}
if (order.Remaining == 0) End(giver, GiveEnd.Done);
}
}
// Where the next unit comes from: the first stack of the order's design in the giver's bays, or the crafted instance.
private static (EquippedCargoBay Bay, ItemInstance Item) Source(Entity giver, GiveOrder order)
{
foreach (var bay in giver.CargoBays)
{
if (order.Item is CraftedItemInstance crafted)
{
if (bay.Cargo.ContainsKey(crafted)) return (bay, crafted);
}
else if (bay.ItemsOfType.TryGetValue(order.Item.Data.Key, out var stacks))
{
var stack = stacks.OfType<SimpleCommodity>().FirstOrDefault();
if (stack != null) return (bay, stack);
}
}
return (null, null);
}
// One unit of a commodity, or the crafted item, into the first receiver bay that takes it.
private static bool Move(EquippedCargoBay from, ItemInstance item, Entity receiver) =>
item is SimpleCommodity commodity
? receiver.CargoBays.Any(bay => from.TryTransferItem(bay, commodity, 1))
: receiver.CargoBays.Any(bay => from.TryTransferItem(bay, (CraftedItemInstance) item));
}