using JetBrains.Annotations; using System.Collections.Generic; using System.IO; using UdonSharp.Compiler; using UnityEditor; using UnityEngine; using VRC.Udon.Serialization.OdinSerializer; using VRC.Udon.Serialization.OdinSerializer.Utilities; namespace UdonSharp { /// /// Handles cache data for U# that gets saved to the Library. All data this uses is intermediate generated data that is not required and can be regenerated from the source files. /// [InitializeOnLoad] public class UdonSharpEditorCache { #region Instance and serialization management [System.Serializable] struct SourceHashLookupStorage { [OdinSerialize, System.NonSerialized] public Dictionary sourceFileHashLookup; public DebugInfoType lastScriptBuildType; } private const string CACHE_DIR_PATH = "Library/UdonSharpCache/"; private const string CACHE_FILE_PATH = "Library/UdonSharpCache/UdonSharpEditorCache.asset"; public static UdonSharpEditorCache Instance => GetInstance(); static UdonSharpEditorCache _instance; static readonly object instanceLock = new object(); private static UdonSharpEditorCache GetInstance() { lock (instanceLock) { if (_instance != null) return _instance; _instance = new UdonSharpEditorCache(); if (File.Exists(CACHE_FILE_PATH)) { SourceHashLookupStorage storage = SerializationUtility.DeserializeValue(File.ReadAllBytes(CACHE_FILE_PATH), DataFormat.Binary); _instance.sourceFileHashLookup = storage.sourceFileHashLookup; _instance.LastBuildType = storage.lastScriptBuildType; } return _instance; } } static UdonSharpEditorCache() { AssemblyReloadEvents.beforeAssemblyReload += AssemblyReloadSave; } // Saves cache on play mode exit/enter and once we've entered the target mode reload the state from disk to persist the changes across play/edit mode static internal void SaveOnPlayExit(PlayModeStateChange state) { if (state == PlayModeStateChange.ExitingPlayMode || state == PlayModeStateChange.ExitingEditMode) { SaveAllCache(); } } static internal void SaveAllCache() { if (_instance != null) Instance.SaveAllCacheData(); } internal static void ResetInstance() { _instance = null; } class UdonSharpEditorCacheWriter : UnityEditor.AssetModificationProcessor { public static string[] OnWillSaveAssets(string[] paths) { Instance.SaveAllCacheData(); return paths; } public static AssetDeleteResult OnWillDeleteAsset(string assetPath, RemoveAssetOptions options) { UdonSharpProgramAsset programAsset = AssetDatabase.LoadAssetAtPath(assetPath); if (programAsset) { Instance.ClearSourceHash(programAsset); } else if(AssetDatabase.IsValidFolder(assetPath)) { string[] assetGuids = AssetDatabase.FindAssets($"t:{nameof(UdonSharpProgramAsset)}", new string[] { assetPath }); foreach (string guid in assetGuids) { programAsset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); if (programAsset) Instance.ClearSourceHash(programAsset); } } return AssetDeleteResult.DidNotDelete; } } static void AssemblyReloadSave() { Instance.SaveAllCacheData(); } void SaveAllCacheData() { if (!Directory.Exists(CACHE_DIR_PATH)) Directory.CreateDirectory(CACHE_DIR_PATH); if (_sourceDirty) { SourceHashLookupStorage storage = new SourceHashLookupStorage() { sourceFileHashLookup = _instance.sourceFileHashLookup, lastScriptBuildType = LastBuildType, }; File.WriteAllBytes(CACHE_FILE_PATH, SerializationUtility.SerializeValue(storage, DataFormat.Binary)); _sourceDirty = false; } FlushDirtyDebugInfos(); FlushUasmCache(); } #endregion #region Source file modification cache bool _sourceDirty = false; Dictionary sourceFileHashLookup = new Dictionary(); public bool IsSourceFileDirty(UdonSharpProgramAsset programAsset) { if (programAsset?.sourceCsScript == null) return false; if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(programAsset, out string programAssetGuid, out long _)) return false; // We haven't seen the source file before, so it needs to be compiled if (!sourceFileHashLookup.TryGetValue(programAssetGuid, out string sourceFileHash)) return true; string currentHash = HashSourceFile(programAsset.sourceCsScript); if (currentHash != sourceFileHash) return true; return false; } public void UpdateSourceHash(UdonSharpProgramAsset programAsset, string sourceText) { if (programAsset?.sourceCsScript == null) return; if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(programAsset, out string programAssetGuid, out long _)) return; string newHash = UdonSharpUtils.HashString(sourceText); if (sourceFileHashLookup.ContainsKey(programAssetGuid)) { if (sourceFileHashLookup[programAssetGuid] != newHash) _sourceDirty = true; sourceFileHashLookup[programAssetGuid] = newHash; } else { sourceFileHashLookup.Add(programAssetGuid, newHash); _sourceDirty = true; } } /// /// Clears the source hash, this is used when a script hits a compile error in order to allow an undo to compile the scripts. /// /// public void ClearSourceHash(UdonSharpProgramAsset programAsset) { if (programAsset?.sourceCsScript == null) return; if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(programAsset, out string programAssetGuid, out long _)) return; if (sourceFileHashLookup.ContainsKey(programAssetGuid)) { if (sourceFileHashLookup[programAssetGuid] != "") _sourceDirty = true; sourceFileHashLookup[programAssetGuid] = ""; } else { sourceFileHashLookup.Add(programAssetGuid, ""); _sourceDirty = true; } } private static string HashSourceFile(MonoScript script) { string scriptPath = AssetDatabase.GetAssetPath(script); string scriptText = ""; try { scriptText = UdonSharpUtils.ReadFileTextSync(scriptPath); } catch (System.Exception e) { scriptText = Random.value.ToString(); Debug.Log(e); } return UdonSharpUtils.HashString(scriptText); } DebugInfoType _lastBuildType = DebugInfoType.Editor; public DebugInfoType LastBuildType { get => _lastBuildType; set { if (_lastBuildType != value) _sourceDirty = true; _lastBuildType = value; } } #endregion #region Debug info cache public enum DebugInfoType { Editor, Client, } private const string DEBUG_INFO_PATH = "Library/UdonSharpCache/DebugInfo/"; ClassDebugInfo LoadDebugInfo(UdonSharpProgramAsset sourceProgram, DebugInfoType debugInfoType) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sourceProgram, out string guid, out long _)) { return null; } string debugInfoPath = $"{DEBUG_INFO_PATH}{guid}_{debugInfoType}.asset"; if (!File.Exists(debugInfoPath)) return null; ClassDebugInfo classDebugInfo = null; try { classDebugInfo = SerializationUtility.DeserializeValue(File.ReadAllBytes(debugInfoPath), DataFormat.Binary); } catch (System.Exception e) { Debug.LogError(e); return null; } return classDebugInfo; } void SaveDebugInfo(UdonSharpProgramAsset sourceProgram, DebugInfoType debugInfoType, ClassDebugInfo debugInfo) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(sourceProgram, out string guid, out long _)) { return; } string debugInfoPath = $"{DEBUG_INFO_PATH}{guid}_{debugInfoType}.asset"; if (!Directory.Exists(DEBUG_INFO_PATH)) Directory.CreateDirectory(DEBUG_INFO_PATH); File.WriteAllBytes(debugInfoPath, SerializationUtility.SerializeValue(debugInfo, DataFormat.Binary)); } Dictionary> _classDebugInfoLookup = new Dictionary>(); /// /// Gets the debug info for a given program asset. If debug info type for Client is specified when there is no client debug info, will fall back to Editor debug info. /// /// /// /// [PublicAPI] public ClassDebugInfo GetDebugInfo(UdonSharpProgramAsset sourceProgram, DebugInfoType debugInfoType) { if (!_classDebugInfoLookup.TryGetValue(sourceProgram, out var debugInfo)) { debugInfo = new Dictionary(); _classDebugInfoLookup.Add(sourceProgram, debugInfo); } if (debugInfo.TryGetValue(debugInfoType, out ClassDebugInfo info)) { return info; } ClassDebugInfo loadedInfo = LoadDebugInfo(sourceProgram, debugInfoType); if (loadedInfo != null) { debugInfo.Add(debugInfoType, loadedInfo); return loadedInfo; } if (debugInfoType == DebugInfoType.Client) { if (debugInfo.TryGetValue(DebugInfoType.Editor, out info)) return info; loadedInfo = LoadDebugInfo(sourceProgram, DebugInfoType.Editor); if (loadedInfo != null) { debugInfo.Add(DebugInfoType.Editor, loadedInfo); return loadedInfo; } } return null; } HashSet dirtyDebugInfos = new HashSet(new ReferenceEqualityComparer()); object setDebugInfoLock = new object(); public void SetDebugInfo(UdonSharpProgramAsset sourceProgram, DebugInfoType debugInfoType, ClassDebugInfo debugInfo) { lock (setDebugInfoLock) { dirtyDebugInfos.Add(debugInfo); if (!_classDebugInfoLookup.TryGetValue(sourceProgram, out var debugInfos)) { debugInfos = new Dictionary(); _classDebugInfoLookup.Add(sourceProgram, debugInfos); } if (!debugInfos.ContainsKey(debugInfoType)) debugInfos.Add(debugInfoType, debugInfo); else debugInfos[debugInfoType] = debugInfo; } } void FlushDirtyDebugInfos() { foreach (var sourceProgramInfos in _classDebugInfoLookup) { foreach (var debugInfo in sourceProgramInfos.Value) { if (dirtyDebugInfos.Contains(debugInfo.Value)) { SaveDebugInfo(sourceProgramInfos.Key, debugInfo.Key, debugInfo.Value); } } } dirtyDebugInfos.Clear(); } #endregion #region UASM cache const string UASM_DIR_PATH = "Library/UdonSharpCache/UASM/"; // UdonSharpProgramAsset GUID to uasm lookup Dictionary _uasmCache = new Dictionary(); void FlushUasmCache() { if (!Directory.Exists(UASM_DIR_PATH)) Directory.CreateDirectory(UASM_DIR_PATH); foreach (var uasmCacheEntry in _uasmCache) { string filePath = $"{UASM_DIR_PATH}{uasmCacheEntry.Key}.uasm"; File.WriteAllText(filePath, uasmCacheEntry.Value); } } /// /// Gets the uasm string for the last build of the given program asset /// /// /// [PublicAPI] public string GetUASMStr(UdonSharpProgramAsset programAsset) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(programAsset, out string guid, out long _)) return ""; if (_uasmCache.TryGetValue(guid, out string uasm)) return uasm; string filePath = $"{UASM_DIR_PATH}{guid}.uasm"; if (File.Exists(filePath)) { uasm = UdonSharpUtils.ReadFileTextSync(filePath); _uasmCache.Add(guid, uasm); return uasm; } return ""; } static object uasmSetLock = new object(); public void SetUASMStr(UdonSharpProgramAsset programAsset, string uasm) { lock (uasmSetLock) { if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(programAsset, out string guid, out long _)) return; if (_uasmCache.ContainsKey(guid)) _uasmCache[guid] = uasm; else _uasmCache.Add(guid, uasm); } } #endregion } }