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}