Client LuaCsForBarotrauma
BarotraumaClient/ClientSource/Items/Components/Signal/CircuitBox.cs
1 #nullable enable
2 
3 using System;
4 using System.Collections.Generic;
5 using System.Collections.Immutable;
6 using System.Linq;
8 using Microsoft.Xna.Framework;
9 
11 {
12  internal sealed partial class CircuitBox
13  {
14  public CircuitBoxUI? UI;
15  public readonly Dictionary<Character, CircuitBoxCursor> ActiveCursors = new Dictionary<Character, CircuitBoxCursor>();
16  public Option<ItemPrefab> HeldComponent = Option.None;
17 
18  private const float CursorUpdateInterval = 1f;
19  private float cursorUpdateTimer;
20 
21  private readonly Vector2[] recordedCursorPositions = new Vector2[10];
22  private Option<Vector2> recordedDragStart = Option.None;
23  private Option<ItemPrefab> recordedHeldPrefab = Option.None;
24 
29  private bool wasInitializedByServer;
30 
31  public Sprite? WireSprite { get; private set; }
32  public Sprite? ConnectionSprite { get; private set; }
33  public Sprite? WireConnectorSprite { get; private set; }
34  public Sprite? ConnectionScrewSprite { get; private set; }
35  public UISprite? NodeFrameSprite { get; private set; }
36  public UISprite? NodeTopSprite { get; private set; }
37 
38  protected override void CreateGUI()
39  {
40  base.CreateGUI();
42  UI?.CreateGUI(GuiFrame);
43  }
44 
45  partial void InitProjSpecific(ContentXElement element)
46  {
47  UI = new CircuitBoxUI(this);
48  IsActive = true;
49  CreateGUI();
50 
51  foreach (var subElement in element.Elements())
52  {
53  switch (subElement.Name.ToString().ToLowerInvariant())
54  {
55  case "wiresprite":
56  WireSprite = new Sprite(subElement);
57  break;
58  case "connectionsprite":
59  ConnectionSprite = new Sprite(subElement);
60  break;
61  case "wireconnectorsprite":
62  WireConnectorSprite = new Sprite(subElement);
63  break;
64  case "connectionscrewsprite":
65  ConnectionScrewSprite = new Sprite(subElement);
66  break;
67  }
68  }
69 
70  if (GUIStyle.GetComponentStyle("CircuitBoxTop") is { } topStyle)
71  {
72  NodeTopSprite = topStyle.Sprites[GUIComponent.ComponentState.None][0];
73  }
74 
75  if (GUIStyle.GetComponentStyle("CircuitBoxFrame") is { } compStyle)
76  {
77  NodeFrameSprite = compStyle.Sprites[GUIComponent.ComponentState.None][0];
78  }
79  }
80 
81  public override bool ShouldDrawHUD(Character character)
82  => character == Character.Controlled && (character.SelectedItem == item || character.SelectedSecondaryItem == item);
83 
84  public override void UpdateHUDComponentSpecific(Character character, float deltaTime, Camera cam)
85  {
86  if (UI is null) { return; }
87 
88  UI.Update(deltaTime);
89 
90  if (GameMain.NetworkMember is null) { return; }
91 
92  foreach (var (cursorChar, cursor) in ActiveCursors)
93  {
94  if (!cursor.IsActive) { continue; }
95 
96  ActiveCursors[cursorChar].Update(deltaTime);
97  }
98 
99  Vector2 cursorPos = UI.GetCursorPosition();
100  int lastCursorPosIndex = recordedCursorPositions.Length - 1;
101 
102  if (cursorUpdateTimer < CursorUpdateInterval)
103  {
104  cursorUpdateTimer += deltaTime;
105  int cursorIndex = (int)MathF.Floor(cursorUpdateTimer * lastCursorPosIndex);
106  RecordCursorPosition(cursorIndex);
107  }
108  else
109  {
110  RecordCursorPosition(lastCursorPosIndex);
111  SendCursorState(recordedCursorPositions, recordedDragStart, recordedHeldPrefab.Select(static c => c.Identifier));
112 
113  recordedDragStart = Option.None;
114  recordedHeldPrefab = Option.None;
115  cursorUpdateTimer = 0f;
116  }
117 
118  void RecordCursorPosition(int index)
119  {
120  var dragStart = UI.GetDragStart();
121  if (dragStart.IsSome()) { recordedDragStart = dragStart; }
122 
123  var heldComponent = HeldComponent;
124  if (heldComponent.IsSome()) { recordedHeldPrefab = heldComponent; }
125 
126  if (index >= 0 && index < recordedCursorPositions.Length) { recordedCursorPositions[index] = cursorPos; }
127  }
128  }
129 
130  public void RemoveComponents(IReadOnlyCollection<CircuitBoxComponent> node)
131  {
132  if (Locked) { return; }
133  var ids = node.Select(static n => n.ID).ToImmutableArray();
134 
135  if (GameMain.NetworkMember is null)
136  {
137  CreateRefundItemsForUsedResources(ids, Character.Controlled);
138  RemoveComponentInternal(ids);
139  return;
140  }
141 
142  if (!node.Any()) { return; }
143 
144  CreateClientEvent(new CircuitBoxRemoveComponentEvent(ids));
145  }
146 
147  public void AddWire(CircuitBoxConnection one, CircuitBoxConnection two)
148  {
149  if (Locked) { return; }
150  if (GameMain.NetworkMember is null)
151  {
152  Connect(one, two, static delegate { }, CircuitBoxWire.SelectedWirePrefab);
153  return;
154  }
155 
156  if (!VerifyConnection(one, two)) { return; }
157 
158  CreateClientEvent(new CircuitBoxClientAddWireEvent(Color.White, CircuitBoxConnectorIdentifier.FromConnection(one), CircuitBoxConnectorIdentifier.FromConnection(two), CircuitBoxWire.SelectedWirePrefab.UintIdentifier));
159  }
160 
161  public void RemoveWires(IReadOnlyCollection<CircuitBoxWire> wires)
162  {
163  if (Locked) { return; }
164  var ids = wires.Select(static w => w.ID).ToImmutableArray();
165  if (GameMain.NetworkMember is null)
166  {
167  RemoveWireInternal(ids);
168  return;
169  }
170 
171  if (!ids.Any()) { return; }
172  CreateClientEvent(new CircuitBoxRemoveWireEvent(ids));
173  }
174 
175  public void SelectComponents(IReadOnlyCollection<CircuitBoxNode> moveables, bool overwrite)
176  {
177  if (Character.Controlled is not { ID: var controlledId }) { return; }
178 
179  var ids = ImmutableArray.CreateBuilder<ushort>();
180  var ios = ImmutableArray.CreateBuilder<CircuitBoxInputOutputNode.Type>();
181  var labelIds = ImmutableArray.CreateBuilder<ushort>();
182 
183  foreach (var moveable in moveables)
184  {
185  if (moveable is { IsSelected: true, IsSelectedByMe: false }) { continue; }
186 
187  switch (moveable)
188  {
189  case CircuitBoxComponent node:
190  ids.Add(node.ID);
191  break;
192  case CircuitBoxInputOutputNode io:
193  ios.Add(io.NodeType);
194  break;
195  case CircuitBoxLabelNode label:
196  labelIds.Add(label.ID);
197  break;
198  }
199  }
200 
201  if (GameMain.NetworkMember is null)
202  {
203  SelectComponentsInternal(ids, controlledId, overwrite);
204  SelectInputOutputInternal(ios, controlledId, overwrite);
205  SelectLabelsInternal(labelIds, controlledId, overwrite);
206  return;
207  }
208 
209  if (!ids.Any() && !ios.Any() && !labelIds.Any() && !overwrite) { return; }
210 
211  CreateClientEvent(new CircuitBoxSelectNodesEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), overwrite, controlledId));
212  }
213 
214  public void SelectWires(IReadOnlyCollection<CircuitBoxWire> wires, bool overwrite)
215  {
216  if (Character.Controlled is not { ID: var controlledId }) { return; }
217 
218  var ids = (from wire in wires where !wire.IsSelected || wire.IsSelectedByMe select wire.ID).ToImmutableArray();
219 
220  if (GameMain.NetworkMember is null)
221  {
222  SelectWiresInternal(ids, controlledId, overwrite);
223  return;
224  }
225 
226  if (!ids.Any() && !overwrite) { return; }
227 
228  CreateClientEvent(new CircuitBoxSelectWiresEvent(ids, overwrite, Character.Controlled.ID));
229  }
230 
231  public void MoveComponent(Vector2 moveAmount, IReadOnlyCollection<CircuitBoxNode> moveables)
232  {
233  if (Locked) { return; }
234  var ids = ImmutableArray.CreateBuilder<ushort>();
235  var ios = ImmutableArray.CreateBuilder<CircuitBoxInputOutputNode.Type>();
236  var labelIds = ImmutableArray.CreateBuilder<ushort>();
237 
238  foreach (CircuitBoxNode move in moveables)
239  {
240  switch (move)
241  {
242  case CircuitBoxComponent node:
243  ids.Add(node.ID);
244  break;
245  case CircuitBoxInputOutputNode io:
246  ios.Add(io.NodeType);
247  break;
248  case CircuitBoxLabelNode label:
249  labelIds.Add(label.ID);
250  break;
251  }
252  }
253 
254  if (GameMain.NetworkMember is null)
255  {
256  MoveNodesInternal(ids, ios, labelIds, moveAmount);
257  return;
258  }
259 
260  if (!ids.Any() && !ios.Any() && !labelIds.Any()) { return; }
261 
262 
263  CreateClientEvent(new CircuitBoxMoveComponentEvent(ids.ToImmutable(), ios.ToImmutable(), labelIds.ToImmutable(), moveAmount));
264  }
265 
266  public void AddComponent(ItemPrefab prefab, Vector2 pos)
267  {
268  if (Locked) { return; }
269  if (GameMain.NetworkMember is null)
270  {
271  ItemPrefab resource;
272 
273  if (IsFull) { return; }
274 
275  if (IsInGame())
276  {
277  if (!GetApplicableResourcePlayerHas(prefab, Character.Controlled).TryUnwrap(out var r)) { return; }
278  resource = r.Prefab;
279  RemoveItem(r);
280  }
281  else
282  {
283  resource = ItemPrefab.Prefabs[Tags.FPGACircuit];
284  }
285 
286  AddComponentInternal(ICircuitBoxIdentifiable.FindFreeID(Components), prefab, resource, pos, Character.Controlled, onItemSpawned: null);
287  return;
288  }
289 
290  CreateClientEvent(new CircuitBoxAddComponentEvent(prefab.UintIdentifier, pos));
291  }
292 
293  public void RenameLabel(CircuitBoxLabelNode label, Color color, NetLimitedString header, NetLimitedString body)
294  {
295  if (Locked) { return; }
296  if (GameMain.NetworkMember is null)
297  {
298  label.EditText(header, body);
299  label.Color = color;
300  return;
301  }
302 
303  CreateClientEvent(new CircuitBoxRenameLabelEvent(label.ID, color, header, body));
304  }
305 
306  public void SetConnectionLabelOverrides(CircuitBoxInputOutputNode node, Dictionary<string, string> newOverrides)
307  {
308  if (GameMain.NetworkMember is null)
309  {
310  node.ReplaceAllConnectionLabelOverrides(newOverrides);
311  return;
312  }
313 
314  CreateClientEvent(new CircuitBoxRenameConnectionLabelsEvent(node.NodeType, newOverrides.ToNetDictionary()));
315  }
316 
317  public void ResizeNode(CircuitBoxNode node, CircuitBoxResizeDirection dir, Vector2 amount)
318  {
319  if (Locked) { return; }
320  var resize = node.ResizeBy(dir, amount);
321  if (GameMain.NetworkMember is null)
322  {
323  node.ApplyResize(resize.Size, resize.Pos);
324  return;
325  }
326 
327  // TODO this needs to be refactored at some point, probably not now
328  // the problem here is that the circuit box supports resizing all nodes
329  // but we limit the resizing to only labels on the client
330  // and on the server we only have a network message that targets labels
331  // so if we ever want the ability to resize other nodes (could be useful) the network message
332  // needs to know what type of ID it's targeting
333  if (node is not ICircuitBoxIdentifiable identifiable)
334  {
335  DebugConsole.ThrowError("Tried to resize a node that doesn't have an ID.");
336  return;
337  }
338 
339  CreateClientEvent(new CircuitBoxResizeLabelEvent(identifiable.ID, resize.Pos, resize.Size));
340  }
341 
342  public void AddLabel(Vector2 pos)
343  {
344  if (Locked) { return; }
345  if (GameMain.NetworkMember is null)
346  {
347  AddLabelInternal(ICircuitBoxIdentifiable.FindFreeID(Labels), GUIStyle.Blue, pos, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty);
348  return;
349  }
350 
351  CreateClientEvent(new CircuitBoxAddLabelEvent(pos, GUIStyle.Blue, CircuitBoxLabelNode.DefaultHeaderText, NetLimitedString.Empty));
352  }
353 
354  public void RemoveLabel(IReadOnlyCollection<CircuitBoxLabelNode> labels)
355  {
356  if (Locked) { return; }
357  if (!labels.Any()) { return; }
358 
359  var ids = labels.Select(static n => n.ID).ToImmutableArray();
360 
361  if (GameMain.NetworkMember is null)
362  {
363  RemoveLabelInternal(ids);
364  return;
365  }
366 
367  CreateClientEvent(new CircuitBoxRemoveLabelEvent(ids));
368  }
369 
370  public partial void OnViewUpdateProjSpecific()
371  {
372  UI?.MouseSnapshotHandler.UpdateConnections();
373  UI?.UpdateComponentList();
374  }
375 
376  protected override void OnResolutionChanged()
377  {
378  base.OnResolutionChanged();
379  CreateGUI();
380  }
381 
382  // Remove selection when the circuit box is deselected
383  public partial void OnDeselected(Character c)
384  {
385  cursorUpdateTimer = 0f;
386 
387  // Server will broadcast the deselection, we don't need to do it ourselves
388  if (GameMain.NetworkMember is not null) { return; }
389  ClearAllSelectionsInternal(c.ID);
390  }
391 
392  public void ClientRead(INetSerializableStruct data)
393  {
394  switch (data)
395  {
396  case NetCircuitBoxCursorInfo cursorInfo:
397  {
398  ClientReadCursor(cursorInfo);
399  break;
400  }
401  case CircuitBoxErrorEvent errorData:
402  {
403  DebugConsole.ThrowError($"The server responded with an error: {errorData.Message}");
404  break;
405  }
406  default:
407  throw new ArgumentOutOfRangeException(nameof(data), data, "This data cannot be handled using direct network messages.");
408  }
409  }
410 
411  public void SendMessage(CircuitBoxOpcode opcode, INetSerializableStruct data)
412  {
413  IWriteMessage msg = new WriteOnlyMessage().WithHeader(ClientPacketHeader.CIRCUITBOX);
414 
415  msg.WriteNetSerializableStruct(new NetCircuitBoxHeader(
416  Opcode: opcode,
417  ItemID: item.ID,
418  ComponentIndex: (byte)item.GetComponentIndex(this)));
419 
420  msg.WriteNetSerializableStruct(data);
421 
422  DeliveryMethod deliveryMethod =
423  UnrealiableOpcodes.Contains(opcode)
424  ? DeliveryMethod.Unreliable
425  : DeliveryMethod.Reliable;
426 
427  GameMain.Client?.ClientPeer?.Send(msg, deliveryMethod);
428  }
429 
430  private void SendCursorState(Vector2[] cursorPositions, Option<Vector2> dragStart, Option<Identifier> heldComponent)
431  {
432  if (!IsRoundRunning()) { return; }
433 
434  var msg = new NetCircuitBoxCursorInfo(
435  RecordedPositions: cursorPositions,
436  DragStart: dragStart,
437  HeldItem: heldComponent);
438 
439  SendMessage(CircuitBoxOpcode.Cursor, msg);
440  }
441 
442  public void ClientReadCursor(NetCircuitBoxCursorInfo info)
443  {
444  if (Entity.FindEntityByID(info.CharacterID) is not Character character) { return; }
445 
446  if (!ActiveCursors.ContainsKey(character))
447  {
448  var newCursor = new CircuitBoxCursor(info);
449  ActiveCursors.Add(character, newCursor);
450  return;
451  }
452 
453  var activeCursor = ActiveCursors[character];
454  activeCursor.UpdateInfo(info);
455  activeCursor.ResetTimers();
456  }
457 
458  public void CreateClientEvent(INetSerializableStruct data)
459  => item.CreateClientEvent(this, new CircuitBoxEventData(data));
460 
461  public void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData? extraData = null)
462  {
463  if (extraData is null) { return; }
464  var eventData = ExtractEventData<CircuitBoxEventData>(extraData);
465  msg.WriteByte((byte)eventData.Opcode);
466  msg.WriteNetSerializableStruct(eventData.Data);
467  }
468 
469  public void ClientEventRead(IReadMessage msg, float sendingTime)
470  {
471  var header = (CircuitBoxOpcode)msg.ReadByte();
472 
473  switch (header)
474  {
475  case CircuitBoxOpcode.AddComponent:
476  {
477  var data = INetSerializableStruct.Read<CircuitBoxServerCreateComponentEvent>(msg);
478  AddComponentFromData(data);
479  break;
480  }
481  case CircuitBoxOpcode.DeleteComponent:
482  {
483  var data = INetSerializableStruct.Read<CircuitBoxRemoveComponentEvent>(msg);
484  RemoveComponentInternal(data.TargetIDs);
485  break;
486  }
487  case CircuitBoxOpcode.MoveComponent:
488  {
489  var data = INetSerializableStruct.Read<CircuitBoxMoveComponentEvent>(msg);
490  MoveNodesInternal(data.TargetIDs, data.IOs, data.LabelIDs, data.MoveAmount);
491  break;
492  }
493  case CircuitBoxOpcode.UpdateSelection:
494  {
495  var data = INetSerializableStruct.Read<CircuitBoxServerUpdateSelection>(msg);
496 
497  var nodeDict = data.ComponentIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);
498  var wireDict = data.WireIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);
499  var ioDict = data.InputOutputs.ToImmutableDictionary(static s => s.Type, static s => s.SelectedBy);
500  var labelDict = data.LabelIds.ToImmutableDictionary(static s => s.ID, static s => s.SelectedBy);
501 
502  UpdateSelections(nodeDict, wireDict, ioDict, labelDict);
503  break;
504  }
505  case CircuitBoxOpcode.AddWire:
506  {
507  var data = INetSerializableStruct.Read<CircuitBoxServerCreateWireEvent>(msg);
508  AddWireFromData(data);
509  break;
510  }
511  case CircuitBoxOpcode.RemoveWire:
512  {
513  var data = INetSerializableStruct.Read<CircuitBoxRemoveWireEvent>(msg);
514  RemoveWireInternal(data.TargetIDs);
515  break;
516  }
517  case CircuitBoxOpcode.ServerInitialize:
518  {
519  Components.Clear();
520  Wires.Clear();
521  Labels.Clear();
522 
523  var data = INetSerializableStruct.Read<CircuitBoxInitializeStateFromServerEvent>(msg);
524  foreach (var compData in data.Components) { AddComponentFromData(compData); }
525  foreach (var wireData in data.Wires) { AddWireFromData(wireData); }
526 
527  foreach (var labelData in data.Labels)
528  {
529  AddLabelInternal(labelData.ID, labelData.Color, labelData.Position, labelData.Header, labelData.Body);
530  ResizeLabelInternal(labelData.ID, labelData.Position, labelData.Size);
531  }
532 
533  foreach (var node in InputOutputNodes)
534  {
535  node.Position = node.NodeType switch
536  {
537  CircuitBoxInputOutputNode.Type.Input => data.InputPos,
538  CircuitBoxInputOutputNode.Type.Output => data.OutputPos,
539  _ => node.Position
540  };
541  }
542 
543  foreach (var labelOverride in data.LabelOverrides)
544  {
545  RenameConnectionLabelsInternal(labelOverride.Type, labelOverride.Override.ToDictionary());
546  }
547 
548  wasInitializedByServer = true;
549  break;
550  }
551  case CircuitBoxOpcode.RenameLabel:
552  {
553  var data = INetSerializableStruct.Read<CircuitBoxRenameLabelEvent>(msg);
554  RenameLabelInternal(data.LabelId, data.Color, data.NewHeader, data.NewBody);
555  break;
556  }
557  case CircuitBoxOpcode.AddLabel:
558  {
559  var data = INetSerializableStruct.Read<CircuitBoxServerAddLabelEvent>(msg);
560  AddLabelInternal(data.ID, data.Color, data.Position, data.Header, data.Body);
561  ResizeLabelInternal(data.ID, data.Position, data.Size);
562  break;
563  }
564  case CircuitBoxOpcode.RemoveLabel:
565  {
566  var data = INetSerializableStruct.Read<CircuitBoxRemoveLabelEvent>(msg);
567  RemoveLabelInternal(data.TargetIDs);
568  break;
569  }
570  case CircuitBoxOpcode.ResizeLabel:
571  {
572  var data = INetSerializableStruct.Read<CircuitBoxResizeLabelEvent>(msg);
573  ResizeLabelInternal(data.ID, data.Position, data.Size);
574  break;
575  }
576  case CircuitBoxOpcode.RenameConnections:
577  {
578  var data = INetSerializableStruct.Read<CircuitBoxRenameConnectionLabelsEvent>(msg);
579  RenameConnectionLabelsInternal(data.Type, data.Override.ToDictionary());
580  break;
581  }
582  default:
583  throw new ArgumentOutOfRangeException(nameof(header), header, "This opcode cannot be handled using entity events");
584  }
585  }
586 
587  public void AddComponentFromData(CircuitBoxServerCreateComponentEvent data)
588  {
589  if (ItemPrefab.Prefabs.Find(p => p.UintIdentifier == data.UsedResource) is not { } prefab)
590  {
591  throw new Exception($"No item prefab found for \"{data.UsedResource}\"");
592  }
593 
594  AddComponentInternalUnsafe(data.ComponentId, FindItemByID(data.BackingItemId), prefab, data.Position);
595  }
596 
597  public void AddWireFromData(CircuitBoxServerCreateWireEvent data)
598  {
599  var (req, wireId, possibleItemId) = data;
600  var prefab = ItemPrefab.Prefabs.Find(p => p.UintIdentifier == req.SelectedWirePrefabIdentifier);
601  if (prefab is null)
602  {
603  throw new Exception($"No prefab found for \"{req.SelectedWirePrefabIdentifier}\"");
604  }
605 
606  if (!req.Start.FindConnection(this).TryUnwrap(out var start))
607  {
608  throw new Exception($"No connection found for ({req.Start})");
609  }
610 
611  if (!req.End.FindConnection(this).TryUnwrap(out var end))
612  {
613  throw new Exception($"No connection found for ({req.Start})");
614  }
615 
616  if (possibleItemId.TryUnwrap(out var backingItem))
617  {
618  CreateWireWithItem(start, end, wireId, FindItemByID(backingItem));
619  }
620  else
621  {
622  CreateWireWithoutItem(start, end, wireId, prefab);
623  }
624  }
625 
626  public static Item FindItemByID(ushort id)
627  => Entity.FindEntityByID(id) as Item ?? throw new Exception($"No item with ID {id} exists.");
628 
629  public override void AddToGUIUpdateList(int order = 0)
630  {
631  base.AddToGUIUpdateList(order);
632  UI?.AddToGUIUpdateList();
633  }
634  }
635 }
readonly ushort ID
Unique, but non-persistent identifier. Stays the same if the entities are created in the exactly same...
Definition: Entity.cs:43
virtual void ClearChildren()
int GetComponentIndex(ItemComponent component)
void ClientEventWrite(IWriteMessage msg, NetEntityEvent.IData extraData=null)