using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Xml.Linq;
using Barotrauma.Extensions;
using Barotrauma.Items.Components;
using Microsoft.Xna.Framework;
namespace Barotrauma
{
internal readonly struct InventorySlotItem
{
public readonly int Slot;
public readonly Item Item;
public InventorySlotItem(int slot, Item item)
{
Slot = slot;
Item = item;
}
public void Deconstruct(out int slot, out Item item)
{
slot = Slot;
item = Item;
}
}
internal abstract partial class Command
{
public abstract LocalizedString GetDescription();
}
///
/// A command for setting and reverting a MapEntity rectangle
///
///
///
internal class TransformCommand : Command
{
private readonly List Receivers;
private readonly List NewData;
private readonly List OldData;
private readonly bool Resized;
///
/// A command for setting and reverting a MapEntity rectangle
///
/// Entities whose rectangle has been altered
/// The new rectangle that is or will be applied to the map entity
/// Old rectangle the map entity had before
/// If the transform was resized or not
///
/// All lists should be equal in length, for every receiver there should be a corresponding entry at the same position in newData and oldData.
///
public TransformCommand(List receivers, List newData, List oldData, bool resized)
{
Receivers = receivers;
NewData = newData;
OldData = oldData;
Resized = resized;
}
public override void Execute() => SetRects(NewData);
public override void UnExecute() => SetRects(OldData);
public override void Cleanup()
{
NewData.Clear();
OldData.Clear();
Receivers.Clear();
}
private void SetRects(IReadOnlyList rects)
{
if (Receivers.Count != rects.Count)
{
DebugConsole.ThrowError($"Receivers.Count did not match Rects.Count ({Receivers.Count} vs {rects.Count}).");
return;
}
for (int i = 0; i < rects.Count; i++)
{
MapEntity entity = Receivers[i].GetReplacementOrThis();
Rectangle Rect = rects[i];
Vector2 diff = Rect.Location.ToVector2() - entity.Rect.Location.ToVector2();
entity.Move(diff);
entity.Rect = Rect;
}
}
public override LocalizedString GetDescription()
{
if (Resized)
{
return TextManager.GetWithVariable("Undo.ResizedItem", "[item]", Receivers.FirstOrDefault()?.Name);
}
return Receivers.Count > 1
? TextManager.GetWithVariable("Undo.MovedItemsMultiple", "[count]", Receivers.Count.ToString())
: TextManager.GetWithVariable("Undo.MovedItem", "[item]", Receivers.FirstOrDefault()?.Name);
}
}
///
/// A command that removes and unremoves map entities
///
///
///
///
internal class AddOrDeleteCommand : Command
{
private readonly Dictionary PreviousInventories = new Dictionary();
public readonly List Receivers;
private readonly List CloneList;
private readonly bool WasDeleted;
private readonly List ContainedItemsCommand = new List();
// We need to 'snapshot' the state of the circuit box and the best way to do that is to save it to XML.
private readonly List CircuitBoxData = new List();
///
/// Creates a command where all entities share the same state.
///
/// Entities that were deleted or added
/// Whether or not all entities are or are going to be deleted
/// Ignore item inventories when set to false, workaround for pasting
public AddOrDeleteCommand(List receivers, bool wasDeleted, bool handleInventoryBehavior = true)
{
Debug.Assert(receivers.Count > 0, "Command has 0 receivers");
WasDeleted = wasDeleted;
Receivers = new List(receivers);
try
{
foreach (MapEntity receiver in receivers)
{
if (receiver is Item { ParentInventory: not null } it)
{
PreviousInventories.Add(new InventorySlotItem(it.ParentInventory.FindIndex(it), it), it.ParentInventory);
}
}
List clonedTargets = MapEntity.Clone(receivers);
List itemsToDelete = new List();
foreach (MapEntity receiver in Receivers)
{
if (receiver is not Item it) { continue; }
foreach (var cb in it.GetComponents())
{
CircuitBoxData.Add(cb.Save(new XElement("root")));
}
foreach (ItemContainer component in it.GetComponents())
{
if (component.Inventory == null) { continue; }
itemsToDelete.AddRange(component.Inventory.AllItems.Where(static item => !item.Removed));
}
}
if (itemsToDelete.Any() && handleInventoryBehavior)
{
ContainedItemsCommand.Add(new AddOrDeleteCommand(itemsToDelete, wasDeleted));
if (wasDeleted)
{
foreach (MapEntity item in itemsToDelete)
{
if (item != null && !item.Removed)
{
item.Remove();
}
}
}
}
foreach (MapEntity clone in clonedTargets)
{
clone.ShallowRemove();
if (clone is Item it)
{
foreach (ItemContainer container in it.GetComponents())
{
container.Inventory?.DeleteAllItems();
}
}
}
CloneList = clonedTargets;
}
// This should never happen except if we decide to make a new type of MapEntity that isn't finished yet
catch (Exception e)
{
Receivers = new List();
CloneList = new List();
DebugConsole.ThrowError("Could not store object", e);
}
}
public override void Execute()
=> Process(true);
public override void UnExecute()
=> Process(false);
private void Process(bool redo)
{
var items = DeleteUndelete(redo);
foreach (var cmd in ContainedItemsCommand)
{
cmd.Process(redo);
}
ApplyCircuitBoxDataIfAny(items);
}
///
/// We need to manually copy over the circuit box data because of how the undo handles inventory items.
/// The undo system recursively deletes inventory items and creates a separate command for each one.
/// This causes the circuit box to lose its internal inventory when it's cloned and then restored and make it
/// unable to load the state from XML.
///
/// The workaround to this is to ignore the XML that is being loaded when the item is created and instead
/// save the XML into the command and then load it back after the undo system has restored the items which
/// is what this function does.
///
private void ApplyCircuitBoxDataIfAny(ImmutableArray items)
{
int cbIndex = 0;
foreach (var newItem in items)
{
foreach (ItemComponent component in newItem.Components)
{
if (component is not CircuitBox cb) { continue; }
if (cbIndex < 0 || cbIndex >= CircuitBoxData.Count)
{
DebugConsole.ThrowError("Unable to restore wiring in circuit box, index out of range.");
continue;
}
var cbData = CircuitBoxData[cbIndex];
cbIndex++;
cb.LoadFromXML(new ContentXElement(null, cbData));
}
}
}
public override void Cleanup()
{
foreach (MapEntity entity in CloneList)
{
if (!entity.Removed)
{
entity.Remove();
}
}
CloneList?.Clear();
Receivers.Clear();
PreviousInventories?.Clear();
ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup());
CircuitBoxData.Clear();
}
private ImmutableArray DeleteUndelete(bool redo)
{
bool wasDeleted = WasDeleted;
// We are redoing instead of undoing, flip the behavior
if (redo) { wasDeleted = !wasDeleted; }
// collect newly created items so we can update their circuit boxes if any
var builder = ImmutableArray.CreateBuilder();
if (wasDeleted)
{
Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted");
List clones = MapEntity.Clone(CloneList);
int length = Math.Min(Receivers.Count, clones.Count);
for (int i = 0; i < length; i++)
{
MapEntity clone = clones[i],
receiver = Receivers[i];
if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem)
{
builder.Add(cloneItem);
foreach (ItemComponent ic in item.Components)
{
int index = item.GetComponentIndex(ic);
ItemComponent component = cloneItem.Components.ElementAtOrDefault(index);
switch (component)
{
case null:
continue;
case ItemContainer { Inventory: not null } newContainer when ic is ItemContainer { Inventory: not null } itemContainer:
itemContainer.Inventory.GetReplacementOrThis().ReplacedBy = newContainer.Inventory;
goto default;
default:
ic.GetReplacementOrThis().ReplacedBy = component;
break;
}
}
}
receiver.GetReplacementOrThis().ReplacedBy = clone;
}
for (int i = 0; i < length; i++)
{
MapEntity clone = clones[i],
receiver = Receivers[i];
if (clone is Item it)
{
foreach (var (slotRef, inventory) in PreviousInventories)
{
if (slotRef.Item == receiver)
{
inventory.GetReplacementOrThis().TryPutItem(it, slotRef.Slot, false, false, null, createNetworkEvent: false);
}
}
}
}
foreach (MapEntity clone in clones)
{
clone.Submarine = Submarine.MainSub;
}
return builder.ToImmutable();
}
else
{
foreach (MapEntity t in Receivers)
{
MapEntity receiver = t.GetReplacementOrThis();
if (!receiver.Removed)
{
receiver.Remove();
}
}
return builder.ToImmutable();
}
}
public void MergeInto(AddOrDeleteCommand master)
{
master.Receivers.AddRange(Receivers);
master.CloneList.AddRange(CloneList);
master.ContainedItemsCommand.AddRange(ContainedItemsCommand);
foreach (var (slot, item) in PreviousInventories)
{
master.PreviousInventories.Add(slot, item);
}
}
public override LocalizedString GetDescription()
{
if (WasDeleted)
{
return Receivers.Count > 1
? TextManager.GetWithVariable("Undo.RemovedItemsMultiple", "[count]", Receivers.Count.ToString())
: TextManager.GetWithVariable("Undo.RemovedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null");
}
return Receivers.Count > 1
? TextManager.GetWithVariable("Undo.AddedItemsMultiple", "[count]", Receivers.Count.ToString())
: TextManager.GetWithVariable("Undo.AddedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null");
}
}
///
/// A command that places or drops items out of inventories
///
///
///
internal class InventoryPlaceCommand : Command
{
private readonly Inventory Inventory;
private readonly List Receivers;
private readonly bool wasDropped;
public InventoryPlaceCommand(Inventory inventory, List items, bool dropped)
{
Inventory = inventory;
Receivers = items.Select(item => new InventorySlotItem(inventory.FindIndex(item), item)).ToList();
wasDropped = dropped;
}
public override void Execute() => ContainUncontain(false);
public override void UnExecute() => ContainUncontain(true);
public override void Cleanup()
{
Receivers.Clear();
}
private void ContainUncontain(bool drop)
{
// flip the behavior if the item was dropped instead of inserted
if (wasDropped) { drop = !drop; }
foreach (var (slot, receiver) in Receivers)
{
Item item = (Item) receiver.GetReplacementOrThis();
if (drop)
{
item.Drop(null, createNetworkEvent: false);
}
else
{
Inventory.GetReplacementOrThis().TryPutItem(item, slot, false, false, null, createNetworkEvent: false);
}
}
}
public override LocalizedString GetDescription()
{
if (wasDropped)
{
return TextManager.GetWithVariable("Undo.DroppedItem", "[item]", Receivers.FirstOrDefault().Item.Name);
}
string container = "[ERROR]";
if (Inventory.Owner is Item item)
{
container = item.Name;
}
return Receivers.Count > 1
? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", ("[count]", Receivers.Count.ToString()), ("[container]", container))
: TextManager.GetWithVariables("Undo.ContainedItem", ("[item]", Receivers.FirstOrDefault().Item.Name), ("[container]", container));
}
}
///
/// A command that sets item properties
///
internal class PropertyCommand : Command
{
private Dictionary