using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; 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(); private readonly List Receivers; private readonly List CloneList; private readonly bool WasDeleted; private readonly List ContainedItemsCommand = 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 it && it.ParentInventory != null) { 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 Item it) { foreach (ItemContainer component in it.GetComponents()) { if (component.Inventory == null) { continue; } itemsToDelete.AddRange(component.Inventory.AllItems.Where(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() { DeleteUndelete(true); ContainedItemsCommand?.ForEach(cmd => cmd.Execute()); } public override void UnExecute() { DeleteUndelete(false); ContainedItemsCommand?.ForEach(cmd => cmd.UnExecute()); } public override void Cleanup() { foreach (MapEntity entity in CloneList) { if (!entity.Removed) { entity.Remove(); } } CloneList?.Clear(); Receivers.Clear(); PreviousInventories?.Clear(); ContainedItemsCommand?.ForEach(cmd => cmd.Cleanup()); } private void DeleteUndelete(bool redo) { bool wasDeleted = WasDeleted; // We are redoing instead of undoing, flip the behavior if (redo) { wasDeleted = !wasDeleted; } if (wasDeleted) { Debug.Assert(Receivers.All(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) { foreach (ItemComponent ic in item.Components) { int index = item.GetComponentIndex(ic); ItemComponent component = cloneItem.Components.ElementAtOrDefault(index); switch (component) { case null: continue; case ItemContainer newContainer when newContainer.Inventory != null && ic is ItemContainer itemContainer && itemContainer.Inventory != null: 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; } } else { foreach (MapEntity t in Receivers) { MapEntity receiver = t.GetReplacementOrThis(); if (!receiver.Removed) { receiver.Remove(); } } } } 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> OldProperties; private readonly List Receivers; private readonly Identifier PropertyName; private readonly object NewProperties; private string sanitizedProperty; public readonly int PropertyCount; /// /// A command that sets item properties /// /// Affected entities /// Real property name, not all lowercase /// /// public PropertyCommand(List receivers, Identifier propertyName, object newData, Dictionary> oldData) { Receivers = receivers; PropertyName = propertyName; OldProperties = oldData; NewProperties = newData; PropertyCount = receivers.Count; SanitizeProperty(); } public PropertyCommand(ISerializableEntity receiver, Identifier propertyName, object newData, object oldData) { Receivers = new List { receiver }; PropertyName = propertyName; OldProperties = new Dictionary> { { oldData, Receivers } }; NewProperties = newData; PropertyCount = 1; SanitizeProperty(); } public bool MergeInto(PropertyCommand master) { if (!master.Receivers.SequenceEqual(Receivers)) { return false; } master.OldProperties = OldProperties; return true; } private void SanitizeProperty() { sanitizedProperty = NewProperties switch { float f => f.FormatSingleDecimal(), Point point => XMLExtensions.PointToString(point), Vector2 vector2 => vector2.FormatZeroDecimal(), Vector3 vector3 => vector3.FormatSingleDecimal(), Vector4 vector4 => vector4.FormatSingleDecimal(), Color color => XMLExtensions.ColorToString(color), Rectangle rectangle => XMLExtensions.RectToString(rectangle), _ => NewProperties.ToString() }; } public override void Execute() => SetProperties(false); public override void UnExecute() => SetProperties(true); public override void Cleanup() { Receivers.Clear(); OldProperties.Clear(); } private void SetProperties(bool undo) { foreach (ISerializableEntity t in Receivers) { ISerializableEntity receiver; switch (t) { case MapEntity me when me.GetReplacementOrThis() is ISerializableEntity sEntity: receiver = sEntity; break; case ItemComponent ic when ic.GetReplacementOrThis() is ISerializableEntity sItemComponent: receiver = sItemComponent; break; default: receiver = t; break; } object data = NewProperties; if (undo) { foreach (var (key, value) in OldProperties) { if (value.Contains(t)) { data = key; } } } if (receiver.SerializableProperties != null) { Dictionary props = receiver.SerializableProperties; if (props.TryGetValue(PropertyName, out SerializableProperty prop)) { prop.TrySetValue(receiver, data); // Update the editing hud if (MapEntity.EditingHUD == null || (MapEntity.EditingHUD.UserData != receiver && (receiver is ItemComponent ic && MapEntity.EditingHUD.UserData != ic.Item))) { continue; } GUIListBox list = MapEntity.EditingHUD.GetChild(); if (list == null) { continue; } IEnumerable editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast(); SerializableEntityEditor.LockEditing = true; foreach (SerializableEntityEditor editor in editors) { if (editor.UserData == receiver && editor.Fields.TryGetValue(PropertyName, out GUIComponent[] _)) { editor.UpdateValue(prop, data); } } SerializableEntityEditor.LockEditing = false; } } } } public override LocalizedString GetDescription() { return Receivers.Count > 1 ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple", ("[property]", PropertyName.Value), ("[count]", Receivers.Count.ToString()), ("[value]", sanitizedProperty)) : TextManager.GetWithVariables("Undo.ChangedProperty", ("[property]", PropertyName.Value), ("[item]", Receivers.FirstOrDefault()?.Name), ("[value]", sanitizedProperty)); } } /// /// A command that moves items around in inventories /// /// /// internal class InventoryMoveCommand : Command { private readonly Inventory oldInventory; private readonly Inventory newInventory; private readonly int oldSlot; private readonly int newSlot; private readonly Item targetItem; public InventoryMoveCommand(Inventory oldInventory, Inventory newInventory, Item item, int oldSlot, int newSlot) { this.newInventory = newInventory; this.oldInventory = oldInventory; this.oldSlot = oldSlot; this.newSlot = newSlot; targetItem = item; } public override void Execute() { if (targetItem.GetReplacementOrThis() is Item item) { newInventory?.GetReplacementOrThiS().TryPutItem(item, newSlot, true, false, null, createNetworkEvent: false); } } public override void UnExecute() { if (targetItem.GetReplacementOrThis() is Item item) { oldInventory?.GetReplacementOrThiS().TryPutItem(item, oldSlot, true, false, null, createNetworkEvent: false); } } public override void Cleanup() { } public override LocalizedString GetDescription() { return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name); } } }