using System; using System.IO; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using UdonSharp; using UdonSharp.Compiler; using UdonSharp.Compiler.Symbols; using UnityEditor; using UnityEngine; using VRC.SDKBase; using VRC.Udon.Serialization.OdinSerializer; using VRC.Udon.Serialization.OdinSerializer.Utilities; using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; namespace UdonSharpEditor { using SyntaxTree = Microsoft.CodeAnalysis.SyntaxTree; [InitializeOnLoad] internal class UdonSharpUpgrader { static UdonSharpUpgrader() { EditorApplication.update += OnEditorUpdate; } private static void OnEditorUpdate() { if (_needsProgramUpgradePass && !EditorApplication.isCompiling && !EditorApplication.isUpdating) { UpgradeScripts(); } } [MenuItem("VRChat SDK/Udon Sharp/Force Upgrade")] internal static void ForceUpgrade() { UdonSharpProgramAsset.GetAllUdonSharpPrograms().ForEach(QueueUpgrade); UdonSharpEditorCache.Instance.QueueUpgradePass(); UdonSharpEditorManager._didSceneUpgrade = false; } private static bool _needsProgramUpgradePass; public static void QueueUpgrade(UdonSharpProgramAsset programAsset) { if (EditorApplication.isPlayingOrWillChangePlaymode) return; if (programAsset == null || programAsset.sourceCsScript == null) return; if (programAsset.ScriptVersion >= UdonSharpProgramVersion.CurrentVersion) return; _needsProgramUpgradePass = true; } /// /// Runs upgrade process on all U# scripts /// /// True if some scripts have been updated internal static bool UpgradeScripts() { bool upgraded = UpgradeScripts(UdonSharpProgramAsset.GetAllUdonSharpPrograms()); _needsProgramUpgradePass = false; return upgraded; } private static int _assemblyCounter; private static bool UpgradeScripts(UdonSharpProgramAsset[] programAssets) { if (programAssets.Length == 0) return false; if (programAssets.All(e => e.ScriptVersion >= UdonSharpProgramVersion.CurrentVersion)) return false; CompilationContext compilationContext = new CompilationContext(new UdonSharpCompileOptions()); ModuleBinding[] bindings = compilationContext.LoadSyntaxTreesAndCreateModules(CompilationContext.GetAllFilteredSourcePaths(false), UdonSharpUtils.GetProjectDefines(false)); CSharpCompilation compilation = CSharpCompilation.Create( $"UdonSharpRoslynUpgradeAssembly{_assemblyCounter++}", bindings.Select(e => e.tree), CompilationContext.GetMetadataReferences(), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); bool scriptUpgraded = false; bool versionUpgraded = false; foreach (var programAsset in programAssets) { string assetPath = AssetDatabase.GetAssetPath(programAsset.sourceCsScript); ModuleBinding binding = bindings.FirstOrDefault(e => e.filePath == assetPath); if (binding == null) continue; if (programAsset.ScriptVersion < UdonSharpProgramVersion.V1SerializationUpdate) { SyntaxTree bindingTree = binding.tree; SemanticModel bindingModel = compilation.GetSemanticModel(bindingTree); SerializationUpdateSyntaxRewriter rewriter = new SerializationUpdateSyntaxRewriter(bindingModel); SyntaxNode newRoot = rewriter.Visit(bindingTree.GetRoot()); if (rewriter.Modified) { try { File.WriteAllText(binding.filePath, newRoot.ToFullString(), Encoding.UTF8); scriptUpgraded = true; UdonSharpUtils.Log($"Upgraded field serialization attributes on U# script '{binding.filePath}'", programAsset.sourceCsScript); } catch (Exception e) { UdonSharpUtils.LogError($"Could not upgrade U# script, exception: {e}"); } } // We expect this to come through a second time after scripts have been updated and change the version on the asset. else { programAsset.ScriptVersion = UdonSharpProgramVersion.V1SerializationUpdate; EditorUtility.SetDirty(programAsset); versionUpgraded = true; } } } if (scriptUpgraded) { AssetDatabase.Refresh(); return true; } if (versionUpgraded) { UdonSharpCompilerV1.CompileSync(new UdonSharpCompileOptions()); } return false; } private class SerializationUpdateSyntaxRewriter : CSharpSyntaxRewriter { public bool Modified { get; private set; } private readonly SemanticModel model; public SerializationUpdateSyntaxRewriter(SemanticModel model) { this.model = model; } private static bool IsFieldSerializedWithoutOdin(IFieldSymbol fieldSymbol) { if (fieldSymbol.IsConst) return false; if (fieldSymbol.IsStatic) return false; if (fieldSymbol.IsReadOnly) return false; var fieldAttributes = fieldSymbol.GetAttributes(); bool HasAttribute() { string fullTypeName = typeof(T).FullName; foreach (var fieldAttribute in fieldAttributes) { if (TypeSymbol.GetFullTypeName(fieldAttribute.AttributeClass) == fullTypeName) return true; } return false; } if (HasAttribute() && !HasAttribute()) return false; return (fieldSymbol.DeclaredAccessibility == Accessibility.Public || HasAttribute()) && !HasAttribute(); } public override SyntaxNode VisitFieldDeclaration(FieldDeclarationSyntax node) { FieldDeclarationSyntax fieldDeclaration = (FieldDeclarationSyntax)base.VisitFieldDeclaration(node); var typeInfo = model.GetTypeInfo(node.Declaration.Type); if (typeInfo.Type == null) { UdonSharpUtils.LogWarning($"Could not find symbol for {node}"); return fieldDeclaration; } ITypeSymbol rootType = typeInfo.Type; while (rootType.TypeKind == TypeKind.Array) rootType = ((IArrayTypeSymbol)rootType).ElementType; if (rootType.TypeKind == TypeKind.Error || rootType.TypeKind == TypeKind.Unknown) { UdonSharpUtils.LogWarning($"Type {typeInfo.Type} for field '{fieldDeclaration.Declaration}' is invalid"); return fieldDeclaration; } IFieldSymbol firstFieldSymbol = (IFieldSymbol)model.GetDeclaredSymbol(node.Declaration.Variables.First()); rootType = firstFieldSymbol.Type; // If the field is not serialized or is using Odin already, we don't need to do anything. if (!IsFieldSerializedWithoutOdin(firstFieldSymbol)) return fieldDeclaration; // Getting the type may fail if it's a user type that hasn't compiled on the C# side yet. For now we skip it, but we should do a simplified check for jagged arrays if (!TypeSymbol.TryGetSystemType(rootType, out Type systemType)) return fieldDeclaration; // If Unity can serialize the type, we're good if (UnitySerializationUtility.GuessIfUnityWillSerialize(systemType)) return fieldDeclaration; // Common type that gets picked up as serialized but shouldn't be // todo: Add actual checking for if a type is serializable, which isn't consistent. Unity/System library types in large part are serializable but don't have the System.Serializable tag, but types outside those assemblies need the tag to be serialized. if (systemType == typeof(VRCPlayerApi) || systemType == typeof(VRCPlayerApi[])) return fieldDeclaration; Modified = true; NameSyntax odinSerializeName = IdentifierName("VRC"); odinSerializeName = QualifiedName(odinSerializeName, IdentifierName("Udon")); odinSerializeName = QualifiedName(odinSerializeName, IdentifierName("Serialization")); odinSerializeName = QualifiedName(odinSerializeName, IdentifierName("OdinSerializer")); odinSerializeName = QualifiedName(odinSerializeName, IdentifierName("OdinSerialize")); // Somehow it seems like there's literally no decent way to maintain the indent on inserted code so we'll just inline the comment because Roslyn is dumb SyntaxTrivia commentTrivia = Comment(" /* UdonSharp auto-upgrade: serialization */ "); AttributeListSyntax newAttribList = AttributeList(SeparatedList(new [] { Attribute(odinSerializeName)})).WithTrailingTrivia(commentTrivia); SyntaxList attributeList = fieldDeclaration.AttributeLists; if (attributeList.Count > 0) { SyntaxTriviaList trailingTrivia = attributeList.Last().GetTrailingTrivia(); trailingTrivia = trailingTrivia.Insert(0, commentTrivia); attributeList.Replace(attributeList[attributeList.Count - 1], attributeList[attributeList.Count -1].WithoutTrailingTrivia()); newAttribList = newAttribList.WithTrailingTrivia(trailingTrivia); } else { newAttribList = newAttribList.WithLeadingTrivia(fieldDeclaration.GetLeadingTrivia()); fieldDeclaration = fieldDeclaration.WithoutLeadingTrivia(); } attributeList = attributeList.Add(newAttribList); return fieldDeclaration.WithAttributeLists(attributeList); } } } }