Client LuaCsForBarotrauma
SubEditorCommands.cs
1 using System;
2 using System.Collections.Generic;
3 using System.Collections.Immutable;
4 using System.Diagnostics;
5 using System.Linq;
6 using System.Xml.Linq;
8 using Microsoft.Xna.Framework;
9 
10 namespace Barotrauma
11 {
12  internal readonly struct InventorySlotItem
13  {
14  public readonly int Slot;
15  public readonly Item Item;
16 
17  public InventorySlotItem(int slot, Item item)
18  {
19  Slot = slot;
20  Item = item;
21  }
22 
23  public void Deconstruct(out int slot, out Item item)
24  {
25  slot = Slot;
26  item = Item;
27  }
28  }
29 
30  internal abstract partial class Command
31  {
32  public abstract LocalizedString GetDescription();
33  }
34 
40  internal class TransformCommand : Command
41  {
42  private readonly List<MapEntity> Receivers;
43  private readonly List<Rectangle> NewData;
44  private readonly List<Rectangle> OldData;
45  private readonly bool Resized;
46 
57  public TransformCommand(List<MapEntity> receivers, List<Rectangle> newData, List<Rectangle> oldData, bool resized)
58  {
59  Receivers = receivers;
60  NewData = newData;
61  OldData = oldData;
62  Resized = resized;
63  }
64 
65  public override void Execute() => SetRects(NewData);
66  public override void UnExecute() => SetRects(OldData);
67 
68  public override void Cleanup()
69  {
70  NewData.Clear();
71  OldData.Clear();
72  Receivers.Clear();
73  }
74 
75  private void SetRects(IReadOnlyList<Rectangle> rects)
76  {
77  if (Receivers.Count != rects.Count)
78  {
79  DebugConsole.ThrowError($"Receivers.Count did not match Rects.Count ({Receivers.Count} vs {rects.Count}).");
80  return;
81  }
82 
83  for (int i = 0; i < rects.Count; i++)
84  {
85  MapEntity entity = Receivers[i].GetReplacementOrThis();
86  Rectangle Rect = rects[i];
87  Vector2 diff = Rect.Location.ToVector2() - entity.Rect.Location.ToVector2();
88  entity.Move(diff);
89  entity.Rect = Rect;
90  }
91  }
92 
93  public override LocalizedString GetDescription()
94  {
95  if (Resized)
96  {
97  return TextManager.GetWithVariable("Undo.ResizedItem", "[item]", Receivers.FirstOrDefault()?.Name);
98  }
99 
100  return Receivers.Count > 1
101  ? TextManager.GetWithVariable("Undo.MovedItemsMultiple", "[count]", Receivers.Count.ToString())
102  : TextManager.GetWithVariable("Undo.MovedItem", "[item]", Receivers.FirstOrDefault()?.Name);
103  }
104  }
105 
112  internal class AddOrDeleteCommand : Command
113  {
114  private readonly Dictionary<InventorySlotItem, Inventory> PreviousInventories = new Dictionary<InventorySlotItem, Inventory>();
115  public readonly List<MapEntity> Receivers;
116  private readonly List<MapEntity> CloneList;
117  private readonly bool WasDeleted;
118  private readonly List<AddOrDeleteCommand> ContainedItemsCommand = new List<AddOrDeleteCommand>();
119 
120  // We need to 'snapshot' the state of the circuit box and the best way to do that is to save it to XML.
121  private readonly List<XElement> CircuitBoxData = new List<XElement>();
122 
129  public AddOrDeleteCommand(List<MapEntity> receivers, bool wasDeleted, bool handleInventoryBehavior = true)
130  {
131  Debug.Assert(receivers.Count > 0, "Command has 0 receivers");
132  WasDeleted = wasDeleted;
133  Receivers = new List<MapEntity>(receivers);
134 
135  try
136  {
137  foreach (MapEntity receiver in receivers)
138  {
139  if (receiver is Item { ParentInventory: not null } it)
140  {
141  PreviousInventories.Add(new InventorySlotItem(it.ParentInventory.FindIndex(it), it), it.ParentInventory);
142  }
143  }
144 
145  List<MapEntity> clonedTargets = MapEntity.Clone(receivers);
146 
147  List<MapEntity> itemsToDelete = new List<MapEntity>();
148  foreach (MapEntity receiver in Receivers)
149  {
150  if (receiver is not Item it) { continue; }
151 
152  foreach (var cb in it.GetComponents<CircuitBox>())
153  {
154  CircuitBoxData.Add(cb.Save(new XElement("root")));
155  }
156 
157  foreach (ItemContainer component in it.GetComponents<ItemContainer>())
158  {
159  if (component.Inventory == null) { continue; }
160  itemsToDelete.AddRange(component.Inventory.AllItems.Where(static item => !item.Removed));
161  }
162  }
163 
164  if (itemsToDelete.Any() && handleInventoryBehavior)
165  {
166  ContainedItemsCommand.Add(new AddOrDeleteCommand(itemsToDelete, wasDeleted));
167  if (wasDeleted)
168  {
169  foreach (MapEntity item in itemsToDelete)
170  {
171  if (item != null && !item.Removed)
172  {
173  item.Remove();
174  }
175  }
176  }
177  }
178 
179  foreach (MapEntity clone in clonedTargets)
180  {
181  clone.ShallowRemove();
182  if (clone is Item it)
183  {
184  foreach (ItemContainer container in it.GetComponents<ItemContainer>())
185  {
186  container.Inventory?.DeleteAllItems();
187  }
188  }
189  }
190 
191  CloneList = clonedTargets;
192  }
193  // This should never happen except if we decide to make a new type of MapEntity that isn't finished yet
194  catch (Exception e)
195  {
196  Receivers = new List<MapEntity>();
197  CloneList = new List<MapEntity>();
198  DebugConsole.ThrowError("Could not store object", e);
199  }
200  }
201 
202  public override void Execute()
203  => Process(true);
204 
205  public override void UnExecute()
206  => Process(false);
207 
208  private void Process(bool redo)
209  {
210  var items = DeleteUndelete(redo);
211  foreach (var cmd in ContainedItemsCommand)
212  {
213  cmd.Process(redo);
214  }
215  ApplyCircuitBoxDataIfAny(items);
216  }
217 
228  private void ApplyCircuitBoxDataIfAny(ImmutableArray<Item> items)
229  {
230  int cbIndex = 0;
231  foreach (var newItem in items)
232  {
233  foreach (ItemComponent component in newItem.Components)
234  {
235  if (component is not CircuitBox cb) { continue; }
236 
237  if (cbIndex < 0 || cbIndex >= CircuitBoxData.Count)
238  {
239  DebugConsole.ThrowError("Unable to restore wiring in circuit box, index out of range.");
240  continue;
241  }
242 
243  var cbData = CircuitBoxData[cbIndex];
244  cbIndex++;
245 
246  cb.LoadFromXML(new ContentXElement(null, cbData));
247  }
248  }
249  }
250 
251  public override void Cleanup()
252  {
253  foreach (MapEntity entity in CloneList)
254  {
255  if (!entity.Removed)
256  {
257  entity.Remove();
258  }
259  }
260 
261  CloneList?.Clear();
262  Receivers.Clear();
263  PreviousInventories?.Clear();
264  ContainedItemsCommand?.ForEach(static cmd => cmd.Cleanup());
265  CircuitBoxData.Clear();
266  }
267 
268  private ImmutableArray<Item> DeleteUndelete(bool redo)
269  {
270  bool wasDeleted = WasDeleted;
271 
272  // We are redoing instead of undoing, flip the behavior
273  if (redo) { wasDeleted = !wasDeleted; }
274 
275  // collect newly created items so we can update their circuit boxes if any
276  var builder = ImmutableArray.CreateBuilder<Item>();
277 
278  if (wasDeleted)
279  {
280  Debug.Assert(Receivers.All(static entity => entity.GetReplacementOrThis().Removed), "Tried to redo a deletion but some items were not deleted");
281 
282  List<MapEntity> clones = MapEntity.Clone(CloneList);
283  int length = Math.Min(Receivers.Count, clones.Count);
284  for (int i = 0; i < length; i++)
285  {
286  MapEntity clone = clones[i],
287  receiver = Receivers[i];
288 
289  if (receiver.GetReplacementOrThis() is Item item && clone is Item cloneItem)
290  {
291  builder.Add(cloneItem);
292  foreach (ItemComponent ic in item.Components)
293  {
294  int index = item.GetComponentIndex(ic);
295  ItemComponent component = cloneItem.Components.ElementAtOrDefault(index);
296  switch (component)
297  {
298  case null:
299  continue;
300  case ItemContainer { Inventory: not null } newContainer when ic is ItemContainer { Inventory: not null } itemContainer:
301  itemContainer.Inventory.GetReplacementOrThiS().ReplacedBy = newContainer.Inventory;
302  goto default;
303  default:
304  ic.GetReplacementOrThis().ReplacedBy = component;
305  break;
306  }
307  }
308  }
309 
310  receiver.GetReplacementOrThis().ReplacedBy = clone;
311  }
312 
313  for (int i = 0; i < length; i++)
314  {
315  MapEntity clone = clones[i],
316  receiver = Receivers[i];
317 
318  if (clone is Item it)
319  {
320  foreach (var (slotRef, inventory) in PreviousInventories)
321  {
322  if (slotRef.Item == receiver)
323  {
324  inventory.GetReplacementOrThiS().TryPutItem(it, slotRef.Slot, false, false, null, createNetworkEvent: false);
325  }
326  }
327  }
328  }
329 
330  foreach (MapEntity clone in clones)
331  {
332  clone.Submarine = Submarine.MainSub;
333  }
334 
335  return builder.ToImmutable();
336  }
337  else
338  {
339  foreach (MapEntity t in Receivers)
340  {
341  MapEntity receiver = t.GetReplacementOrThis();
342  if (!receiver.Removed)
343  {
344  receiver.Remove();
345  }
346  }
347 
348  return builder.ToImmutable();
349  }
350  }
351 
352  public void MergeInto(AddOrDeleteCommand master)
353  {
354  master.Receivers.AddRange(Receivers);
355  master.CloneList.AddRange(CloneList);
356  master.ContainedItemsCommand.AddRange(ContainedItemsCommand);
357  foreach (var (slot, item) in PreviousInventories)
358  {
359  master.PreviousInventories.Add(slot, item);
360  }
361  }
362 
363  public override LocalizedString GetDescription()
364  {
365  if (WasDeleted)
366  {
367  return Receivers.Count > 1
368  ? TextManager.GetWithVariable("Undo.RemovedItemsMultiple", "[count]", Receivers.Count.ToString())
369  : TextManager.GetWithVariable("Undo.RemovedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null");
370  }
371 
372  return Receivers.Count > 1
373  ? TextManager.GetWithVariable("Undo.AddedItemsMultiple", "[count]", Receivers.Count.ToString())
374  : TextManager.GetWithVariable("Undo.AddedItem", "[item]", Receivers.FirstOrDefault()?.Name ?? "null");
375  }
376  }
377 
383  internal class InventoryPlaceCommand : Command
384  {
385  private readonly Inventory Inventory;
386  private readonly List<InventorySlotItem> Receivers;
387  private readonly bool wasDropped;
388 
389  public InventoryPlaceCommand(Inventory inventory, List<Item> items, bool dropped)
390  {
391  Inventory = inventory;
392  Receivers = items.Select(item => new InventorySlotItem(inventory.FindIndex(item), item)).ToList();
393  wasDropped = dropped;
394  }
395 
396  public override void Execute() => ContainUncontain(false);
397  public override void UnExecute() => ContainUncontain(true);
398 
399  public override void Cleanup()
400  {
401  Receivers.Clear();
402  }
403 
404  private void ContainUncontain(bool drop)
405  {
406  // flip the behavior if the item was dropped instead of inserted
407  if (wasDropped) { drop = !drop; }
408 
409  foreach (var (slot, receiver) in Receivers)
410  {
411  Item item = (Item) receiver.GetReplacementOrThis();
412 
413  if (drop)
414  {
415  item.Drop(null, createNetworkEvent: false);
416  }
417  else
418  {
419  Inventory.GetReplacementOrThiS().TryPutItem(item, slot, false, false, null, createNetworkEvent: false);
420  }
421  }
422  }
423 
424  public override LocalizedString GetDescription()
425  {
426  if (wasDropped)
427  {
428  return TextManager.GetWithVariable("Undo.DroppedItem", "[item]", Receivers.FirstOrDefault().Item.Name);
429  }
430 
431  string container = "[ERROR]";
432 
433  if (Inventory.Owner is Item item)
434  {
435  container = item.Name;
436  }
437 
438  return Receivers.Count > 1
439  ? TextManager.GetWithVariables("Undo.ContainedItemsMultiple", ("[count]", Receivers.Count.ToString()), ("[container]", container))
440  : TextManager.GetWithVariables("Undo.ContainedItem", ("[item]", Receivers.FirstOrDefault().Item.Name), ("[container]", container));
441  }
442  }
443 
447  internal class PropertyCommand : Command
448  {
449  private Dictionary<object, List<ISerializableEntity>> OldProperties;
450  private readonly List<ISerializableEntity> Receivers;
451  private readonly Identifier PropertyName;
452  private readonly object NewProperties;
453  private string sanitizedProperty;
454 
455  public readonly int PropertyCount;
456 
464  public PropertyCommand(List<ISerializableEntity> receivers, Identifier propertyName, object newData, Dictionary<object, List<ISerializableEntity>> oldData)
465  {
466  Receivers = receivers;
467  PropertyName = propertyName;
468  OldProperties = oldData;
469  NewProperties = newData;
470  PropertyCount = receivers.Count;
471  SanitizeProperty();
472  }
473 
474  public PropertyCommand(ISerializableEntity receiver, Identifier propertyName, object newData, object oldData)
475  {
476  Receivers = new List<ISerializableEntity> { receiver };
477  PropertyName = propertyName;
478  OldProperties = new Dictionary<object, List<ISerializableEntity>> { { oldData, Receivers } };
479  NewProperties = newData;
480  PropertyCount = 1;
481  SanitizeProperty();
482  }
483 
484  public bool MergeInto(PropertyCommand master)
485  {
486  if (!master.Receivers.SequenceEqual(Receivers)) { return false; }
487  master.OldProperties = OldProperties;
488  return true;
489  }
490 
491  private void SanitizeProperty()
492  {
493  sanitizedProperty = NewProperties switch
494  {
495  float f => f.FormatSingleDecimal(),
496  Point point => XMLExtensions.PointToString(point),
497  Vector2 vector2 => vector2.FormatZeroDecimal(),
498  Vector3 vector3 => vector3.FormatSingleDecimal(),
499  Vector4 vector4 => vector4.FormatSingleDecimal(),
500  Color color => XMLExtensions.ColorToString(color),
501  Rectangle rectangle => XMLExtensions.RectToString(rectangle),
502  _ => NewProperties.ToString()
503  };
504  }
505 
506  public override void Execute() => SetProperties(false);
507  public override void UnExecute() => SetProperties(true);
508 
509  public override void Cleanup()
510  {
511  Receivers.Clear();
512  OldProperties.Clear();
513  }
514 
515  private void SetProperties(bool undo)
516  {
517  foreach (ISerializableEntity t in Receivers)
518  {
519  ISerializableEntity receiver;
520  switch (t)
521  {
522  case MapEntity me when me.GetReplacementOrThis() is ISerializableEntity sEntity:
523  receiver = sEntity;
524  break;
525  case ItemComponent ic when ic.GetReplacementOrThis() is ISerializableEntity sItemComponent:
526  receiver = sItemComponent;
527  break;
528  default:
529  receiver = t;
530  break;
531  }
532 
533  object data = NewProperties;
534 
535  if (undo)
536  {
537  foreach (var (key, value) in OldProperties)
538  {
539  if (value.Contains(t)) { data = key; }
540  }
541  }
542 
543  if (receiver.SerializableProperties != null)
544  {
545  Dictionary<Identifier, SerializableProperty> props = receiver.SerializableProperties;
546 
547  if (props.TryGetValue(PropertyName, out SerializableProperty prop))
548  {
549  prop.TrySetValue(receiver, data);
550  // Update the editing hud
551  if (MapEntity.EditingHUD == null || (MapEntity.EditingHUD.UserData != receiver && (receiver is ItemComponent ic && MapEntity.EditingHUD.UserData != ic.Item))) { continue; }
552 
553  GUIListBox list = MapEntity.EditingHUD.GetChild<GUIListBox>();
554  if (list == null) { continue; }
555 
556  IEnumerable<SerializableEntityEditor> editors = list.Content.FindChildren(comp => comp is SerializableEntityEditor).Cast<SerializableEntityEditor>();
557  SerializableEntityEditor.LockEditing = true;
558  foreach (SerializableEntityEditor editor in editors)
559  {
560  if (editor.UserData == receiver && editor.Fields.TryGetValue(PropertyName, out GUIComponent[] _))
561  {
562  editor.UpdateValue(prop, data);
563  }
564  }
565 
566  SerializableEntityEditor.LockEditing = false;
567  }
568  }
569  }
570  }
571 
572  public override LocalizedString GetDescription()
573  {
574  return Receivers.Count > 1
575  ? TextManager.GetWithVariables("Undo.ChangedPropertyMultiple",
576  ("[property]", PropertyName.Value),
577  ("[count]", Receivers.Count.ToString()),
578  ("[value]", sanitizedProperty))
579  : TextManager.GetWithVariables("Undo.ChangedProperty",
580  ("[property]", PropertyName.Value),
581  ("[item]", Receivers.FirstOrDefault()?.Name),
582  ("[value]", sanitizedProperty));
583  }
584  }
585 
591  internal class InventoryMoveCommand : Command
592  {
593  private readonly Inventory oldInventory;
594  private readonly Inventory newInventory;
595  private readonly int oldSlot;
596  private readonly int newSlot;
597  private readonly Item targetItem;
598 
599  public InventoryMoveCommand(Inventory oldInventory, Inventory newInventory, Item item, int oldSlot, int newSlot)
600  {
601  this.newInventory = newInventory;
602  this.oldInventory = oldInventory;
603  this.oldSlot = oldSlot;
604  this.newSlot = newSlot;
605  targetItem = item;
606  }
607 
608  public override void Execute()
609  {
610  if (targetItem.GetReplacementOrThis() is Item item)
611  {
612  newInventory?.GetReplacementOrThiS().TryPutItem(item, newSlot, true, false, null, createNetworkEvent: false);
613  }
614  }
615 
616  public override void UnExecute()
617  {
618  if (targetItem.GetReplacementOrThis() is Item item)
619  {
620  oldInventory?.GetReplacementOrThiS().TryPutItem(item, oldSlot, true, false, null, createNetworkEvent: false);
621  }
622  }
623 
624  public override void Cleanup() { }
625 
626  public override LocalizedString GetDescription()
627  {
628  return TextManager.GetWithVariable("Undo.MovedItem", "[item]", targetItem.Name);
629  }
630  }
631 }
Inventory(Entity owner, int capacity, int slotsPerRow=5)
void DeleteAllItems()
Deletes all items inside the inventory (and also recursively all items inside the items)
virtual IEnumerable< Item > AllItems
All items contained in the inventory. Stacked items are returned as individual instances....
The base class for components holding the different functionalities of the item