Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaClient/ClientSource/SubEditorCommands.cs
2023-10-19 17:18:51 +03:00

602 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
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();
}
/// <summary>
/// A command for setting and reverting a MapEntity rectangle
/// <see cref="SubEditorScreen"/>
/// <see cref="MapEntity"/>
/// </summary>
internal class TransformCommand : Command
{
private readonly List<MapEntity> Receivers;
private readonly List<Rectangle> NewData;
private readonly List<Rectangle> OldData;
private readonly bool Resized;
/// <summary>
/// A command for setting and reverting a MapEntity rectangle
/// </summary>
/// <param name="receivers">Entities whose rectangle has been altered</param>
/// <param name="newData">The new rectangle that is or will be applied to the map entity</param>
/// <param name="oldData">Old rectangle the map entity had before</param>
/// <param name="resized">If the transform was resized or not</param>
/// <remarks>
/// All lists should be equal in length, for every receiver there should be a corresponding entry at the same position in newData and oldData.
/// </remarks>
public TransformCommand(List<MapEntity> receivers, List<Rectangle> newData, List<Rectangle> 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<Rectangle> 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);
}
}
/// <summary>
/// A command that removes and unremoves map entities
/// <see cref="ItemPrefab"/>
/// <see cref="StructurePrefab"/>
/// <seealso cref="SubEditorScreen"/>
/// </summary>
internal class AddOrDeleteCommand : Command
{
private readonly Dictionary<InventorySlotItem, Inventory> PreviousInventories = new Dictionary<InventorySlotItem, Inventory>();
private readonly List<MapEntity> Receivers;
private readonly List<MapEntity> CloneList;
private readonly bool WasDeleted;
private readonly List<AddOrDeleteCommand> ContainedItemsCommand = new List<AddOrDeleteCommand>();
/// <summary>
/// Creates a command where all entities share the same state.
/// </summary>
/// <param name="receivers">Entities that were deleted or added</param>
/// <param name="wasDeleted">Whether or not all entities are or are going to be deleted</param>
/// <param name="handleInventoryBehavior">Ignore item inventories when set to false, workaround for pasting</param>
public AddOrDeleteCommand(List<MapEntity> receivers, bool wasDeleted, bool handleInventoryBehavior = true)
{
Debug.Assert(receivers.Count > 0, "Command has 0 receivers");
WasDeleted = wasDeleted;
Receivers = new List<MapEntity>(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<MapEntity> clonedTargets = MapEntity.Clone(receivers);
List<MapEntity> itemsToDelete = new List<MapEntity>();
foreach (MapEntity receiver in Receivers)
{
if (receiver is Item it)
{
foreach (ItemContainer component in it.GetComponents<ItemContainer>())
{
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<ItemContainer>())
{
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<MapEntity>();
CloneList = new List<MapEntity>();
DebugConsole.ThrowError("Could not store object", e);
}
}
public override void Execute()
{
var items = DeleteUndelete(true);
ContainedItemsCommand?.ForEach(static cmd => cmd.Execute());
CircuitBoxWorkaround(items);
}
public override void UnExecute()
{
var items = DeleteUndelete(false);
ContainedItemsCommand?.ForEach(static cmd => cmd.UnExecute());
CircuitBoxWorkaround(items);
}
// FIXME Temporary workaround for circuit boxes throwing console errors and breaking completely when undoing a deletion
private static void CircuitBoxWorkaround(Option<ImmutableArray<MapEntity>> entitiesOption)
{
if (!entitiesOption.TryUnwrap(out var entities)) { return; }
foreach (var entity in entities)
{
if (entity is not Item it) { continue; }
if (it.GetComponent<CircuitBox>() is not null)
{
foreach (var container in it.GetComponents<ItemContainer>())
{
container.Inventory.DeleteAllItems();
}
}
}
}
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());
}
private Option<ImmutableArray<MapEntity>> 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(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted");
List<MapEntity> 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 { 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 Option.Some(clones.ToImmutableArray());
}
else
{
foreach (MapEntity t in Receivers)
{
MapEntity receiver = t.GetReplacementOrThis();
if (!receiver.Removed)
{
receiver.Remove();
}
}
return Option.None;
}
}
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");
}
}
/// <summary>
/// A command that places or drops items out of inventories
/// </summary>
/// <see cref="Inventory"/>
/// <see cref="MapEntity"/>
internal class InventoryPlaceCommand : Command
{
private readonly Inventory Inventory;
private readonly List<InventorySlotItem> Receivers;
private readonly bool wasDropped;
public InventoryPlaceCommand(Inventory inventory, List<Item> 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));
}
}
/// <summary>
/// A command that sets item properties
/// </summary>
internal class PropertyCommand : Command
{
private Dictionary<object, List<ISerializableEntity>> OldProperties;
private readonly List<ISerializableEntity> Receivers;
private readonly Identifier PropertyName;
private readonly object NewProperties;
private string sanitizedProperty;
public readonly int PropertyCount;
/// <summary>
/// A command that sets item properties
/// </summary>
/// <param name="receivers">Affected entities</param>
/// <param name="propertyName">Real property name, not all lowercase</param>
/// <param name="newData"></param>
/// <param name="oldData"></param>
public PropertyCommand(List<ISerializableEntity> receivers, Identifier propertyName, object newData, Dictionary<object, List<ISerializableEntity>> 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<ISerializableEntity> { receiver };
PropertyName = propertyName;
OldProperties = new Dictionary<object, List<ISerializableEntity>> { { 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<Identifier, SerializableProperty> 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<GUIListBox>();
if (list == null) { continue; }
IEnumerable<SerializableEntityEditor> editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast<SerializableEntityEditor>();
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));
}
}
/// <summary>
/// A command that moves items around in inventories
/// </summary>
/// <see cref="oldInventory"/>
/// <see cref="MapEntity"/>
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);
}
}
}