Files
Silly-Home/Packages/com.vrchat.worlds/Integrations/UdonSharp/Editor/UdonSharpUtils.cs
2026-06-07 16:58:24 +01:00

566 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using UdonSharp.Compiler.Udon;
using UnityEditor;
using UnityEngine;
using VRC.Udon;
namespace UdonSharp
{
internal static class UdonSharpUtils
{
// https://stackoverflow.com/questions/6386202/get-type-name-without-any-generics-info
public static string GetNameWithoutGenericArity(this System.Type t)
{
if (!t.IsGenericType)
return t.Name;
string name = t.Name;
int index = name.IndexOf('`');
return index == -1 ? name : name.Substring(0, index);
}
private static readonly HashSet<System.Type> _unsignedTypes = new HashSet<System.Type>()
{
typeof(byte),
typeof(ushort),
typeof(uint),
typeof(ulong),
};
private static readonly HashSet<System.Type> _signedTypes = new HashSet<System.Type>()
{
typeof(sbyte),
typeof(short),
typeof(int),
typeof(long),
};
private static readonly HashSet<System.Type> _integerTypes = new HashSet<System.Type>()
{
typeof(byte),
typeof(sbyte),
typeof(short),
typeof(ushort),
typeof(int),
typeof(uint),
typeof(long),
typeof(ulong),
};
private static readonly HashSet<System.Type> _floatTypes = new HashSet<System.Type>()
{
typeof(float),
typeof(double),
typeof(decimal),
};
public static bool IsSignedType(System.Type type)
{
return _signedTypes.Contains(type);
}
public static bool IsUnsignedType(System.Type type)
{
return _unsignedTypes.Contains(type);
}
public static bool IsIntegerType(System.Type type)
{
return _integerTypes.Contains(type);
}
public static bool IsFloatType(System.Type type)
{
return _floatTypes.Contains(type);
}
public static bool IsNumericType(System.Type type)
{
return IsIntegerType(type) || IsFloatType(type);
}
public static bool IsExplicitlyAssignableFrom(this System.Type targetType, System.Type assignee)
{
// Normal explicit assign
if (targetType.IsAssignableFrom(assignee))
return true;
// Numeric conversions
if (IsNumericType(targetType) && IsNumericType(assignee))
return true;
// Handle user-defined implicit conversion operators defined on both sides
// Roughly follows https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions#processing-of-user-defined-implicit-conversions
// I doubt I'll ever deal with properly supporting nullable but ¯\_(ツ)_/¯
if (System.Nullable.GetUnderlyingType(targetType) != null)
targetType = System.Nullable.GetUnderlyingType(targetType);
if (System.Nullable.GetUnderlyingType(assignee) != null)
assignee = System.Nullable.GetUnderlyingType(assignee);
List<System.Type> operatorTypes = new List<System.Type>();
operatorTypes.Add(targetType);
System.Type currentSourceType = assignee;
while (currentSourceType != null)
{
operatorTypes.Add(currentSourceType);
currentSourceType = currentSourceType.BaseType;
}
foreach (System.Type operatorType in operatorTypes)
{
IEnumerable<MethodInfo> methods = operatorType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(e => e.Name == "op_Implicit");
foreach (MethodInfo methodInfo in methods)
{
if (methodInfo.ReturnType == targetType && (methodInfo.GetParameters()[0].ParameterType == assignee || methodInfo.GetParameters()[0].ParameterType == typeof(UnityEngine.Object)))
return true;
}
}
foreach (System.Type operatorType in operatorTypes)
{
IEnumerable<MethodInfo> methods = operatorType.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(e => e.Name == "op_Explicit");
foreach (MethodInfo methodInfo in methods)
{
if (methodInfo.ReturnType == targetType && (methodInfo.GetParameters()[0].ParameterType == assignee || methodInfo.GetParameters()[0].ParameterType == typeof(UnityEngine.Object)))
return true;
}
}
return false;
}
/// <summary>
/// Escapes property names into something Udon allows since C# will put '<' and '>' in property backing field names
/// </summary>
internal static string UnmanglePropertyFieldName(string propertyName)
{
return propertyName?.Replace('<', '_').Replace('>', '_');
}
public static bool IsUserDefinedBehaviour(System.Type type)
{
return type == typeof(UdonSharpBehaviour) ||
type.IsSubclassOf(typeof(UdonSharpBehaviour)) ||
(type.IsArray && (type.GetElementType() == typeof(UdonSharpBehaviour) || type.GetElementType().IsSubclassOf(typeof(UdonSharpBehaviour)) ||
type.GetElementType() == typeof(UdonBehaviour) || type.GetElementType().IsSubclassOf(typeof(UdonBehaviour))));
}
public static bool IsUserJaggedArray(System.Type type)
{
return type.IsArray && type.GetElementType().IsArray;
}
public static bool IsUserDefinedEnum(System.Type type)
{
return (type.IsEnum && !CompilerUdonInterface.IsExternType(type)) ||
(type.IsArray && type.GetElementType().IsEnum && !CompilerUdonInterface.IsExternType(type.GetElementType()));
}
public static bool IsUserDefinedType(System.Type type)
{
return IsUserDefinedBehaviour(type) ||
IsUserJaggedArray(type) ||
IsUserDefinedEnum(type);
}
private static volatile Dictionary<Type, Type> _inheritedTypeMap;
private static readonly object _inheritedTypeMapLock = new object();
private static Dictionary<Type, Type> GetInheritedTypeMap()
{
if (_inheritedTypeMap != null)
return _inheritedTypeMap;
lock (_inheritedTypeMapLock)
{
if (_inheritedTypeMap != null)
return _inheritedTypeMap;
Dictionary<Type, Type> typeMap = new Dictionary<Type, Type>();
IEnumerable<Type> typeList = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "VRCSDK3").GetTypes();
foreach (Type childType in typeList)
{
if (childType.BaseType?.Namespace != null &&
childType.BaseType.Namespace.StartsWith("VRC.SDKBase", StringComparison.Ordinal) &&
childType.BaseType.Name != "VRCNetworkBehaviour")
{
if (typeMap.ContainsKey(childType.BaseType))
{
LogWarning($"Inherited type map already contains an entry for {childType.BaseType}, existing pair: ({childType.BaseType}, {typeMap[childType.BaseType]})");
continue;
}
typeMap.Add(childType.BaseType, childType);
}
}
typeMap.Add(typeof(VRC.SDK3.Video.Components.VRCUnityVideoPlayer), typeof(VRC.SDK3.Video.Components.Base.BaseVRCVideoPlayer));
typeMap.Add(typeof(VRC.SDK3.Video.Components.AVPro.VRCAVProVideoPlayer), typeof(VRC.SDK3.Video.Components.Base.BaseVRCVideoPlayer));
_inheritedTypeMap = typeMap;
}
return _inheritedTypeMap;
}
internal static Type RemapBaseType(Type type)
{
Dictionary<Type, Type> typeMap = GetInheritedTypeMap();
int arrayDepth = 0;
Type currentType = type;
while (currentType.IsArray)
{
currentType = currentType.GetElementType();
++arrayDepth;
}
if (typeMap.ContainsKey(currentType))
{
type = typeMap[currentType];
while (arrayDepth-- > 0)
type = type.MakeArrayType();
}
return type;
}
[ThreadStatic]
private static Dictionary<Type, Type> _userTypeToUdonTypeCache;
public static Type UserTypeToUdonType(Type type)
{
if (type == null)
return null;
if (_userTypeToUdonTypeCache == null)
_userTypeToUdonTypeCache = new Dictionary<Type, Type>();
if (_userTypeToUdonTypeCache.TryGetValue(type, out Type foundType))
return foundType;
Type udonType = null;
if (IsUserDefinedType(type))
{
if (type.IsArray)
{
if (!type.GetElementType().IsArray)
{
if (IsUserDefinedEnum(type))
udonType = type.GetElementType().GetEnumUnderlyingType().MakeArrayType();
else
udonType = typeof(UnityEngine.Component[]);// Hack because VRC doesn't expose the array type of UdonBehaviour
}
else // Jagged arrays
{
udonType = typeof(object[]);
}
}
else
{
udonType = typeof(VRC.Udon.UdonBehaviour);
}
}
if (udonType == null)
udonType = type;
udonType = RemapBaseType(udonType);
_userTypeToUdonTypeCache.Add(type, udonType);
return udonType;
}
public static void LogBuildError(string message, string filePath, int line, int character)
{
MethodInfo buildErrorLogMethod = typeof(Debug).GetMethod("LogPlayerBuildError", BindingFlags.NonPublic | BindingFlags.Static);
string errorMessage = $"[<color=#FF00FF>UdonSharp</color>] {filePath}({line},{character}): {message}";
buildErrorLogMethod.Invoke(null, new object[] {
errorMessage,
filePath,
line,
character });
}
public static void Log(object message)
{
Debug.Log($"[<color=#0c824c>UdonSharp</color>] {message}");
}
public static void Log(object message, UnityEngine.Object context)
{
Debug.Log($"[<color=#0c824c>UdonSharp</color>] {message}", context);
}
public static void LogWarning(object message)
{
Debug.LogWarning($"[<color=#FF00FF>UdonSharp</color>] {message}");
}
public static void LogWarning(object message, UnityEngine.Object context)
{
Debug.LogWarning($"[<color=#FF00FF>UdonSharp</color>] {message}", context);
}
public static void LogError(object message)
{
Debug.LogError($"[<color=#FF00FF>UdonSharp</color>] {message}");
}
public static void LogError(object message, UnityEngine.Object context)
{
Debug.LogError($"[<color=#FF00FF>UdonSharp</color>] {message}", context);
}
#if !UNITY_2022_3_OR_NEWER
private static readonly MethodInfo _displayProgressBar = typeof(Editor).Assembly.GetTypes().FirstOrDefault(e => e.Name == "AsyncProgressBar")?.GetMethod("Display");
private static readonly MethodInfo _clearProgressBar = typeof(Editor).Assembly.GetTypes().FirstOrDefault(e => e.Name == "AsyncProgressBar")?.GetMethod("Clear");
#endif
private static int _progressId;
public static void ShowAsyncProgressBar(string text, float progress)
{
#if UNITY_2022_3_OR_NEWER
if (_progressId == 0)
{
_progressId = Progress.Start("U# Compile", "Compiling UdonSharp scripts...");
}
Progress.Report(_progressId, progress, text);
#else
_displayProgressBar.Invoke(null, new object[] {text, progress});
#endif
}
public static void ClearAsyncProgressBar()
{
#if UNITY_2022_3_OR_NEWER
if (_progressId == 0) return;
Progress.Remove(_progressId);
_progressId = 0;
#else
_clearProgressBar.Invoke(null, null);
#endif
}
public static void LogRuntimeError(string message, string prefix, string filePath, int line, int character)
{
MethodInfo buildErrorLogMethod = typeof(UnityEngine.Debug).GetMethod("LogPlayerBuildError", BindingFlags.NonPublic | BindingFlags.Static);
string errorMessage = $"[<color=#FF00FF>UdonSharp</color>]{prefix} {filePath}({line + 1},{character}): {message}";
buildErrorLogMethod.Invoke(null, new object[] {
errorMessage,
filePath,
line + 1,
character });
}
public static string ReadFileTextSync(string filePath, float timeoutSeconds = 2f)
{
bool sourceLoaded = false;
string fileText = "";
DateTime startTime = DateTime.Now;
while (true)
{
System.IO.IOException exception = null;
try
{
fileText = System.IO.File.ReadAllText(filePath);
sourceLoaded = true;
}
catch (System.IO.IOException e)
{
exception = e;
if (e is System.IO.FileNotFoundException ||
e is System.IO.DirectoryNotFoundException)
throw;
}
if (sourceLoaded)
break;
System.Threading.Thread.Sleep(20);
TimeSpan timeFromStart = DateTime.Now - startTime;
if (timeFromStart.TotalSeconds > timeoutSeconds)
{
Debug.LogError($"Timeout when attempting to read file {filePath}");
if (exception != null)
throw exception;
}
}
return fileText;
}
internal static string HashString(string stringToHash)
{
using (SHA1Managed sha256 = new SHA1Managed())
{
return BitConverter.ToString(sha256.ComputeHash(Encoding.UTF8.GetBytes(stringToHash))).Replace("-", "");
}
}
internal static bool AllElementsMatch<T>(IEnumerable<T> collection)
{
IEnumerable<T> enumerable = collection as T[] ?? collection.ToArray();
if (!enumerable.Any())
return true;
T firstValue = enumerable.First();
return enumerable.All(e => e.Equals(firstValue));
}
/// <summary>
/// Returns if a normal System.Object is null, and handles when a UnityEngine.Object referenced as a System.Object is null
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
internal static bool IsUnityObjectNull(this object value)
{
if (value == null)
return true;
if (value is UnityEngine.Object unityEngineObject && unityEngineObject == null)
return true;
return false;
}
internal static string[] GetProjectDefines(bool editorBuild)
{
List<string> defines = new List<string>();
foreach (string define in UnityEditor.EditorUserBuildSettings.activeScriptCompilationDefines)
{
if (!editorBuild)
if (define.StartsWith("UNITY_EDITOR"))
continue;
defines.Add(define);
}
defines.Add("COMPILER_UDONSHARP");
return defines.ToArray();
}
internal static void ShowEditorNotification(string notificationString)
{
typeof(SceneView).GetMethod("ShowNotification", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { notificationString });
}
public static bool DoesUnityProjectHaveCompileErrors()
{
Type logEntryType = typeof(Editor).Assembly.GetType("UnityEditor.LogEntries");
MethodInfo getLinesAndModeMethod = logEntryType.GetMethod("GetLinesAndModeFromEntryInternal", BindingFlags.Public | BindingFlags.Static);
bool hasCompileError = false;
int logEntryCount = (int)logEntryType.GetMethod("StartGettingEntries", BindingFlags.Public | BindingFlags.Static).Invoke(null, Array.Empty<object>());
try
{
object[] getLinesParams = { 0, 1, 0, "" };
for (int i = 0; i < logEntryCount; ++i)
{
getLinesParams[0] = i;
getLinesAndModeMethod.Invoke(null, getLinesParams);
int mode = (int)getLinesParams[2];
// 1 << 11 == ConsoleWindow.Mode.ScriptCompileError
if ((mode & (1 << 11)) != 0)
{
hasCompileError = true;
break;
}
}
}
finally
{
logEntryType.GetMethod("EndGettingEntries").Invoke(null, Array.Empty<object>());
}
return hasCompileError;
}
internal static void SetDirty(UnityEngine.Object obj)
{
EditorUtility.SetDirty(obj);
PrefabUtility.RecordPrefabInstancePropertyModifications(obj);
}
private static PropertyInfo _getLoadedAssembliesProp;
internal static IEnumerable<Assembly> GetLoadedEditorAssemblies()
{
if (_getLoadedAssembliesProp == null)
{
Assembly editorAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(e => e.GetName().Name == "UnityEditor");
Type editorAssembliesType = editorAssembly.GetType("UnityEditor.EditorAssemblies");
_getLoadedAssembliesProp = editorAssembliesType.GetProperty("loadedAssemblies", BindingFlags.Static | BindingFlags.NonPublic);
}
return (Assembly[])_getLoadedAssembliesProp.GetValue(null);
}
/// <summary>
/// Used to prevent Odin's DefaultSerializationBinder from getting a callback to register an assembly in specific cases where it will explode due to https://github.com/mono/mono/issues/20968
/// </summary>
internal class UdonSharpAssemblyLoadStripScope : IDisposable
{
Delegate[] originalDelegates;
public UdonSharpAssemblyLoadStripScope()
{
FieldInfo info = AppDomain.CurrentDomain.GetType().GetField("AssemblyLoad", BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance);
AssemblyLoadEventHandler handler = info.GetValue(AppDomain.CurrentDomain) as AssemblyLoadEventHandler;
originalDelegates = handler?.GetInvocationList();
if (originalDelegates != null)
{
foreach (Delegate del in originalDelegates)
AppDomain.CurrentDomain.AssemblyLoad -= (AssemblyLoadEventHandler)del;
}
}
public void Dispose()
{
if (originalDelegates != null)
{
foreach (Delegate del in originalDelegates)
AppDomain.CurrentDomain.AssemblyLoad += (AssemblyLoadEventHandler)del;
}
}
}
}
}