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
}
}