A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using UnityEditor;
5using UnityEngine;
6
7namespace Unity.VisualScripting
8{
9 [FuzzyOption(typeof(IUnit))]
10 public class UnitOption<TUnit> : IUnitOption where TUnit : IUnit
11 {
12 public UnitOption()
13 {
14 sourceScriptGuids = new HashSet<string>();
15 }
16
17 public UnitOption(TUnit unit) : this()
18 {
19 this.unit = unit;
20
21 FillFromUnit();
22 }
23
24 [DoNotSerialize]
25 protected bool filled { get; private set; }
26
27 private TUnit _unit;
28
29 protected UnitOptionRow source { get; private set; }
30
31 public TUnit unit
32 {
33 get
34 {
35 // Load the node on demand to avoid deserialization overhead
36 // Deserializing the entire database takes many seconds,
37 // which is the reason why UnitOptionRow and SQLite are used
38 // in the first place.
39
40 if (_unit == null)
41 {
42 _unit = (TUnit)new SerializationData(source.unit).Deserialize();
43 }
44
45 return _unit;
46 }
47 protected set
48 {
49 _unit = value;
50 }
51 }
52
53 IUnit IUnitOption.unit => unit;
54
55 public Type unitType { get; private set; }
56
57 protected IUnitDescriptor descriptor => unit.Descriptor<IUnitDescriptor>();
58
59 // Avoid using the descriptions for each option, because we don't need all fields described until the option is hovered
60
61 protected UnitDescription description => unit.Description<UnitDescription>();
62
63 protected UnitPortDescription PortDescription(IUnitPort port)
64 {
65 return port.Description<UnitPortDescription>();
66 }
67
68 public virtual IUnit InstantiateUnit()
69 {
70 var instance = unit.CloneViaSerialization();
71 instance.Define();
72 return instance;
73 }
74
75 void IUnitOption.PreconfigureUnit(IUnit unit)
76 {
77 PreconfigureUnit((TUnit)unit);
78 }
79
80 public virtual void PreconfigureUnit(TUnit unit)
81 {
82 }
83
84 protected virtual void FillFromUnit()
85 {
86 unit.EnsureDefined();
87 unitType = unit.GetType();
88
89 labelHuman = Label(true);
90 haystackHuman = Haystack(true);
91
92 labelProgrammer = Label(false);
93 haystackProgrammer = Haystack(false);
94
95 category = Category();
96 order = Order();
97 favoriteKey = FavoriteKey();
98 UnityAPI.Async(() => icon = Icon());
99
100 showControlInputsInFooter = ShowControlInputsInFooter();
101 showControlOutputsInFooter = ShowControlOutputsInFooter();
102 showValueInputsInFooter = ShowValueInputsInFooter();
103 showValueOutputsInFooter = ShowValueOutputsInFooter();
104
105 controlInputCount = unit.controlInputs.Count;
106 controlOutputCount = unit.controlOutputs.Count;
107 valueInputTypes = unit.valueInputs.Select(vi => vi.type).ToHashSet();
108 valueOutputTypes = unit.valueOutputs.Select(vo => vo.type).ToHashSet();
109
110 filled = true;
111 }
112
113 protected virtual void FillFromData()
114 {
115 unit.EnsureDefined();
116 unitType = unit.GetType();
117 UnityAPI.Async(() => icon = Icon());
118
119 showControlInputsInFooter = ShowControlInputsInFooter();
120 showControlOutputsInFooter = ShowControlOutputsInFooter();
121 showValueInputsInFooter = ShowValueInputsInFooter();
122 showValueOutputsInFooter = ShowValueOutputsInFooter();
123
124 filled = true;
125 }
126
127 public virtual void Deserialize(UnitOptionRow row)
128 {
129 source = row;
130
131 if (row.sourceScriptGuids != null)
132 {
133 sourceScriptGuids = row.sourceScriptGuids.Split(',').ToHashSet();
134 }
135
136 unitType = Codebase.DeserializeType(row.unitType);
137
138 category = row.category == null ? null : new UnitCategory(row.category);
139 labelHuman = row.labelHuman;
140 labelProgrammer = row.labelProgrammer;
141 order = row.order;
142 haystackHuman = row.haystackHuman;
143 haystackProgrammer = row.haystackProgrammer;
144 favoriteKey = row.favoriteKey;
145
146 controlInputCount = row.controlInputCount;
147 controlOutputCount = row.controlOutputCount;
148 }
149
150 public virtual UnitOptionRow Serialize()
151 {
152 var row = new UnitOptionRow();
153
154 if (sourceScriptGuids.Count == 0)
155 {
156 // Important to set to null here, because the code relies on
157 // null checks, not empty string checks.
158 row.sourceScriptGuids = null;
159 }
160 else
161 {
162 row.sourceScriptGuids = string.Join(",", sourceScriptGuids.ToArray());
163 }
164
165 row.optionType = Codebase.SerializeType(GetType());
166 row.unitType = Codebase.SerializeType(unitType);
167 row.unit = unit.Serialize().json;
168
169 row.category = category?.fullName;
170 row.labelHuman = labelHuman;
171 row.labelProgrammer = labelProgrammer;
172 row.order = order;
173 row.haystackHuman = haystackHuman;
174 row.haystackProgrammer = haystackProgrammer;
175 row.favoriteKey = favoriteKey;
176
177 row.controlInputCount = controlInputCount;
178 row.controlOutputCount = controlOutputCount;
179 row.valueInputTypes = valueInputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty();
180 row.valueOutputTypes = valueOutputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty();
181
182 return row;
183 }
184
185 public virtual void OnPopulate()
186 {
187 if (!filled)
188 {
189 FillFromData();
190 }
191 }
192
193 public virtual void Prewarm() { }
194
195
196 #region Configuration
197
198 public object value => this;
199
200 public bool parentOnly => false;
201
202 public virtual string headerLabel => label;
203
204 public virtual bool showHeaderIcon => false;
205
206 public virtual bool favoritable => true;
207
208 #endregion
209
210
211 #region Properties
212
213 public HashSet<string> sourceScriptGuids { get; protected set; }
214
215 protected string labelHuman { get; set; }
216
217 protected string labelProgrammer { get; set; }
218
219 public string label => BoltCore.Configuration.humanNaming ? labelHuman : labelProgrammer;
220
221 public UnitCategory category { get; private set; }
222
223 public int order { get; private set; }
224
225 public EditorTexture icon { get; private set; }
226
227 protected string haystackHuman { get; set; }
228
229 protected string haystackProgrammer { get; set; }
230
231 public string haystack => BoltCore.Configuration.humanNaming ? haystackHuman : haystackProgrammer;
232
233 public string favoriteKey { get; private set; }
234
235 public virtual string formerHaystack => BoltFlowNameUtility.UnitPreviousTitle(unitType);
236
237 GUIStyle IFuzzyOption.style => Style();
238
239 #endregion
240
241
242 #region Contextual Filtering
243
244 public int controlInputCount { get; private set; }
245
246 public int controlOutputCount { get; private set; }
247
248 private HashSet<Type> _valueInputTypes;
249
250 private HashSet<Type> _valueOutputTypes;
251
252 // On demand loading for initialization performance (type deserialization is expensive)
253
254 public HashSet<Type> valueInputTypes
255 {
256 get
257 {
258 if (_valueInputTypes == null)
259 {
260 if (string.IsNullOrEmpty(source.valueInputTypes))
261 {
262 _valueInputTypes = new HashSet<Type>();
263 }
264 else
265 {
266 _valueInputTypes = source.valueInputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet();
267 }
268 }
269
270 return _valueInputTypes;
271 }
272 private set
273 {
274 _valueInputTypes = value;
275 }
276 }
277
278 public HashSet<Type> valueOutputTypes
279 {
280 get
281 {
282 if (_valueOutputTypes == null)
283 {
284 if (string.IsNullOrEmpty(source.valueOutputTypes))
285 {
286 _valueOutputTypes = new HashSet<Type>();
287 }
288 else
289 {
290 _valueOutputTypes = source.valueOutputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet();
291 }
292 }
293
294 return _valueOutputTypes;
295 }
296 private set
297 {
298 _valueOutputTypes = value;
299 }
300 }
301
302 #endregion
303
304
305 #region Providers
306
307 protected virtual string Label(bool human)
308 {
309 return BoltFlowNameUtility.UnitTitle(unitType, false, true);
310 }
311
312 protected virtual UnitCategory Category()
313 {
314 return unitType.GetAttribute<UnitCategory>();
315 }
316
317 protected virtual int Order()
318 {
319 return unitType.GetAttribute<UnitOrderAttribute>()?.order ?? int.MaxValue;
320 }
321
322 protected virtual string Haystack(bool human)
323 {
324 return Label(human);
325 }
326
327 protected virtual EditorTexture Icon()
328 {
329 return descriptor.Icon();
330 }
331
332 protected virtual GUIStyle Style()
333 {
334 return FuzzyWindow.defaultOptionStyle;
335 }
336
337 protected virtual string FavoriteKey()
338 {
339 return unit.GetType().FullName;
340 }
341
342 #endregion
343
344
345 #region Search
346
347 public virtual string SearchResultLabel(string query)
348 {
349 var label = SearchUtility.HighlightQuery(haystack, query);
350
351 if (category != null)
352 {
353 label += $" <color=#{ColorPalette.unityForegroundDim.ToHexString()}>(in {category.fullName})</color>";
354 }
355
356 return label;
357 }
358
359 #endregion
360
361
362 #region Footer
363
364 private string summary => description.summary;
365
366 public bool hasFooter => !StringUtility.IsNullOrWhiteSpace(summary) || footerPorts.Any();
367
368 protected virtual bool ShowControlInputsInFooter()
369 {
370 return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ControlInputs ?? false;
371 }
372
373 protected virtual bool ShowControlOutputsInFooter()
374 {
375 return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ControlOutputs ?? false;
376 }
377
378 protected virtual bool ShowValueInputsInFooter()
379 {
380 return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ValueInputs ?? true;
381 }
382
383 protected virtual bool ShowValueOutputsInFooter()
384 {
385 return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ValueOutputs ?? true;
386 }
387
388 [DoNotSerialize]
389 protected bool showControlInputsInFooter { get; private set; }
390
391 [DoNotSerialize]
392 protected bool showControlOutputsInFooter { get; private set; }
393
394 [DoNotSerialize]
395 protected bool showValueInputsInFooter { get; private set; }
396
397 [DoNotSerialize]
398 protected bool showValueOutputsInFooter { get; private set; }
399
400 private IEnumerable<IUnitPort> footerPorts
401 {
402 get
403 {
404 if (showControlInputsInFooter)
405 {
406 foreach (var controlInput in unit.controlInputs)
407 {
408 yield return controlInput;
409 }
410 }
411
412 if (showControlOutputsInFooter)
413 {
414 foreach (var controlOutput in unit.controlOutputs)
415 {
416 yield return controlOutput;
417 }
418 }
419
420 if (showValueInputsInFooter)
421 {
422 foreach (var valueInput in unit.valueInputs)
423 {
424 yield return valueInput;
425 }
426 }
427
428 if (showValueOutputsInFooter)
429 {
430 foreach (var valueOutput in unit.valueOutputs)
431 {
432 yield return valueOutput;
433 }
434 }
435 }
436 }
437
438 public float GetFooterHeight(float width)
439 {
440 var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary);
441 var hasIcon = icon != null;
442 var hasPorts = footerPorts.Any();
443
444 var height = 0f;
445
446 width -= 2 * FooterStyles.padding;
447
448 height += FooterStyles.padding;
449
450 if (hasSummary)
451 {
452 if (hasIcon)
453 {
454 height += Mathf.Max(FooterStyles.unitIconSize, GetFooterSummaryHeight(width - FooterStyles.unitIconSize - FooterStyles.spaceAfterUnitIcon));
455 }
456 else
457 {
458 height += GetFooterSummaryHeight(width);
459 }
460 }
461
462 if (hasSummary && hasPorts)
463 {
464 height += FooterStyles.spaceBetweenDescriptionAndPorts;
465 }
466
467 foreach (var port in footerPorts)
468 {
469 height += GetFooterPortHeight(width, port);
470 height += FooterStyles.spaceBetweenPorts;
471 }
472
473 if (hasPorts)
474 {
475 height -= FooterStyles.spaceBetweenPorts;
476 }
477
478 height += FooterStyles.padding;
479
480 return height;
481 }
482
483 public void OnFooterGUI(Rect position)
484 {
485 var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary);
486 var hasIcon = icon != null;
487 var hasPorts = footerPorts.Any();
488
489 var y = position.y;
490
491 y += FooterStyles.padding;
492
493 position.x += FooterStyles.padding;
494 position.width -= FooterStyles.padding * 2;
495
496 if (hasSummary)
497 {
498 if (hasIcon)
499 {
500 var iconPosition = new Rect
501 (
502 position.x,
503 y,
504 FooterStyles.unitIconSize,
505 FooterStyles.unitIconSize
506 );
507
508 var summaryWidth = position.width - iconPosition.width - FooterStyles.spaceAfterUnitIcon;
509
510 var summaryPosition = new Rect
511 (
512 iconPosition.xMax + FooterStyles.spaceAfterUnitIcon,
513 y,
514 summaryWidth,
515 GetFooterSummaryHeight(summaryWidth)
516 );
517
518 GUI.DrawTexture(iconPosition, icon?[FooterStyles.unitIconSize]);
519
520 OnFooterSummaryGUI(summaryPosition);
521
522 y = Mathf.Max(iconPosition.yMax, summaryPosition.yMax);
523 }
524 else
525 {
526 OnFooterSummaryGUI(position.VerticalSection(ref y, GetFooterSummaryHeight(position.width)));
527 }
528 }
529
530 if (hasSummary && hasPorts)
531 {
532 y += FooterStyles.spaceBetweenDescriptionAndPorts;
533 }
534
535 foreach (var port in footerPorts)
536 {
537 OnFooterPortGUI(position.VerticalSection(ref y, GetFooterPortHeight(position.width, port)), port);
538 y += FooterStyles.spaceBetweenPorts;
539 }
540
541 if (hasPorts)
542 {
543 y -= FooterStyles.spaceBetweenPorts;
544 }
545
546 y += FooterStyles.padding;
547 }
548
549 private float GetFooterSummaryHeight(float width)
550 {
551 return FooterStyles.description.CalcHeight(new GUIContent(summary), width);
552 }
553
554 private void OnFooterSummaryGUI(Rect position)
555 {
556 EditorGUI.LabelField(position, summary, FooterStyles.description);
557 }
558
559 private string GetFooterPortLabel(IUnitPort port)
560 {
561 string type;
562
563 if (port is ValueInput)
564 {
565 type = ((IUnitValuePort)port).type.DisplayName() + " Input";
566 }
567 else if (port is ValueOutput)
568 {
569 type = ((IUnitValuePort)port).type.DisplayName() + " Output";
570 }
571 else if (port is ControlInput)
572 {
573 type = "Trigger Input";
574 }
575 else if (port is ControlOutput)
576 {
577 type = "Trigger Output";
578 }
579 else
580 {
581 throw new NotSupportedException();
582 }
583
584 var portDescription = PortDescription(port);
585
586 if (!StringUtility.IsNullOrWhiteSpace(portDescription.summary))
587 {
588 return $"<b>{portDescription.label}:</b> {portDescription.summary} {LudiqGUIUtility.DimString($"({type})")}";
589 }
590 else
591 {
592 return $"<b>{portDescription.label}:</b> {LudiqGUIUtility.DimString($"({type})")}";
593 }
594 }
595
596 private float GetFooterPortDescriptionHeight(float width, IUnitPort port)
597 {
598 return FooterStyles.portDescription.CalcHeight(new GUIContent(GetFooterPortLabel(port)), width);
599 }
600
601 private void OnFooterPortDescriptionGUI(Rect position, IUnitPort port)
602 {
603 GUI.Label(position, GetFooterPortLabel(port), FooterStyles.portDescription);
604 }
605
606 private float GetFooterPortHeight(float width, IUnitPort port)
607 {
608 var descriptionWidth = width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon;
609
610 return GetFooterPortDescriptionHeight(descriptionWidth, port);
611 }
612
613 private void OnFooterPortGUI(Rect position, IUnitPort port)
614 {
615 var iconPosition = new Rect
616 (
617 position.x,
618 position.y,
619 FooterStyles.portIconSize,
620 FooterStyles.portIconSize
621 );
622
623 var descriptionWidth = position.width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon;
624
625 var descriptionPosition = new Rect
626 (
627 iconPosition.xMax + FooterStyles.spaceAfterPortIcon,
628 position.y,
629 descriptionWidth,
630 GetFooterPortDescriptionHeight(descriptionWidth, port)
631 );
632
633 var portDescription = PortDescription(port);
634
635 var icon = portDescription.icon?[FooterStyles.portIconSize];
636
637 if (icon != null)
638 {
639 GUI.DrawTexture(iconPosition, icon);
640 }
641
642 OnFooterPortDescriptionGUI(descriptionPosition, port);
643 }
644
645 public static class FooterStyles
646 {
647 static FooterStyles()
648 {
649 description = new GUIStyle(EditorStyles.label);
650 description.padding = new RectOffset(0, 0, 0, 0);
651 description.wordWrap = true;
652 description.richText = true;
653
654 portDescription = new GUIStyle(EditorStyles.label);
655 portDescription.padding = new RectOffset(0, 0, 0, 0);
656 portDescription.wordWrap = true;
657 portDescription.richText = true;
658 portDescription.imagePosition = ImagePosition.TextOnly;
659 }
660
661 public static readonly GUIStyle description;
662 public static readonly GUIStyle portDescription;
663 public static readonly float spaceAfterUnitIcon = 7;
664 public static readonly int unitIconSize = IconSize.Medium;
665 public static readonly float spaceAfterPortIcon = 6;
666 public static readonly int portIconSize = IconSize.Small;
667 public static readonly float spaceBetweenDescriptionAndPorts = 8;
668 public static readonly float spaceBetweenPorts = 8;
669 public static readonly float padding = 8;
670 }
671
672 #endregion
673 }
674}