270 lines
11 KiB
C#
270 lines
11 KiB
C#
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs upgrade process on all U# scripts
|
|
/// </summary>
|
|
/// <returns>True if some scripts have been updated</returns>
|
|
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<T>()
|
|
{
|
|
string fullTypeName = typeof(T).FullName;
|
|
|
|
foreach (var fieldAttribute in fieldAttributes)
|
|
{
|
|
if (TypeSymbol.GetFullTypeName(fieldAttribute.AttributeClass) == fullTypeName)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (HasAttribute<NonSerializedAttribute>() && !HasAttribute<OdinSerializeAttribute>()) return false;
|
|
|
|
return (fieldSymbol.DeclaredAccessibility == Accessibility.Public || HasAttribute<SerializeField>()) && !HasAttribute<OdinSerializeAttribute>();
|
|
}
|
|
|
|
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<AttributeListSyntax> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|