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 _unsignedTypes = new HashSet() { typeof(byte), typeof(ushort), typeof(uint), typeof(ulong), }; private static readonly HashSet _signedTypes = new HashSet() { typeof(sbyte), typeof(short), typeof(int), typeof(long), }; private static readonly HashSet _integerTypes = new HashSet() { typeof(byte), typeof(sbyte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), }; private static readonly HashSet _floatTypes = new HashSet() { 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 operatorTypes = new List(); operatorTypes.Add(targetType); System.Type currentSourceType = assignee; while (currentSourceType != null) { operatorTypes.Add(currentSourceType); currentSourceType = currentSourceType.BaseType; } foreach (System.Type operatorType in operatorTypes) { IEnumerable 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 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; } /// /// Escapes property names into something Udon allows since C# will put '<' and '>' in property backing field names /// 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 _inheritedTypeMap; private static readonly object _inheritedTypeMapLock = new object(); private static Dictionary GetInheritedTypeMap() { if (_inheritedTypeMap != null) return _inheritedTypeMap; lock (_inheritedTypeMapLock) { if (_inheritedTypeMap != null) return _inheritedTypeMap; Dictionary typeMap = new Dictionary(); IEnumerable 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 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 _userTypeToUdonTypeCache; public static Type UserTypeToUdonType(Type type) { if (type == null) return null; if (_userTypeToUdonTypeCache == null) _userTypeToUdonTypeCache = new Dictionary(); 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 = $"[UdonSharp] {filePath}({line},{character}): {message}"; buildErrorLogMethod.Invoke(null, new object[] { errorMessage, filePath, line, character }); } public static void Log(object message) { Debug.Log($"[UdonSharp] {message}"); } public static void Log(object message, UnityEngine.Object context) { Debug.Log($"[UdonSharp] {message}", context); } public static void LogWarning(object message) { Debug.LogWarning($"[UdonSharp] {message}"); } public static void LogWarning(object message, UnityEngine.Object context) { Debug.LogWarning($"[UdonSharp] {message}", context); } public static void LogError(object message) { Debug.LogError($"[UdonSharp] {message}"); } public static void LogError(object message, UnityEngine.Object context) { Debug.LogError($"[UdonSharp] {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 = $"[UdonSharp]{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(IEnumerable collection) { IEnumerable enumerable = collection as T[] ?? collection.ToArray(); if (!enumerable.Any()) return true; T firstValue = enumerable.First(); return enumerable.All(e => e.Equals(firstValue)); } /// /// Returns if a normal System.Object is null, and handles when a UnityEngine.Object referenced as a System.Object is null /// /// /// 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 defines = new List(); 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()); 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()); } return hasCompileError; } internal static void SetDirty(UnityEngine.Object obj) { EditorUtility.SetDirty(obj); PrefabUtility.RecordPrefabInstancePropertyModifications(obj); } private static PropertyInfo _getLoadedAssembliesProp; internal static IEnumerable 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); } /// /// 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 /// 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; } } } } }