619 lines
22 KiB
C#
619 lines
22 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using Microsoft.CSharp;
|
|
using System.CodeDom;
|
|
using UnityEngine;
|
|
using System.Security.Cryptography;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using VRC.Udon.Graph;
|
|
using VRC.Udon.Graph.Interfaces;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace VRC.Udon.Editor.ProgramSources.UdonGraphProgram.UI
|
|
{
|
|
public static class UdonGraphExtensions
|
|
{
|
|
private static readonly Dictionary<string, string> FriendlyNameCache;
|
|
|
|
static UdonGraphExtensions()
|
|
{
|
|
FriendlyNameCache = new Dictionary<string, string>();
|
|
StartsWithCache = new Dictionary<(string s, string prefix), bool>();
|
|
}
|
|
|
|
#region Serialization Utilities
|
|
private const byte ZIP_VERSION = 0;
|
|
public static string ZipString(string str)
|
|
{
|
|
using (MemoryStream output = new MemoryStream())
|
|
{
|
|
using (DeflateStream gzip =
|
|
new DeflateStream(output, System.IO.Compression.CompressionLevel.Optimal)) //, CompressionMode.Compress
|
|
{
|
|
using (StreamWriter writer =
|
|
new StreamWriter(gzip, System.Text.Encoding.UTF8))
|
|
{
|
|
writer.Write(str);
|
|
}
|
|
}
|
|
List<byte> outputList = output.ToArray().ToList();
|
|
outputList.Insert(0, ZIP_VERSION); //Version Number
|
|
return Convert.ToBase64String(outputList.ToArray());
|
|
}
|
|
}
|
|
|
|
public static string UnZipString(string input)
|
|
{
|
|
List<byte> inputList = new List<byte>(Convert.FromBase64String(input));
|
|
if (inputList[0] != ZIP_VERSION) //Version Number
|
|
{
|
|
return "";
|
|
}
|
|
inputList.RemoveAt(0);
|
|
using (MemoryStream inputStream = new MemoryStream(inputList.ToArray()))
|
|
{
|
|
using (DeflateStream gzip =
|
|
new DeflateStream(inputStream, CompressionMode.Decompress))
|
|
{
|
|
using (StreamReader reader =
|
|
new StreamReader(gzip, System.Text.Encoding.UTF8))
|
|
{
|
|
return reader.ReadToEnd();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Color Utilities
|
|
|
|
public static class NodeColors
|
|
{
|
|
public static readonly Color Base = new Color(77f / 255f, 157f / 255f, 1);
|
|
public static readonly Color Const = new Color(0.4f, 0.14f, 1f);
|
|
public static readonly Color Variable = new Color(1f, 0.86f, 0.3f);
|
|
public static readonly Color Function = new Color(1f, 0.42f, 0.14f);
|
|
public static readonly Color Event = new Color(0.53f, 1f, 0.3f);
|
|
public static readonly Color Return = new Color(0.89f, 0.2f, 0.2f);
|
|
}
|
|
|
|
private static readonly MD5 _md5Hasher = MD5.Create();
|
|
private static readonly Dictionary<Type, Color> _typeColors = new Dictionary<Type, Color>();
|
|
|
|
public static Color MapTypeToColor(Type type)
|
|
{
|
|
if (type == null)
|
|
{
|
|
return Color.white;
|
|
}
|
|
|
|
if (type.IsPrimitive)
|
|
{
|
|
return new Color(0.12f, 0.53f, 0.9f);
|
|
}
|
|
|
|
if (typeof(UnityEngine.Object).IsAssignableFrom(type))
|
|
{
|
|
return new Color(0.9f, 0.23f, 0.39f);
|
|
}
|
|
|
|
if (type.IsValueType)
|
|
{
|
|
return NodeColors.Variable;
|
|
}
|
|
|
|
if (_typeColors.ContainsKey(type))
|
|
{
|
|
return _typeColors[type];
|
|
}
|
|
|
|
byte[] hashed = _md5Hasher.ComputeHash(type.ToString() == "T"
|
|
? System.Text.Encoding.UTF8.GetBytes("T")
|
|
: System.Text.Encoding.UTF8.GetBytes(type.Name));
|
|
int iValue = BitConverter.ToInt32(hashed, 0);
|
|
|
|
//TODO: Make this provide more varied colors
|
|
Color color = Color.HSVToRGB((iValue & 0xff) / 255f, .69f, 1f);
|
|
|
|
|
|
_typeColors.Add(type, color);
|
|
return color;
|
|
}
|
|
#endregion
|
|
|
|
#region Documentation Utilities
|
|
|
|
public static bool ShouldShowDocumentationLink(UdonNodeDefinition definition)
|
|
{
|
|
List<string> specialNames = new List<string>
|
|
{
|
|
"Block",
|
|
"Branch",
|
|
"For",
|
|
"While",
|
|
"Foreach",
|
|
"Get_Variable",
|
|
"Set_Variable",
|
|
"Event_Custom",
|
|
"Event_OnDataStorageAdded",
|
|
"Event_OnDataStorageChanged",
|
|
"Event_OnDataStorageRemoved",
|
|
"Event_OnDrop",
|
|
"Event_Interact",
|
|
"Event_OnNetworkReady",
|
|
"Event_OnOwnershipTransferred",
|
|
"Event_OnPickup",
|
|
"Event_OnPickupUseDown",
|
|
"Event_OnPickupUseUp",
|
|
"Event_OnPlayerJoined",
|
|
"Event_OnPlayerLeft",
|
|
"Event_OnSpawn",
|
|
"Event_OnStationEntered",
|
|
"Event_OnStationExited",
|
|
"Event_OnVideoEnd",
|
|
"Event_OnVideoPause",
|
|
"Event_OnVideoPlay",
|
|
"Event_OnVideoStart",
|
|
"VRCUdonCommonInterfacesIUdonEventReceiver.__SendCustomEvent__SystemString__SystemVoid",
|
|
"VRCUdonCommonInterfacesIUdonEventReceiver.__SetHeapVariable__SystemString_SystemObject__SystemVoid",
|
|
"VRCUdonCommonInterfacesIUdonEventReceiver.__GetHeapVariable__SystemString__SystemObject",
|
|
"Const_VRCUdonCommonInterfacesIUdonEventReceiver",
|
|
};
|
|
|
|
// Don't show for any of these
|
|
return !(definition.type == null ||
|
|
definition.type.Namespace == null ||
|
|
specialNames.Contains(definition.fullName) ||
|
|
(!definition.type.Namespace.Contains("UnityEngine") &&
|
|
!definition.type.Namespace.Contains("System")));
|
|
}
|
|
|
|
public static string GetDocumentationLink(UdonNodeDefinition definition)
|
|
{
|
|
if (definition.fullName.StartsWithCached("Event_"))
|
|
{
|
|
string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/MonoBehaviour.";
|
|
url += definition.name;
|
|
url += ".html";
|
|
return url;
|
|
}
|
|
|
|
if (definition.fullName.Contains("Array.__ctor"))
|
|
{
|
|
//I couldn't find the array constructor documentation
|
|
return "https://docs.microsoft.com/en-us/dotnet/api/system.array?view=netframework-4.8";
|
|
}
|
|
|
|
if (definition.fullName.Contains("Array.__Get"))
|
|
{
|
|
return "https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#indexer-operator-";
|
|
}
|
|
|
|
if (definition.fullName.Contains(".__Equals__SystemObject"))
|
|
{
|
|
return "https://docs.microsoft.com/en-us/dotnet/api/system.object.equals?view=netframework-4.8";
|
|
}
|
|
|
|
if (definition.name.Contains("[]"))
|
|
{
|
|
string url = "https://docs.microsoft.com/en-us/dotnet/api/system.array.";
|
|
url += definition.name.Split(' ')[1];
|
|
url += "?view=netframework-4.8";
|
|
return url;
|
|
}
|
|
|
|
if (definition.type.Namespace.Contains("UnityEngine"))
|
|
{
|
|
string url = "https://docs.unity3d.com/2018.4/Documentation/ScriptReference/";
|
|
if (definition.type.Namespace != "UnityEngine")
|
|
{
|
|
url += definition.type.Namespace.Replace("UnityEngine.", "");
|
|
url += ".";
|
|
}
|
|
url += definition.type.Name;
|
|
|
|
if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_"))
|
|
{
|
|
if (definition.fullName.Contains("__get_"))
|
|
{
|
|
url += "-" + definition.name.Split(new[] { "get_" }, StringSplitOptions.None)[1];
|
|
}
|
|
else
|
|
{
|
|
url += "-" + definition.name.Split(new[] { "set_" }, StringSplitOptions.None)[1];
|
|
}
|
|
|
|
url += ".html";
|
|
return url;
|
|
}
|
|
|
|
if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_") || definition.fullName.Contains("Variable_"))
|
|
{
|
|
url += ".html";
|
|
return url;
|
|
}
|
|
|
|
{
|
|
// Methods
|
|
url += "." + definition.name.Split(' ')[1];
|
|
url += ".html";
|
|
return url;
|
|
}
|
|
}
|
|
|
|
if (definition.type.Namespace.Contains("System"))
|
|
{
|
|
string url = "https://docs.microsoft.com/en-us/dotnet/api/system.";
|
|
url += definition.type.Name;
|
|
if (definition.fullName.Contains("__get_") || definition.fullName.Contains("__set_"))
|
|
{
|
|
url += "." + definition.name.Split(' ')[1].Replace("get_", "").Replace("set_", "");
|
|
url += "?view=netframework-4.8";
|
|
return url;
|
|
}
|
|
|
|
if (definition.name == "ctor")
|
|
{
|
|
url += ".-ctor";
|
|
url += "?view=netframework-4.8#System_";
|
|
url += definition.type.Name + "__ctor_";
|
|
foreach (var pType in definition.Inputs)
|
|
{
|
|
url += "System_" + pType.type.Name.Replace('[', '_').Replace(']', '_') + "_";
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
if (definition.fullName.Contains("Const_") || definition.fullName.Contains("Type_"))
|
|
{
|
|
url += "?view=netframework-4.8";
|
|
return url;
|
|
}
|
|
|
|
{
|
|
// Methods
|
|
// not entirely sure what case this catches, but we were always doing the split before, and it was breaking if we didn't have . in the name.
|
|
if (definition.name.Contains('.'))
|
|
{
|
|
url += "." + definition.name.Split(' ')[1];
|
|
}
|
|
url += "?view=netframework-4.8";
|
|
return url;
|
|
}
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
#endregion
|
|
|
|
public static string FriendlyNameify(this string typeString)
|
|
{
|
|
if (typeString == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (FriendlyNameCache.ContainsKey(typeString))
|
|
{
|
|
return FriendlyNameCache[typeString];
|
|
}
|
|
string originalString = typeString;
|
|
typeString = typeString.Replace("Single", "float");
|
|
typeString = typeString.Replace("Int32", "int");
|
|
typeString = typeString.Replace("String", "string");
|
|
typeString = typeString.Replace("VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
|
|
typeString = typeString.Replace("UdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
|
|
typeString = typeString.Replace("IUdonEventReceiver", "UdonBehaviour");
|
|
typeString = typeString.Replace("Const_VRCUdonCommonInterfacesIUdonEventReceiver", "UdonBehaviour");
|
|
typeString = typeString.Replace("Array", "[]");
|
|
typeString = typeString.Replace("SDK3VideoComponentsBaseBase", "");
|
|
typeString = typeString.Replace("SDKBase", "");
|
|
typeString = typeString.Replace("SDK3Components", "");
|
|
typeString = typeString.Replace("VRCVRC", "VRC");
|
|
typeString = typeString.Replace("TMPro", "");
|
|
// ReSharper disable once StringLiteralTypo
|
|
if (typeString.Replace("ector", "").Contains("ctor")) //Handle "Vector/vector"
|
|
{
|
|
typeString = typeString.ReplaceLast("ctor", "constructor");
|
|
}
|
|
|
|
if (typeString == "IUdonEventReceiver")
|
|
{
|
|
typeString = "UdonBehaviour";
|
|
}
|
|
FriendlyNameCache.Add(originalString, typeString);
|
|
return typeString;
|
|
}
|
|
|
|
private static readonly Dictionary<(string s, string prefix), bool> StartsWithCache;
|
|
public static bool StartsWithCached(this string s, string prefix)
|
|
{
|
|
if (StartsWithCache.ContainsKey((s, prefix)))
|
|
{
|
|
return StartsWithCache[(s, prefix)];
|
|
}
|
|
bool doesStartWith = s.StartsWith(prefix);
|
|
StartsWithCache.Add((s, prefix), doesStartWith);
|
|
return doesStartWith;
|
|
}
|
|
|
|
public static string UppercaseFirst(this string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
char[] a = s.ToCharArray();
|
|
a[0] = char.ToUpper(a[0]);
|
|
return new string(a);
|
|
}
|
|
|
|
public static string ReplaceFirst(this string text, string search, string replace)
|
|
{
|
|
int pos = text.IndexOf(search, StringComparison.Ordinal);
|
|
if (pos < 0)
|
|
{
|
|
return text;
|
|
}
|
|
return text.Substring(0, pos) + replace + text.Substring(pos + search.Length);
|
|
}
|
|
|
|
public static Type SlotTypeConverter(Type type, string fullName)
|
|
{
|
|
if (type == null)
|
|
{
|
|
return typeof(object);
|
|
}
|
|
|
|
if (fullName.Contains("IUdonEventReceiver") && type == typeof(UnityEngine.Object))
|
|
{
|
|
return typeof(UdonBehaviour);
|
|
}
|
|
|
|
return type;
|
|
}
|
|
|
|
public static string FriendlyTypeName(Type t)
|
|
{
|
|
if (t == null)
|
|
{
|
|
return "Flow";
|
|
}
|
|
|
|
if (!t.IsPrimitive)
|
|
{
|
|
if (t == typeof(UnityEngine.Object))
|
|
{
|
|
return "Unity Object";
|
|
}
|
|
return t.Name;
|
|
}
|
|
using (CSharpCodeProvider provider = new CSharpCodeProvider())
|
|
{
|
|
CodeTypeReference typeRef = new CodeTypeReference(t);
|
|
return provider.GetTypeOutput(typeRef);
|
|
}
|
|
}
|
|
|
|
public static string ReplaceLast(this string source, string find, string replace)
|
|
{
|
|
int place = source.LastIndexOf(find, StringComparison.Ordinal);
|
|
|
|
if (place == -1)
|
|
return source;
|
|
|
|
string result = source.Remove(place, find.Length).Insert(place, replace);
|
|
return result;
|
|
}
|
|
|
|
public static string PrettyBaseName(string baseIdentifier)
|
|
{
|
|
string result = baseIdentifier.Replace("UnityEngine", "").Replace("System", "");
|
|
string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None);
|
|
if (resultSplit.Length >= 2)
|
|
{
|
|
result = $"{resultSplit[0]}{resultSplit[1]}";
|
|
}
|
|
result = result.FriendlyNameify();
|
|
result = result.Replace("op_", "");
|
|
result = result.Replace("_", " ");
|
|
return result;
|
|
}
|
|
|
|
public static string PrettyString(string s)
|
|
{
|
|
switch (s)
|
|
{
|
|
case "op_Equality":
|
|
s = "==";
|
|
break;
|
|
|
|
case "op_Inequality":
|
|
s = "!=";
|
|
break;
|
|
|
|
case "op_Addition":
|
|
s = "+";
|
|
break;
|
|
case "VRCUdonCommonInterfacesIUdonEventReceiver":
|
|
s = "UdonBehaviour";
|
|
break;
|
|
// ReSharper disable once RedundantEmptySwitchSection
|
|
default:
|
|
break;
|
|
}
|
|
return s;
|
|
}
|
|
|
|
public static string ParseByCase(string strInput)
|
|
{
|
|
string strOutput = "";
|
|
int intCurrentCharPos = 0;
|
|
int intLastCharPos = strInput.Length - 1;
|
|
for (intCurrentCharPos = 0; intCurrentCharPos <= intLastCharPos; intCurrentCharPos++)
|
|
{
|
|
char chrCurrentInputChar = strInput[intCurrentCharPos];
|
|
char chrPreviousInputChar = chrCurrentInputChar;
|
|
if (intCurrentCharPos > 0)
|
|
{
|
|
chrPreviousInputChar = strInput[intCurrentCharPos - 1];
|
|
}
|
|
|
|
if (char.IsUpper(chrCurrentInputChar) && char.IsLower(chrPreviousInputChar))
|
|
{
|
|
strOutput += " ";
|
|
}
|
|
|
|
strOutput += chrCurrentInputChar;
|
|
}
|
|
|
|
return strOutput;
|
|
}
|
|
|
|
public static string PrettyFullName(Graph.UdonNodeDefinition nodeDefinition, bool keepLong = false)
|
|
{
|
|
string fullName = nodeDefinition.fullName;
|
|
string result;
|
|
if (keepLong)
|
|
{
|
|
result = fullName.Replace("UnityEngine", "UnityEngine.").Replace("System", "System.");
|
|
}
|
|
else
|
|
{
|
|
result = fullName.Replace("UnityEngine", "").Replace("System", "");
|
|
}
|
|
|
|
string[] resultSplit = result.Split(new[] { "__" }, StringSplitOptions.None);
|
|
if (resultSplit.Length >= 3)
|
|
{
|
|
string outName = "";
|
|
if (nodeDefinition.type != typeof(void))
|
|
{
|
|
if (nodeDefinition.Outputs.Count > 0)
|
|
{
|
|
outName = string.Join(", ", nodeDefinition.Outputs.Select(o => o.name));
|
|
}
|
|
}
|
|
|
|
result = nodeDefinition.Inputs.Count > 0
|
|
? $"{resultSplit[0]}{resultSplit[1]}({string.Join(", ", nodeDefinition.Inputs.Select(s => s.name))}{outName})"
|
|
: $"{resultSplit[0]}{resultSplit[1]}({resultSplit[2].Replace("_", ", ")}{outName})";
|
|
}
|
|
else if (resultSplit.Length >= 2)
|
|
{
|
|
result = $"{resultSplit[0]}{resultSplit[1]}()";
|
|
}
|
|
|
|
if (!keepLong)
|
|
{
|
|
result = result.FriendlyNameify();
|
|
result = result.Replace("op_", "");
|
|
result = result.Replace("_", " ");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public static string GetSimpleNameForRegistry(Graph.Interfaces.INodeRegistry registry)
|
|
{
|
|
string registryName = registry.ToString().Replace("NodeRegistry", "").FriendlyNameify();
|
|
registryName = registryName.Substring(registryName.LastIndexOf(".") + 1);
|
|
registryName = registryName.Replace("UnityEngine", "");
|
|
return registryName;
|
|
}
|
|
|
|
private static Dictionary<UdonNodeDefinition, INodeRegistry> _definitionToRegistryLookup;
|
|
public static INodeRegistry GetRegistryForDefinition(UdonNodeDefinition definition)
|
|
{
|
|
// Create lookup if needed
|
|
if(_definitionToRegistryLookup == null)
|
|
{
|
|
_definitionToRegistryLookup = new Dictionary<UdonNodeDefinition, INodeRegistry>();
|
|
|
|
foreach (var registry in UdonEditorManager.Instance.GetNodeRegistries())
|
|
{
|
|
foreach (var nodeDefinition in registry.Value.GetNodeDefinitions())
|
|
{
|
|
_definitionToRegistryLookup.Add(nodeDefinition, registry.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return found Registry or Null
|
|
if (_definitionToRegistryLookup.ContainsKey(definition))
|
|
{
|
|
return _definitionToRegistryLookup[definition];
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static string ToLowerFirstChar(this string input)
|
|
{
|
|
string newString = input;
|
|
if (!String.IsNullOrEmpty(newString) && Char.IsUpper(newString[0]))
|
|
newString = Char.ToLower(newString[0]) + newString.Substring(1);
|
|
return newString;
|
|
}
|
|
|
|
public static string SanitizeVariableName(this string result)
|
|
{
|
|
result = result.Replace(" ", "");
|
|
if (char.IsNumber(result[0]))
|
|
{
|
|
result = $"A{result}";
|
|
}
|
|
Regex rgx = new Regex("[^a-zA-Z0-9 _]");
|
|
result = rgx.Replace(result, "");
|
|
return result;
|
|
}
|
|
|
|
public static char EscapeLikeALiteral(string src)
|
|
{
|
|
switch (src)
|
|
{
|
|
//case "\\'": return '\'';
|
|
//case "\\"": return '\"';
|
|
case "\\0": return '\0';
|
|
case "\\a": return '\a';
|
|
case "\\b": return '\b';
|
|
case "\\f": return '\f';
|
|
case "\\n": return '\n';
|
|
case "\\r": return '\r';
|
|
case "\\t": return '\t';
|
|
case "\\v": return '\v';
|
|
case "\\": return '\\';
|
|
default:
|
|
throw new InvalidOperationException($"src was {src}");
|
|
}
|
|
}
|
|
|
|
public static string UnescapeLikeALiteral(char src)
|
|
{
|
|
switch (src)
|
|
{
|
|
//case "\\'": return '\'';
|
|
//case "\\"": return '\"';
|
|
case '\0': return "\\0";
|
|
case '\a': return "\\a";
|
|
case '\b': return "\\b";
|
|
case '\f': return "\\f";
|
|
case '\n': return "\\n";
|
|
case '\r': return "\\r";
|
|
case '\t': return "\\t";
|
|
case '\v': return "\\v";
|
|
//case '\\': return "\\";
|
|
default:
|
|
return src.ToString();
|
|
}
|
|
}
|
|
}
|
|
}
|